css refactor
This commit is contained in:
@@ -5,30 +5,40 @@
|
||||
* @PURPOSE: Redirect to Dashboard Hub as per UX requirements
|
||||
* @LAYER: UI
|
||||
* @INVARIANT: Always redirects to /dashboards
|
||||
*
|
||||
*
|
||||
* @UX_STATE: Loading -> Shows loading indicator
|
||||
* @UX_FEEDBACK: Redirects to /dashboards
|
||||
*/
|
||||
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { onMount } from "svelte";
|
||||
import { goto } from "$app/navigation";
|
||||
|
||||
onMount(() => {
|
||||
// Redirect to Dashboard Hub as per UX requirements
|
||||
goto('/dashboards', { replaceState: true });
|
||||
goto("/dashboards", { replaceState: true });
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.loading {
|
||||
@apply flex items-center justify-center min-h-screen;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="loading">
|
||||
<svg class="animate-spin h-8 w-8 text-blue-600" 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>
|
||||
<div class="flex items-center justify-center min-h-screen">
|
||||
<svg
|
||||
class="animate-spin h-8 w-8 text-blue-600"
|
||||
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>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -230,7 +230,5 @@
|
||||
<!-- [/SECTION: TEMPLATE] -->
|
||||
</ProtectedRoute>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
|
||||
<!-- [/DEF:AdminRolesPage:Component] -->
|
||||
@@ -346,7 +346,5 @@
|
||||
<!-- [/SECTION: TEMPLATE] -->
|
||||
</ProtectedRoute>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
|
||||
<!-- [/DEF:AdminSettingsPage:Component] -->
|
||||
@@ -278,7 +278,5 @@
|
||||
<!-- [/SECTION: TEMPLATE] -->
|
||||
</ProtectedRoute>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
|
||||
<!-- [/DEF:AdminUsersPage:Component] -->
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* @LAYER: UI
|
||||
* @RELATION: BINDS_TO -> sidebarStore
|
||||
* @INVARIANT: Always shows dataset details when loaded
|
||||
*
|
||||
*
|
||||
* @UX_STATE: Loading -> Shows skeleton loader
|
||||
* @UX_STATE: Loaded -> Shows dataset details with columns and linked dashboards
|
||||
* @UX_STATE: Error -> Shows error banner with retry button
|
||||
@@ -14,16 +14,16 @@
|
||||
* @UX_RECOVERY: Refresh button reloads dataset details
|
||||
*/
|
||||
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { t } from '$lib/i18n';
|
||||
import { api } from '$lib/api.js';
|
||||
import { openDrawerForTask } from '$lib/stores/taskDrawer.js';
|
||||
import { onMount } from "svelte";
|
||||
import { goto } from "$app/navigation";
|
||||
import { page } from "$app/stores";
|
||||
import { t } from "$lib/i18n";
|
||||
import { api } from "$lib/api.js";
|
||||
import { openDrawerForTask } from "$lib/stores/taskDrawer.js";
|
||||
|
||||
// Get dataset ID from URL params
|
||||
$: datasetId = $page.params.id;
|
||||
$: envId = $page.url.searchParams.get('env_id') || '';
|
||||
$: envId = $page.url.searchParams.get("env_id") || "";
|
||||
|
||||
// State
|
||||
let dataset = null;
|
||||
@@ -38,19 +38,19 @@
|
||||
// Load dataset details from API
|
||||
async function loadDatasetDetail() {
|
||||
if (!datasetId || !envId) {
|
||||
error = 'Missing dataset ID or environment ID';
|
||||
error = "Missing dataset ID or environment ID";
|
||||
isLoading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
isLoading = true;
|
||||
error = null;
|
||||
try {
|
||||
const response = await api.getDatasetDetail(envId, datasetId);
|
||||
dataset = response;
|
||||
} catch (err) {
|
||||
error = err.message || 'Failed to load dataset details';
|
||||
console.error('[DatasetDetail][Coherence:Failed]', err);
|
||||
error = err.message || "Failed to load dataset details";
|
||||
console.error("[DatasetDetail][Coherence:Failed]", err);
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
@@ -68,291 +68,242 @@
|
||||
|
||||
// Get column type icon/color
|
||||
function getColumnTypeClass(type) {
|
||||
if (!type) return 'text-gray-500';
|
||||
if (!type) return "text-gray-500";
|
||||
const lowerType = type.toLowerCase();
|
||||
if (lowerType.includes('int') || lowerType.includes('float') || lowerType.includes('num')) {
|
||||
return 'text-blue-600 bg-blue-50';
|
||||
} else if (lowerType.includes('date') || lowerType.includes('time')) {
|
||||
return 'text-green-600 bg-green-50';
|
||||
} else if (lowerType.includes('str') || lowerType.includes('text') || lowerType.includes('char')) {
|
||||
return 'text-purple-600 bg-purple-50';
|
||||
} else if (lowerType.includes('bool')) {
|
||||
return 'text-orange-600 bg-orange-50';
|
||||
if (
|
||||
lowerType.includes("int") ||
|
||||
lowerType.includes("float") ||
|
||||
lowerType.includes("num")
|
||||
) {
|
||||
return "text-blue-600 bg-blue-50";
|
||||
} else if (lowerType.includes("date") || lowerType.includes("time")) {
|
||||
return "text-green-600 bg-green-50";
|
||||
} else if (
|
||||
lowerType.includes("str") ||
|
||||
lowerType.includes("text") ||
|
||||
lowerType.includes("char")
|
||||
) {
|
||||
return "text-purple-600 bg-purple-50";
|
||||
} else if (lowerType.includes("bool")) {
|
||||
return "text-orange-600 bg-orange-50";
|
||||
}
|
||||
return 'text-gray-600 bg-gray-50';
|
||||
return "text-gray-600 bg-gray-50";
|
||||
}
|
||||
|
||||
// Get mapping progress percentage
|
||||
function getMappingProgress(column) {
|
||||
// Placeholder: In real implementation, this would check if column has mapping
|
||||
return column.description ? 100 : 0;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
@apply max-w-7xl mx-auto px-4 py-6;
|
||||
}
|
||||
|
||||
.header {
|
||||
@apply flex items-center justify-between mb-6;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
@apply flex items-center gap-2 text-gray-600 hover:text-gray-900 transition-colors;
|
||||
}
|
||||
|
||||
.title {
|
||||
@apply text-2xl font-bold text-gray-900;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
@apply text-sm text-gray-500 mt-1;
|
||||
}
|
||||
|
||||
.detail-grid {
|
||||
@apply grid grid-cols-1 lg:grid-cols-3 gap-6;
|
||||
}
|
||||
|
||||
.detail-card {
|
||||
@apply bg-white border border-gray-200 rounded-lg p-6;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
@apply text-lg font-semibold text-gray-900 mb-4;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
@apply flex justify-between py-2 border-b border-gray-100 last:border-0;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
@apply text-sm text-gray-500;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
@apply text-sm font-medium text-gray-900;
|
||||
}
|
||||
|
||||
.columns-section {
|
||||
@apply lg:col-span-2;
|
||||
}
|
||||
|
||||
.columns-grid {
|
||||
@apply grid grid-cols-1 md:grid-cols-2 gap-3;
|
||||
}
|
||||
|
||||
.column-item {
|
||||
@apply p-3 border border-gray-200 rounded-lg hover:border-blue-300 transition-colors;
|
||||
}
|
||||
|
||||
.column-header {
|
||||
@apply flex items-center justify-between mb-2;
|
||||
}
|
||||
|
||||
.column-name {
|
||||
@apply font-medium text-gray-900;
|
||||
}
|
||||
|
||||
.column-type {
|
||||
@apply text-xs px-2 py-1 rounded;
|
||||
}
|
||||
|
||||
.column-meta {
|
||||
@apply flex items-center gap-2 text-xs text-gray-500;
|
||||
}
|
||||
|
||||
.column-description {
|
||||
@apply text-sm text-gray-600 mt-2;
|
||||
}
|
||||
|
||||
.mapping-badge {
|
||||
@apply inline-flex items-center px-2 py-0.5 text-xs rounded-full;
|
||||
}
|
||||
|
||||
.mapping-badge.mapped {
|
||||
@apply bg-green-100 text-green-800;
|
||||
}
|
||||
|
||||
.mapping-badge.unmapped {
|
||||
@apply bg-gray-100 text-gray-600;
|
||||
}
|
||||
|
||||
.linked-dashboards-list {
|
||||
@apply space-y-2;
|
||||
}
|
||||
|
||||
.linked-dashboard-item {
|
||||
@apply flex items-center gap-3 p-3 border border-gray-200 rounded-lg hover:bg-gray-50 cursor-pointer transition-colors;
|
||||
}
|
||||
|
||||
.dashboard-icon {
|
||||
@apply w-8 h-8 bg-blue-100 rounded-lg flex items-center justify-center text-blue-600;
|
||||
}
|
||||
|
||||
.dashboard-info {
|
||||
@apply flex-1;
|
||||
}
|
||||
|
||||
.dashboard-title {
|
||||
@apply font-medium text-gray-900;
|
||||
}
|
||||
|
||||
.dashboard-id {
|
||||
@apply text-xs text-gray-500;
|
||||
}
|
||||
|
||||
.sql-section {
|
||||
@apply mt-6;
|
||||
}
|
||||
|
||||
.sql-code {
|
||||
@apply bg-gray-900 text-gray-100 p-4 rounded-lg overflow-x-auto text-sm font-mono;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
@apply py-8 text-center text-gray-500;
|
||||
}
|
||||
|
||||
.skeleton {
|
||||
@apply animate-pulse bg-gray-200 rounded;
|
||||
}
|
||||
|
||||
.error-banner {
|
||||
@apply bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4 flex items-center justify-between;
|
||||
}
|
||||
|
||||
.retry-btn {
|
||||
@apply px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700 transition-colors;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="container">
|
||||
<div class="max-w-7xl mx-auto px-4 py-6">
|
||||
<!-- Header -->
|
||||
<div class="header">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<button class="back-btn" on:click={goBack}>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M19 12H5M12 19l-7-7 7-7"/>
|
||||
<button
|
||||
class="flex items-center gap-2 text-gray-600 hover:text-gray-900 transition-colors"
|
||||
on:click={goBack}
|
||||
>
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M19 12H5M12 19l-7-7 7-7" />
|
||||
</svg>
|
||||
{$t.common?.back || 'Back to Datasets'}
|
||||
{$t.common?.back || "Back to Datasets"}
|
||||
</button>
|
||||
{#if dataset}
|
||||
<h1 class="title mt-4">{dataset.table_name}</h1>
|
||||
<p class="subtitle">{dataset.schema} • {dataset.database}</p>
|
||||
<h1 class="text-2xl font-bold text-gray-900 mt-4">
|
||||
{dataset.table_name}
|
||||
</h1>
|
||||
<p class="text-sm text-gray-500 mt-1">
|
||||
{dataset.schema} • {dataset.database}
|
||||
</p>
|
||||
{:else if !isLoading}
|
||||
<h1 class="title mt-4">{$t.datasets?.detail_title || 'Dataset Details'}</h1>
|
||||
<h1 class="text-2xl font-bold text-gray-900 mt-4">
|
||||
{$t.datasets?.detail_title || "Dataset Details"}
|
||||
</h1>
|
||||
{/if}
|
||||
</div>
|
||||
<button class="retry-btn" on:click={loadDatasetDetail}>
|
||||
{$t.common?.refresh || 'Refresh'}
|
||||
<button
|
||||
class="px-4 py-2 bg-destructive text-white rounded hover:bg-destructive-hover transition-colors"
|
||||
on:click={loadDatasetDetail}
|
||||
>
|
||||
{$t.common?.refresh || "Refresh"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Error Banner -->
|
||||
{#if error}
|
||||
<div class="error-banner">
|
||||
<div
|
||||
class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4 flex items-center justify-between"
|
||||
>
|
||||
<span>{error}</span>
|
||||
<button class="retry-btn" on:click={loadDatasetDetail}>
|
||||
{$t.common?.retry || 'Retry'}
|
||||
<button
|
||||
class="px-4 py-2 bg-destructive text-white rounded hover:bg-destructive-hover transition-colors"
|
||||
on:click={loadDatasetDetail}
|
||||
>
|
||||
{$t.common?.retry || "Retry"}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Loading State -->
|
||||
{#if isLoading}
|
||||
<div class="detail-grid">
|
||||
<div class="detail-card">
|
||||
<div class="skeleton h-6 w-1/2 mb-4"></div>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<div class="bg-white border border-gray-200 rounded-lg p-6">
|
||||
<div class="animate-pulse bg-gray-200 rounded h-6 w-1/2 mb-4"></div>
|
||||
{#each Array(5) as _}
|
||||
<div class="info-row">
|
||||
<div class="skeleton h-4 w-20"></div>
|
||||
<div class="skeleton h-4 w-32"></div>
|
||||
<div
|
||||
class="flex justify-between py-2 border-b border-gray-100 last:border-0"
|
||||
>
|
||||
<div class="animate-pulse bg-gray-200 rounded h-4 w-20"></div>
|
||||
<div class="animate-pulse bg-gray-200 rounded h-4 w-32"></div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="detail-card columns-section">
|
||||
<div class="skeleton h-6 w-1/3 mb-4"></div>
|
||||
<div class="columns-grid">
|
||||
<div class="bg-white border border-gray-200 rounded-lg p-6 lg:col-span-2">
|
||||
<div class="animate-pulse bg-gray-200 rounded h-6 w-1/3 mb-4"></div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
{#each Array(4) as _}
|
||||
<div class="column-item">
|
||||
<div class="skeleton h-4 w-full mb-2"></div>
|
||||
<div class="skeleton h-3 w-16"></div>
|
||||
<div class="p-3 border border-gray-200 rounded-lg">
|
||||
<div
|
||||
class="animate-pulse bg-gray-200 rounded h-4 w-full mb-2"
|
||||
></div>
|
||||
<div class="animate-pulse bg-gray-200 rounded h-3 w-16"></div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if dataset}
|
||||
<div class="detail-grid">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<!-- Dataset Info Card -->
|
||||
<div class="detail-card">
|
||||
<h2 class="card-title">{$t.datasets?.info || 'Dataset Information'}</h2>
|
||||
<div class="info-row">
|
||||
<span class="info-label">{$t.datasets?.table_name || 'Table Name'}</span>
|
||||
<span class="info-value">{dataset.table_name}</span>
|
||||
<div class="bg-white border border-gray-200 rounded-lg p-6">
|
||||
<h2 class="text-lg font-semibold text-gray-900 mb-4">
|
||||
{$t.datasets?.info || "Dataset Information"}
|
||||
</h2>
|
||||
<div class="flex justify-between py-2 border-b border-gray-100">
|
||||
<span class="text-sm text-gray-500"
|
||||
>{$t.datasets?.table_name || "Table Name"}</span
|
||||
>
|
||||
<span class="text-sm font-medium text-gray-900"
|
||||
>{dataset.table_name}</span
|
||||
>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">{$t.datasets?.schema || 'Schema'}</span>
|
||||
<span class="info-value">{dataset.schema || '-'}</span>
|
||||
<div class="flex justify-between py-2 border-b border-gray-100">
|
||||
<span class="text-sm text-gray-500"
|
||||
>{$t.datasets?.schema || "Schema"}</span
|
||||
>
|
||||
<span class="text-sm font-medium text-gray-900"
|
||||
>{dataset.schema || "-"}</span
|
||||
>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">{$t.datasets?.database || 'Database'}</span>
|
||||
<span class="info-value">{dataset.database}</span>
|
||||
<div class="flex justify-between py-2 border-b border-gray-100">
|
||||
<span class="text-sm text-gray-500"
|
||||
>{$t.datasets?.database || "Database"}</span
|
||||
>
|
||||
<span class="text-sm font-medium text-gray-900"
|
||||
>{dataset.database}</span
|
||||
>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">{$t.datasets?.columns_count || 'Columns'}</span>
|
||||
<span class="info-value">{dataset.column_count}</span>
|
||||
<div class="flex justify-between py-2 border-b border-gray-100">
|
||||
<span class="text-sm text-gray-500"
|
||||
>{$t.datasets?.columns_count || "Columns"}</span
|
||||
>
|
||||
<span class="text-sm font-medium text-gray-900"
|
||||
>{dataset.column_count}</span
|
||||
>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">{$t.datasets?.linked_dashboards || 'Linked Dashboards'}</span>
|
||||
<span class="info-value">{dataset.linked_dashboard_count}</span>
|
||||
<div class="flex justify-between py-2 border-b border-gray-100">
|
||||
<span class="text-sm text-gray-500"
|
||||
>{$t.datasets?.linked_dashboards || "Linked Dashboards"}</span
|
||||
>
|
||||
<span class="text-sm font-medium text-gray-900"
|
||||
>{dataset.linked_dashboard_count}</span
|
||||
>
|
||||
</div>
|
||||
{#if dataset.is_sqllab_view}
|
||||
<div class="info-row">
|
||||
<span class="info-label">{$t.datasets?.type || 'Type'}</span>
|
||||
<span class="info-value">SQL Lab View</span>
|
||||
<div class="flex justify-between py-2 border-b border-gray-100">
|
||||
<span class="text-sm text-gray-500"
|
||||
>{$t.datasets?.type || "Type"}</span
|
||||
>
|
||||
<span class="text-sm font-medium text-gray-900">SQL Lab View</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if dataset.created_on}
|
||||
<div class="info-row">
|
||||
<span class="info-label">{$t.datasets?.created || 'Created'}</span>
|
||||
<span class="info-value">{new Date(dataset.created_on).toLocaleDateString()}</span>
|
||||
<div class="flex justify-between py-2 border-b border-gray-100">
|
||||
<span class="text-sm text-gray-500"
|
||||
>{$t.datasets?.created || "Created"}</span
|
||||
>
|
||||
<span class="text-sm font-medium text-gray-900"
|
||||
>{new Date(dataset.created_on).toLocaleDateString()}</span
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
{#if dataset.changed_on}
|
||||
<div class="info-row">
|
||||
<span class="info-label">{$t.datasets?.updated || 'Updated'}</span>
|
||||
<span class="info-value">{new Date(dataset.changed_on).toLocaleDateString()}</span>
|
||||
<div class="flex justify-between py-2">
|
||||
<span class="text-sm text-gray-500"
|
||||
>{$t.datasets?.updated || "Updated"}</span
|
||||
>
|
||||
<span class="text-sm font-medium text-gray-900"
|
||||
>{new Date(dataset.changed_on).toLocaleDateString()}</span
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Linked Dashboards Card -->
|
||||
{#if dataset.linked_dashboards && dataset.linked_dashboards.length > 0}
|
||||
<div class="detail-card">
|
||||
<h2 class="card-title">{$t.datasets?.linked_dashboards || 'Linked Dashboards'} ({dataset.linked_dashboard_count})</h2>
|
||||
<div class="linked-dashboards-list">
|
||||
<div class="bg-white border border-gray-200 rounded-lg p-6">
|
||||
<h2 class="text-lg font-semibold text-gray-900 mb-4">
|
||||
{$t.datasets?.linked_dashboards || "Linked Dashboards"} ({dataset.linked_dashboard_count})
|
||||
</h2>
|
||||
<div class="space-y-2">
|
||||
{#each dataset.linked_dashboards as dashboard}
|
||||
<div
|
||||
class="linked-dashboard-item"
|
||||
<div
|
||||
class="flex items-center gap-3 p-3 border border-gray-200 rounded-lg hover:bg-gray-50 cursor-pointer transition-colors"
|
||||
on:click={() => navigateToDashboard(dashboard.id)}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div class="dashboard-icon">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
|
||||
<line x1="3" y1="9" x2="21" y2="9"/>
|
||||
<line x1="9" y1="21" x2="9" y2="9"/>
|
||||
<div
|
||||
class="w-8 h-8 bg-blue-100 rounded-lg flex items-center justify-center text-blue-600"
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
|
||||
<line x1="3" y1="9" x2="21" y2="9" />
|
||||
<line x1="9" y1="21" x2="9" y2="9" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="dashboard-info">
|
||||
<div class="dashboard-title">{dashboard.title}</div>
|
||||
<div class="dashboard-id">ID: {dashboard.id}{#if dashboard.slug} • {dashboard.slug}{/if}</div>
|
||||
<div class="flex-1">
|
||||
<div class="font-medium text-gray-900">{dashboard.title}</div>
|
||||
<div class="text-xs text-gray-500">
|
||||
ID: {dashboard.id}{#if dashboard.slug}
|
||||
• {dashboard.slug}{/if}
|
||||
</div>
|
||||
</div>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="text-gray-400">
|
||||
<path d="M9 18l6-6-6-6"/>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
class="text-gray-400"
|
||||
>
|
||||
<path d="M9 18l6-6-6-6" />
|
||||
</svg>
|
||||
</div>
|
||||
{/each}
|
||||
@@ -361,58 +312,82 @@
|
||||
{/if}
|
||||
|
||||
<!-- Columns Card -->
|
||||
<div class="detail-card columns-section">
|
||||
<h2 class="card-title">{$t.datasets?.columns || 'Columns'} ({dataset.column_count})</h2>
|
||||
<div class="bg-white border border-gray-200 rounded-lg p-6 lg:col-span-2">
|
||||
<h2 class="text-lg font-semibold text-gray-900 mb-4">
|
||||
{$t.datasets?.columns || "Columns"} ({dataset.column_count})
|
||||
</h2>
|
||||
{#if dataset.columns && dataset.columns.length > 0}
|
||||
<div class="columns-grid">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
{#each dataset.columns as column}
|
||||
<div class="column-item">
|
||||
<div class="column-header">
|
||||
<span class="column-name">{column.name}</span>
|
||||
<div
|
||||
class="p-3 border border-gray-200 rounded-lg hover:border-blue-300 transition-colors"
|
||||
>
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span class="font-medium text-gray-900">{column.name}</span>
|
||||
{#if column.type}
|
||||
<span class="column-type {getColumnTypeClass(column.type)}">{column.type}</span>
|
||||
<span
|
||||
class="text-xs px-2 py-1 rounded {getColumnTypeClass(
|
||||
column.type,
|
||||
)}">{column.type}</span
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="column-meta">
|
||||
<div class="flex items-center gap-2 text-xs text-gray-500">
|
||||
{#if column.is_dttm}
|
||||
<span class="text-xs text-green-600">📅 Date/Time</span>
|
||||
{/if}
|
||||
{#if !column.is_active}
|
||||
<span class="text-xs text-gray-400">(Inactive)</span>
|
||||
{/if}
|
||||
<span class="mapping-badge {column.description ? 'mapped' : 'unmapped'}">
|
||||
{column.description ? '✓ Mapped' : 'Unmapped'}
|
||||
<span
|
||||
class="inline-flex items-center px-2 py-0.5 text-xs rounded-full {column.description
|
||||
? 'bg-green-100 text-green-800'
|
||||
: 'bg-gray-100 text-gray-600'}"
|
||||
>
|
||||
{column.description ? "✓ Mapped" : "Unmapped"}
|
||||
</span>
|
||||
</div>
|
||||
{#if column.description}
|
||||
<p class="column-description">{column.description}</p>
|
||||
<p class="text-sm text-gray-600 mt-2">{column.description}</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="empty-state">
|
||||
{$t.datasets?.no_columns || 'No columns found'}
|
||||
<div class="py-8 text-center text-gray-500">
|
||||
{$t.datasets?.no_columns || "No columns found"}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- SQL Section (for SQL Lab views) -->
|
||||
{#if dataset.sql}
|
||||
<div class="detail-card sql-section lg:col-span-3">
|
||||
<h2 class="card-title">{$t.datasets?.sql_query || 'SQL Query'}</h2>
|
||||
<pre class="sql-code">{dataset.sql}</pre>
|
||||
<div
|
||||
class="bg-white border border-gray-200 rounded-lg p-6 mt-6 lg:col-span-3"
|
||||
>
|
||||
<h2 class="text-lg font-semibold text-gray-900 mb-4">
|
||||
{$t.datasets?.sql_query || "SQL Query"}
|
||||
</h2>
|
||||
<pre
|
||||
class="bg-gray-900 text-gray-100 p-4 rounded-lg overflow-x-auto text-sm font-mono">{dataset.sql}</pre>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="empty-state">
|
||||
<svg class="w-16 h-16 mx-auto mb-4 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M3 3h18v18H3V3zm16 16V5H5v14h14z"/>
|
||||
<div class="py-8 text-center text-gray-500">
|
||||
<svg
|
||||
class="w-16 h-16 mx-auto mb-4 text-gray-400"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M3 3h18v18H3V3zm16 16V5H5v14h14z" />
|
||||
</svg>
|
||||
<p>{$t.datasets?.not_found || 'Dataset not found'}</p>
|
||||
<p>{$t.datasets?.not_found || "Dataset not found"}</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- [/DEF:DatasetDetail:Page] -->
|
||||
<!-- [/DEF:DatasetDetail:Page] -->
|
||||
|
||||
@@ -154,8 +154,5 @@
|
||||
</div>
|
||||
<!-- [/SECTION: TEMPLATE] -->
|
||||
|
||||
<style>
|
||||
/* No additional styles needed, using Tailwind */
|
||||
</style>
|
||||
|
||||
<!-- [/DEF:LoginPage:Component] -->
|
||||
@@ -384,8 +384,5 @@
|
||||
|
||||
<!-- [/SECTION] -->
|
||||
|
||||
<style>
|
||||
/* Page specific styles */
|
||||
</style>
|
||||
|
||||
<!-- [/DEF:MigrationDashboard:Component] -->
|
||||
|
||||
@@ -173,8 +173,5 @@
|
||||
</div>
|
||||
<!-- [/SECTION] -->
|
||||
|
||||
<style>
|
||||
/* Page specific styles */
|
||||
</style>
|
||||
|
||||
<!-- [/DEF:MappingManagement:Component] -->
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* @LAYER: UI
|
||||
* @RELATION: BINDS_TO -> sidebarStore
|
||||
* @INVARIANT: Always shows tabbed interface with all settings categories
|
||||
*
|
||||
*
|
||||
* @UX_STATE: Loading -> Shows skeleton loader
|
||||
* @UX_STATE: Loaded -> Shows tabbed settings interface
|
||||
* @UX_STATE: Error -> Shows error banner with retry button
|
||||
@@ -14,32 +14,32 @@
|
||||
* @UX_RECOVERY: Refresh button reloads settings data
|
||||
*/
|
||||
|
||||
import { onMount } from 'svelte';
|
||||
import { t } from '$lib/i18n';
|
||||
import { api } from '$lib/api.js';
|
||||
import { addToast } from '$lib/toasts';
|
||||
import ProviderConfig from '../../components/llm/ProviderConfig.svelte';
|
||||
import { onMount } from "svelte";
|
||||
import { t } from "$lib/i18n";
|
||||
import { api } from "$lib/api.js";
|
||||
import { addToast } from "$lib/toasts";
|
||||
import ProviderConfig from "../../components/llm/ProviderConfig.svelte";
|
||||
|
||||
// State
|
||||
let activeTab = 'environments';
|
||||
let activeTab = "environments";
|
||||
let settings = null;
|
||||
let isLoading = true;
|
||||
let error = null;
|
||||
|
||||
|
||||
// Environment editing state
|
||||
let editingEnvId = null;
|
||||
let isAddingEnv = false;
|
||||
let newEnv = {
|
||||
id: '',
|
||||
name: '',
|
||||
url: '',
|
||||
username: '',
|
||||
password: '',
|
||||
id: "",
|
||||
name: "",
|
||||
url: "",
|
||||
username: "",
|
||||
password: "",
|
||||
is_default: false,
|
||||
backup_schedule: {
|
||||
enabled: false,
|
||||
cron_expression: '0 0 * * *'
|
||||
}
|
||||
cron_expression: "0 0 * * *",
|
||||
},
|
||||
};
|
||||
|
||||
// Load settings on mount
|
||||
@@ -55,8 +55,8 @@
|
||||
const response = await api.getConsolidatedSettings();
|
||||
settings = response;
|
||||
} catch (err) {
|
||||
error = err.message || 'Failed to load settings';
|
||||
console.error('[SettingsPage][Coherence:Failed]', err);
|
||||
error = err.message || "Failed to load settings";
|
||||
console.error("[SettingsPage][Coherence:Failed]", err);
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
@@ -70,39 +70,42 @@
|
||||
// Get tab class
|
||||
function getTabClass(tab) {
|
||||
return activeTab === tab
|
||||
? 'text-blue-600 border-b-2 border-blue-600'
|
||||
: 'text-gray-600 hover:text-gray-800 border-transparent hover:border-gray-300';
|
||||
? "text-blue-600 border-b-2 border-blue-600"
|
||||
: "text-gray-600 hover:text-gray-800 border-transparent hover:border-gray-300";
|
||||
}
|
||||
|
||||
// Handle global settings save (Logging, Storage)
|
||||
async function handleSave() {
|
||||
console.log('[SettingsPage][Action] Saving settings');
|
||||
console.log("[SettingsPage][Action] Saving settings");
|
||||
try {
|
||||
// In a real app we might want to only send the changed section,
|
||||
// In a real app we might want to only send the changed section,
|
||||
// but updateConsolidatedSettings expects full object or we can use specific endpoints.
|
||||
// For now we use the consolidated update.
|
||||
await api.updateConsolidatedSettings(settings);
|
||||
addToast($t.settings?.save_success || 'Settings saved', 'success');
|
||||
addToast($t.settings?.save_success || "Settings saved", "success");
|
||||
} catch (err) {
|
||||
console.error('[SettingsPage][Coherence:Failed]', err);
|
||||
addToast($t.settings?.save_failed || 'Failed to save settings', 'error');
|
||||
console.error("[SettingsPage][Coherence:Failed]", err);
|
||||
addToast($t.settings?.save_failed || "Failed to save settings", "error");
|
||||
}
|
||||
}
|
||||
|
||||
// Handle environment actions
|
||||
async function handleTestEnv(id) {
|
||||
console.log(`[SettingsPage][Action] Test environment ${id}`);
|
||||
addToast('Testing connection...', 'info');
|
||||
addToast("Testing connection...", "info");
|
||||
try {
|
||||
const result = await api.testEnvironmentConnection(id);
|
||||
if (result.status === 'success') {
|
||||
addToast('Connection successful', 'success');
|
||||
if (result.status === "success") {
|
||||
addToast("Connection successful", "success");
|
||||
} else {
|
||||
addToast(`Connection failed: ${result.message}`, 'error');
|
||||
addToast(`Connection failed: ${result.message}`, "error");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[SettingsPage][Coherence:Failed] Error testing connection:', err);
|
||||
addToast('Failed to test connection', 'error');
|
||||
console.error(
|
||||
"[SettingsPage][Coherence:Failed] Error testing connection:",
|
||||
err,
|
||||
);
|
||||
addToast("Failed to test connection", "error");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,7 +114,7 @@
|
||||
newEnv = JSON.parse(JSON.stringify(env)); // Deep copy
|
||||
// Ensure backup_schedule exists
|
||||
if (!newEnv.backup_schedule) {
|
||||
newEnv.backup_schedule = { enabled: false, cron_expression: '0 0 * * *' };
|
||||
newEnv.backup_schedule = { enabled: false, cron_expression: "0 0 * * *" };
|
||||
}
|
||||
editingEnvId = env.id;
|
||||
isAddingEnv = false;
|
||||
@@ -119,185 +122,177 @@
|
||||
|
||||
function resetEnvForm() {
|
||||
newEnv = {
|
||||
id: '',
|
||||
name: '',
|
||||
url: '',
|
||||
username: '',
|
||||
password: '',
|
||||
id: "",
|
||||
name: "",
|
||||
url: "",
|
||||
username: "",
|
||||
password: "",
|
||||
is_default: false,
|
||||
backup_schedule: {
|
||||
enabled: false,
|
||||
cron_expression: '0 0 * * *'
|
||||
}
|
||||
cron_expression: "0 0 * * *",
|
||||
},
|
||||
};
|
||||
editingEnvId = null;
|
||||
}
|
||||
|
||||
async function handleAddOrUpdateEnv() {
|
||||
try {
|
||||
console.log(`[SettingsPage][Action] ${editingEnvId ? 'Updating' : 'Adding'} environment.`);
|
||||
|
||||
// Basic validation
|
||||
if (!newEnv.id || !newEnv.name || !newEnv.url) {
|
||||
addToast('Please fill in all required fields (ID, Name, URL)', 'error');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
console.log(
|
||||
`[SettingsPage][Action] ${editingEnvId ? "Updating" : "Adding"} environment.`,
|
||||
);
|
||||
|
||||
if (editingEnvId) {
|
||||
await api.updateEnvironment(editingEnvId, newEnv);
|
||||
addToast('Environment updated', 'success');
|
||||
} else {
|
||||
await api.addEnvironment(newEnv);
|
||||
addToast('Environment added', 'success');
|
||||
}
|
||||
|
||||
resetEnvForm();
|
||||
editingEnvId = null;
|
||||
isAddingEnv = false;
|
||||
await loadSettings();
|
||||
} catch (error) {
|
||||
console.error("[SettingsPage][Coherence:Failed] Failed to save environment:", error);
|
||||
addToast(error.message || 'Failed to save environment', 'error');
|
||||
// Basic validation
|
||||
if (!newEnv.id || !newEnv.name || !newEnv.url) {
|
||||
addToast("Please fill in all required fields (ID, Name, URL)", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
if (editingEnvId) {
|
||||
await api.updateEnvironment(editingEnvId, newEnv);
|
||||
addToast("Environment updated", "success");
|
||||
} else {
|
||||
await api.addEnvironment(newEnv);
|
||||
addToast("Environment added", "success");
|
||||
}
|
||||
|
||||
resetEnvForm();
|
||||
editingEnvId = null;
|
||||
isAddingEnv = false;
|
||||
await loadSettings();
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"[SettingsPage][Coherence:Failed] Failed to save environment:",
|
||||
error,
|
||||
);
|
||||
addToast(error.message || "Failed to save environment", "error");
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDeleteEnv(id) {
|
||||
if (confirm('Are you sure you want to delete this environment?')) {
|
||||
if (confirm("Are you sure you want to delete this environment?")) {
|
||||
console.log(`[SettingsPage][Action] Delete environment ${id}`);
|
||||
try {
|
||||
await api.deleteEnvironment(id);
|
||||
addToast('Environment deleted', 'success');
|
||||
await loadSettings();
|
||||
await api.deleteEnvironment(id);
|
||||
addToast("Environment deleted", "success");
|
||||
await loadSettings();
|
||||
} catch (error) {
|
||||
console.error("[SettingsPage][Coherence:Failed] Failed to delete environment:", error);
|
||||
addToast('Failed to delete environment', 'error');
|
||||
console.error(
|
||||
"[SettingsPage][Coherence:Failed] Failed to delete environment:",
|
||||
error,
|
||||
);
|
||||
addToast("Failed to delete environment", "error");
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
@apply max-w-7xl mx-auto px-4 py-6;
|
||||
}
|
||||
|
||||
.header {
|
||||
@apply flex items-center justify-between mb-6;
|
||||
}
|
||||
|
||||
.title {
|
||||
@apply text-2xl font-bold text-gray-900;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
@apply px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors;
|
||||
}
|
||||
|
||||
.error-banner {
|
||||
@apply bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4 flex items-center justify-between;
|
||||
}
|
||||
|
||||
.retry-btn {
|
||||
@apply px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700 transition-colors;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
@apply border-b border-gray-200 mb-6;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
@apply px-4 py-2 text-sm font-medium transition-colors focus:outline-none;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
@apply bg-white rounded-lg p-6 border border-gray-200;
|
||||
}
|
||||
|
||||
.skeleton {
|
||||
@apply animate-pulse bg-gray-200 rounded;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="container">
|
||||
<div class="max-w-7xl mx-auto px-4 py-6">
|
||||
<!-- Header -->
|
||||
<div class="header">
|
||||
<h1 class="title">{$t.settings?.title || 'Settings'}</h1>
|
||||
<button class="refresh-btn" on:click={loadSettings}>
|
||||
{$t.common?.refresh || 'Refresh'}
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h1 class="text-2xl font-bold text-gray-900">
|
||||
{$t.settings?.title || "Settings"}
|
||||
</h1>
|
||||
<button
|
||||
class="px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary-hover transition-colors"
|
||||
on:click={loadSettings}
|
||||
>
|
||||
{$t.common?.refresh || "Refresh"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Error Banner -->
|
||||
{#if error}
|
||||
<div class="error-banner">
|
||||
<div
|
||||
class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4 flex items-center justify-between"
|
||||
>
|
||||
<span>{error}</span>
|
||||
<button class="retry-btn" on:click={loadSettings}>
|
||||
{$t.common?.retry || 'Retry'}
|
||||
<button
|
||||
class="px-4 py-2 bg-destructive text-white rounded hover:bg-destructive-hover transition-colors"
|
||||
on:click={loadSettings}
|
||||
>
|
||||
{$t.common?.retry || "Retry"}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Loading State -->
|
||||
{#if isLoading}
|
||||
<div class="tab-content">
|
||||
<div class="skeleton h-8"></div>
|
||||
<div class="skeleton h-8"></div>
|
||||
<div class="skeleton h-8"></div>
|
||||
<div class="skeleton h-8"></div>
|
||||
<div class="skeleton h-8"></div>
|
||||
<div class="bg-white rounded-lg p-6 border border-gray-200">
|
||||
<div class="animate-pulse bg-gray-200 rounded h-8"></div>
|
||||
<div class="animate-pulse bg-gray-200 rounded h-8"></div>
|
||||
<div class="animate-pulse bg-gray-200 rounded h-8"></div>
|
||||
<div class="animate-pulse bg-gray-200 rounded h-8"></div>
|
||||
<div class="animate-pulse bg-gray-200 rounded h-8"></div>
|
||||
</div>
|
||||
{:else if settings}
|
||||
<!-- Tabs -->
|
||||
<div class="tabs">
|
||||
<button
|
||||
class="tab-btn {getTabClass('environments')}"
|
||||
on:click={() => handleTabChange('environments')}
|
||||
<div class="border-b border-gray-200 mb-6">
|
||||
<button
|
||||
class="px-4 py-2 text-sm font-medium transition-colors focus:outline-none {getTabClass(
|
||||
'environments',
|
||||
)}"
|
||||
on:click={() => handleTabChange("environments")}
|
||||
>
|
||||
{$t.settings?.environments || 'Environments'}
|
||||
{$t.settings?.environments || "Environments"}
|
||||
</button>
|
||||
<button
|
||||
class="tab-btn {getTabClass('logging')}"
|
||||
on:click={() => handleTabChange('logging')}
|
||||
<button
|
||||
class="px-4 py-2 text-sm font-medium transition-colors focus:outline-none {getTabClass(
|
||||
'logging',
|
||||
)}"
|
||||
on:click={() => handleTabChange("logging")}
|
||||
>
|
||||
{$t.settings?.logging || 'Logging'}
|
||||
{$t.settings?.logging || "Logging"}
|
||||
</button>
|
||||
<button
|
||||
class="tab-btn {getTabClass('connections')}"
|
||||
on:click={() => handleTabChange('connections')}
|
||||
<button
|
||||
class="px-4 py-2 text-sm font-medium transition-colors focus:outline-none {getTabClass(
|
||||
'connections',
|
||||
)}"
|
||||
on:click={() => handleTabChange("connections")}
|
||||
>
|
||||
{$t.settings?.connections || 'Connections'}
|
||||
{$t.settings?.connections || "Connections"}
|
||||
</button>
|
||||
<button
|
||||
class="tab-btn {getTabClass('llm')}"
|
||||
on:click={() => handleTabChange('llm')}
|
||||
<button
|
||||
class="px-4 py-2 text-sm font-medium transition-colors focus:outline-none {getTabClass(
|
||||
'llm',
|
||||
)}"
|
||||
on:click={() => handleTabChange("llm")}
|
||||
>
|
||||
{$t.settings?.llm || 'LLM'}
|
||||
{$t.settings?.llm || "LLM"}
|
||||
</button>
|
||||
<button
|
||||
class="tab-btn {getTabClass('storage')}"
|
||||
on:click={() => handleTabChange('storage')}
|
||||
<button
|
||||
class="px-4 py-2 text-sm font-medium transition-colors focus:outline-none {getTabClass(
|
||||
'storage',
|
||||
)}"
|
||||
on:click={() => handleTabChange("storage")}
|
||||
>
|
||||
{$t.settings?.storage || 'Storage'}
|
||||
{$t.settings?.storage || "Storage"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Tab Content -->
|
||||
<div class="tab-content">
|
||||
{#if activeTab === 'environments'}
|
||||
<div class="bg-white rounded-lg p-6 border border-gray-200">
|
||||
{#if activeTab === "environments"}
|
||||
<!-- Environments Tab -->
|
||||
<div class="text-lg font-medium mb-4">
|
||||
<h2 class="text-xl font-bold mb-4">{$t.settings?.environments || 'Superset Environments'}</h2>
|
||||
<h2 class="text-xl font-bold mb-4">
|
||||
{$t.settings?.environments || "Superset Environments"}
|
||||
</h2>
|
||||
<p class="text-gray-600 mb-6">
|
||||
{$t.settings?.env_description || 'Configure Superset environments for dashboards and datasets.'}
|
||||
{$t.settings?.env_description ||
|
||||
"Configure Superset environments for dashboards and datasets."}
|
||||
</p>
|
||||
|
||||
|
||||
{#if !editingEnvId && !isAddingEnv}
|
||||
<div class="flex justify-end mb-6">
|
||||
<button
|
||||
<button
|
||||
class="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700"
|
||||
on:click={() => { isAddingEnv = true; resetEnvForm(); }}
|
||||
on:click={() => {
|
||||
isAddingEnv = true;
|
||||
resetEnvForm();
|
||||
}}
|
||||
>
|
||||
{$t.settings?.env_add || 'Add Environment'}
|
||||
{$t.settings?.env_add || "Add Environment"}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -305,65 +300,138 @@
|
||||
{#if editingEnvId || isAddingEnv}
|
||||
<!-- Add/Edit Environment Form -->
|
||||
<div class="bg-gray-50 p-6 rounded-lg mb-6 border border-gray-200">
|
||||
<h3 class="text-lg font-medium mb-4">{editingEnvId ? 'Edit' : 'Add'} Environment</h3>
|
||||
<h3 class="text-lg font-medium mb-4">
|
||||
{editingEnvId ? "Edit" : "Add"} Environment
|
||||
</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="env_id" class="block text-sm font-medium text-gray-700">ID</label>
|
||||
<input
|
||||
type="text"
|
||||
id="env_id"
|
||||
bind:value={newEnv.id}
|
||||
disabled={!!editingEnvId}
|
||||
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2 disabled:bg-gray-100 disabled:text-gray-500"
|
||||
<label
|
||||
for="env_id"
|
||||
class="block text-sm font-medium text-gray-700">ID</label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
id="env_id"
|
||||
bind:value={newEnv.id}
|
||||
disabled={!!editingEnvId}
|
||||
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2 disabled:bg-gray-100 disabled:text-gray-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="env_name" class="block text-sm font-medium text-gray-700">Name</label>
|
||||
<input type="text" id="env_name" bind:value={newEnv.name} class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2" />
|
||||
<label
|
||||
for="env_name"
|
||||
class="block text-sm font-medium text-gray-700">Name</label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
id="env_name"
|
||||
bind:value={newEnv.name}
|
||||
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="env_url" class="block text-sm font-medium text-gray-700">URL</label>
|
||||
<input type="text" id="env_url" bind:value={newEnv.url} class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2" />
|
||||
<label
|
||||
for="env_url"
|
||||
class="block text-sm font-medium text-gray-700">URL</label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
id="env_url"
|
||||
bind:value={newEnv.url}
|
||||
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="env_user" class="block text-sm font-medium text-gray-700">Username</label>
|
||||
<input type="text" id="env_user" bind:value={newEnv.username} class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2" />
|
||||
<label
|
||||
for="env_user"
|
||||
class="block text-sm font-medium text-gray-700"
|
||||
>Username</label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
id="env_user"
|
||||
bind:value={newEnv.username}
|
||||
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="env_pass" class="block text-sm font-medium text-gray-700">Password</label>
|
||||
<input type="password" id="env_pass" bind:value={newEnv.password} class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2" />
|
||||
<label
|
||||
for="env_pass"
|
||||
class="block text-sm font-medium text-gray-700"
|
||||
>Password</label
|
||||
>
|
||||
<input
|
||||
type="password"
|
||||
id="env_pass"
|
||||
bind:value={newEnv.password}
|
||||
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center mt-6">
|
||||
<input type="checkbox" id="env_default" bind:checked={newEnv.is_default} class="h-4 w-4 text-blue-600 border-gray-300 rounded" />
|
||||
<label for="env_default" class="ml-2 block text-sm text-gray-900">Default Environment</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="env_default"
|
||||
bind:checked={newEnv.is_default}
|
||||
class="h-4 w-4 text-blue-600 border-gray-300 rounded"
|
||||
/>
|
||||
<label
|
||||
for="env_default"
|
||||
class="ml-2 block text-sm text-gray-900"
|
||||
>Default Environment</label
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="text-lg font-medium mb-4 mt-6">Backup Schedule</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="flex items-center">
|
||||
<input type="checkbox" id="backup_enabled" bind:checked={newEnv.backup_schedule.enabled} class="h-4 w-4 text-blue-600 border-gray-300 rounded" />
|
||||
<label for="backup_enabled" class="ml-2 block text-sm text-gray-900">Enable Automatic Backups</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="backup_enabled"
|
||||
bind:checked={newEnv.backup_schedule.enabled}
|
||||
class="h-4 w-4 text-blue-600 border-gray-300 rounded"
|
||||
/>
|
||||
<label
|
||||
for="backup_enabled"
|
||||
class="ml-2 block text-sm text-gray-900"
|
||||
>Enable Automatic Backups</label
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<label for="cron_expression" class="block text-sm font-medium text-gray-700">Cron Expression</label>
|
||||
<input type="text" id="cron_expression" bind:value={newEnv.backup_schedule.cron_expression} placeholder="0 0 * * *" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2" />
|
||||
<p class="text-xs text-gray-500 mt-1">Example: 0 0 * * * (daily at midnight)</p>
|
||||
<label
|
||||
for="cron_expression"
|
||||
class="block text-sm font-medium text-gray-700"
|
||||
>Cron Expression</label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
id="cron_expression"
|
||||
bind:value={newEnv.backup_schedule.cron_expression}
|
||||
placeholder="0 0 * * *"
|
||||
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2"
|
||||
/>
|
||||
<p class="text-xs text-gray-500 mt-1">
|
||||
Example: 0 0 * * * (daily at midnight)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex gap-2 justify-end">
|
||||
<button
|
||||
on:click={() => { isAddingEnv = false; editingEnvId = null; resetEnvForm(); }}
|
||||
<button
|
||||
on:click={() => {
|
||||
isAddingEnv = false;
|
||||
editingEnvId = null;
|
||||
resetEnvForm();
|
||||
}}
|
||||
class="bg-gray-200 text-gray-700 px-4 py-2 rounded hover:bg-gray-300"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
on:click={handleAddOrUpdateEnv}
|
||||
<button
|
||||
on:click={handleAddOrUpdateEnv}
|
||||
class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"
|
||||
>
|
||||
{editingEnvId ? 'Update' : 'Add'} Environment
|
||||
{editingEnvId ? "Update" : "Add"} Environment
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -374,11 +442,26 @@
|
||||
<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.connections?.name || "Name"}</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">URL</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{$t.connections?.user || "Username"}</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Default</th>
|
||||
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">{$t.settings?.env_actions || "Actions"}</th>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
>{$t.connections?.name || "Name"}</th
|
||||
>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
>URL</th
|
||||
>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
>{$t.connections?.user || "Username"}</th
|
||||
>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
>Default</th
|
||||
>
|
||||
<th
|
||||
class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
>{$t.settings?.env_actions || "Actions"}</th
|
||||
>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
@@ -386,24 +469,38 @@
|
||||
<tr>
|
||||
<td class="px-6 py-4 whitespace-nowrap">{env.name}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">{env.url}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">{env.username}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">{env.username}</td
|
||||
>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
{#if env.is_default}
|
||||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">
|
||||
<span
|
||||
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800"
|
||||
>
|
||||
Yes
|
||||
</span>
|
||||
{:else}
|
||||
<span class="text-gray-500">No</span>
|
||||
{/if}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<button class="text-green-600 hover:text-green-900 mr-4" on:click={() => handleTestEnv(env.id)}>
|
||||
<td
|
||||
class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium"
|
||||
>
|
||||
<button
|
||||
class="text-green-600 hover:text-green-900 mr-4"
|
||||
on:click={() => handleTestEnv(env.id)}
|
||||
>
|
||||
{$t.settings?.env_test || "Test"}
|
||||
</button>
|
||||
<button class="text-indigo-600 hover:text-indigo-900 mr-4" on:click={() => editEnv(env)}>
|
||||
<button
|
||||
class="text-indigo-600 hover:text-indigo-900 mr-4"
|
||||
on:click={() => editEnv(env)}
|
||||
>
|
||||
{$t.common.edit || "Edit"}
|
||||
</button>
|
||||
<button class="text-red-600 hover:text-red-900" on:click={() => handleDeleteEnv(env.id)}>
|
||||
<button
|
||||
class="text-red-600 hover:text-red-900"
|
||||
on:click={() => handleDeleteEnv(env.id)}
|
||||
>
|
||||
{$t.settings?.env_delete || "Delete"}
|
||||
</button>
|
||||
</td>
|
||||
@@ -413,25 +510,41 @@
|
||||
</table>
|
||||
</div>
|
||||
{:else if !isAddingEnv}
|
||||
<div class="mb-4 p-4 bg-yellow-100 border-l-4 border-yellow-500 text-yellow-700">
|
||||
<div
|
||||
class="mb-4 p-4 bg-yellow-100 border-l-4 border-yellow-500 text-yellow-700"
|
||||
>
|
||||
<p class="font-bold">Warning</p>
|
||||
<p>No Superset environments configured. You must add at least one environment to perform backups or migrations.</p>
|
||||
<p>
|
||||
No Superset environments configured. You must add at least one
|
||||
environment to perform backups or migrations.
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if activeTab === 'logging'}
|
||||
{:else if activeTab === "logging"}
|
||||
<!-- Logging Tab -->
|
||||
<div class="text-lg font-medium mb-4">
|
||||
<h2 class="text-xl font-bold mb-4">{$t.settings?.logging || 'Logging Configuration'}</h2>
|
||||
<h2 class="text-xl font-bold mb-4">
|
||||
{$t.settings?.logging || "Logging Configuration"}
|
||||
</h2>
|
||||
<p class="text-gray-600 mb-6">
|
||||
{$t.settings?.logging_description || 'Configure logging and task log levels.'}
|
||||
{$t.settings?.logging_description ||
|
||||
"Configure logging and task log levels."}
|
||||
</p>
|
||||
|
||||
|
||||
<div class="bg-gray-50 p-6 rounded-lg border border-gray-200">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="log_level" class="block text-sm font-medium text-gray-700">Log Level</label>
|
||||
<select id="log_level" bind:value={settings.logging.level} class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2">
|
||||
<label
|
||||
for="log_level"
|
||||
class="block text-sm font-medium text-gray-700"
|
||||
>Log Level</label
|
||||
>
|
||||
<select
|
||||
id="log_level"
|
||||
bind:value={settings.logging.level}
|
||||
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2"
|
||||
>
|
||||
<option value="DEBUG">DEBUG</option>
|
||||
<option value="INFO">INFO</option>
|
||||
<option value="WARNING">WARNING</option>
|
||||
@@ -440,8 +553,16 @@
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="task_log_level" class="block text-sm font-medium text-gray-700">Task Log Level</label>
|
||||
<select id="task_log_level" bind:value={settings.logging.task_log_level} class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2">
|
||||
<label
|
||||
for="task_log_level"
|
||||
class="block text-sm font-medium text-gray-700"
|
||||
>Task Log Level</label
|
||||
>
|
||||
<select
|
||||
id="task_log_level"
|
||||
bind:value={settings.logging.task_log_level}
|
||||
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2"
|
||||
>
|
||||
<option value="DEBUG">DEBUG</option>
|
||||
<option value="INFO">INFO</option>
|
||||
<option value="WARNING">WARNING</option>
|
||||
@@ -450,16 +571,25 @@
|
||||
</div>
|
||||
<div class="md:col-span-2">
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" id="enable_belief_state" bind:checked={settings.logging.enable_belief_state} class="h-4 w-4 text-blue-600 border-gray-300 rounded" />
|
||||
<span class="ml-2 block text-sm text-gray-900">Enable Belief State Logging (Beta)</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="enable_belief_state"
|
||||
bind:checked={settings.logging.enable_belief_state}
|
||||
class="h-4 w-4 text-blue-600 border-gray-300 rounded"
|
||||
/>
|
||||
<span class="ml-2 block text-sm text-gray-900"
|
||||
>Enable Belief State Logging (Beta)</span
|
||||
>
|
||||
</label>
|
||||
<p class="text-xs text-gray-500 mt-1 ml-6">Logs agent reasoning and internal state changes for debugging.</p>
|
||||
<p class="text-xs text-gray-500 mt-1 ml-6">
|
||||
Logs agent reasoning and internal state changes for debugging.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex justify-end">
|
||||
<button
|
||||
on:click={handleSave}
|
||||
<button
|
||||
on:click={handleSave}
|
||||
class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"
|
||||
>
|
||||
Save Logging Config
|
||||
@@ -467,68 +597,114 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if activeTab === 'connections'}
|
||||
{:else if activeTab === "connections"}
|
||||
<!-- Connections Tab -->
|
||||
<div class="text-lg font-medium mb-4">
|
||||
<h2 class="text-xl font-bold mb-4">{$t.settings?.connections || 'Database Connections'}</h2>
|
||||
<h2 class="text-xl font-bold mb-4">
|
||||
{$t.settings?.connections || "Database Connections"}
|
||||
</h2>
|
||||
<p class="text-gray-600 mb-6">
|
||||
{$t.settings?.connections_description || 'Configure database connections for data mapping.'}
|
||||
{$t.settings?.connections_description ||
|
||||
"Configure database connections for data mapping."}
|
||||
</p>
|
||||
|
||||
|
||||
{#if settings.connections && settings.connections.length > 0}
|
||||
<!-- Connections list would go here -->
|
||||
<p class="text-gray-500 italic">No additional connections configured. Superset database connections are used by default.</p>
|
||||
<!-- Connections list would go here -->
|
||||
<p class="text-gray-500 italic">
|
||||
No additional connections configured. Superset database
|
||||
connections are used by default.
|
||||
</p>
|
||||
{:else}
|
||||
<div class="text-center py-8 bg-gray-50 rounded-lg border border-dashed border-gray-300">
|
||||
<p class="text-gray-500">No external connections configured.</p>
|
||||
<button class="mt-4 px-4 py-2 border border-blue-600 text-blue-600 rounded hover:bg-blue-50">
|
||||
Add Connection
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="text-center py-8 bg-gray-50 rounded-lg border border-dashed border-gray-300"
|
||||
>
|
||||
<p class="text-gray-500">No external connections configured.</p>
|
||||
<button
|
||||
class="mt-4 px-4 py-2 border border-blue-600 text-blue-600 rounded hover:bg-blue-50"
|
||||
>
|
||||
Add Connection
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if activeTab === 'llm'}
|
||||
{:else if activeTab === "llm"}
|
||||
<!-- LLM Tab -->
|
||||
<div class="text-lg font-medium mb-4">
|
||||
<h2 class="text-xl font-bold mb-4">{$t.settings?.llm || 'LLM Providers'}</h2>
|
||||
<h2 class="text-xl font-bold mb-4">
|
||||
{$t.settings?.llm || "LLM Providers"}
|
||||
</h2>
|
||||
<p class="text-gray-600 mb-6">
|
||||
{$t.settings?.llm_description || 'Configure LLM providers for dataset documentation.'}
|
||||
{$t.settings?.llm_description ||
|
||||
"Configure LLM providers for dataset documentation."}
|
||||
</p>
|
||||
|
||||
<ProviderConfig providers={settings.llm_providers || []} onSave={loadSettings} />
|
||||
|
||||
<ProviderConfig
|
||||
providers={settings.llm_providers || []}
|
||||
onSave={loadSettings}
|
||||
/>
|
||||
</div>
|
||||
{:else if activeTab === 'storage'}
|
||||
{:else if activeTab === "storage"}
|
||||
<!-- Storage Tab -->
|
||||
<div class="text-lg font-medium mb-4">
|
||||
<h2 class="text-xl font-bold mb-4">{$t.settings?.storage || 'File Storage Configuration'}</h2>
|
||||
<h2 class="text-xl font-bold mb-4">
|
||||
{$t.settings?.storage || "File Storage Configuration"}
|
||||
</h2>
|
||||
<p class="text-gray-600 mb-6">
|
||||
{$t.settings?.storage_description || 'Configure file storage paths and patterns.'}
|
||||
{$t.settings?.storage_description ||
|
||||
"Configure file storage paths and patterns."}
|
||||
</p>
|
||||
|
||||
|
||||
<div class="bg-gray-50 p-6 rounded-lg border border-gray-200">
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
<div>
|
||||
<label for="storage_path" class="block text-sm font-medium text-gray-700">Root Path</label>
|
||||
<input type="text" id="storage_path" bind:value={settings.storage.root_path} class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="backup_path" class="block text-sm font-medium text-gray-700">Backup Path</label>
|
||||
<input type="text" id="backup_path" bind:value={settings.storage.backup_path} class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="repo_path" class="block text-sm font-medium text-gray-700">Repository Path</label>
|
||||
<input type="text" id="repo_path" bind:value={settings.storage.repo_path} class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex justify-end">
|
||||
<button
|
||||
on:click={() => handleSave()}
|
||||
class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"
|
||||
>
|
||||
Save Storage Config
|
||||
</button>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
<div>
|
||||
<label
|
||||
for="storage_path"
|
||||
class="block text-sm font-medium text-gray-700"
|
||||
>Root Path</label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
id="storage_path"
|
||||
bind:value={settings.storage.root_path}
|
||||
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
for="backup_path"
|
||||
class="block text-sm font-medium text-gray-700"
|
||||
>Backup Path</label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
id="backup_path"
|
||||
bind:value={settings.storage.backup_path}
|
||||
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
for="repo_path"
|
||||
class="block text-sm font-medium text-gray-700"
|
||||
>Repository Path</label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
id="repo_path"
|
||||
bind:value={settings.storage.repo_path}
|
||||
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex justify-end">
|
||||
<button
|
||||
on:click={() => handleSave()}
|
||||
class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"
|
||||
>
|
||||
Save Storage Config
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -175,8 +175,5 @@
|
||||
</div>
|
||||
<!-- [/SECTION: TEMPLATE] -->
|
||||
|
||||
<style>
|
||||
/* Styles are handled by Tailwind */
|
||||
</style>
|
||||
|
||||
<!-- [/DEF:GitSettingsPage:Component] -->
|
||||
@@ -218,8 +218,5 @@
|
||||
</div>
|
||||
<!-- [/SECTION: TEMPLATE] -->
|
||||
|
||||
<style>
|
||||
/* ... */
|
||||
</style>
|
||||
|
||||
<!-- [/DEF:StoragePage:Component] -->
|
||||
Reference in New Issue
Block a user