css refactor
This commit is contained in:
@@ -11,19 +11,18 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
// [SECTION: IMPORTS]
|
// [SECTION: IMPORTS]
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from "svelte";
|
||||||
import type { DashboardMetadata } from '../types/dashboard';
|
import type { DashboardMetadata } from "../types/dashboard";
|
||||||
import { t } from '../lib/i18n';
|
import { t } from "../lib/i18n";
|
||||||
import { Button, Input } from '../lib/ui';
|
import { Button, Input } from "../lib/ui";
|
||||||
import GitManager from './git/GitManager.svelte';
|
import GitManager from "./git/GitManager.svelte";
|
||||||
import { api } from '../lib/api';
|
import { api } from "../lib/api";
|
||||||
import { addToast as toast } from '../lib/toasts.js';
|
import { addToast as toast } from "../lib/toasts.js";
|
||||||
// [/SECTION]
|
// [/SECTION]
|
||||||
|
|
||||||
// [SECTION: PROPS]
|
// [SECTION: PROPS]
|
||||||
export let dashboards: DashboardMetadata[] = [];
|
let { dashboards = [], selectedIds = [], environmentId = "ss1" } = $props();
|
||||||
export let selectedIds: number[] = [];
|
|
||||||
export let environmentId: string = "ss1";
|
|
||||||
// [/SECTION]
|
// [/SECTION]
|
||||||
|
|
||||||
// [SECTION: STATE]
|
// [SECTION: STATE]
|
||||||
@@ -59,26 +58,29 @@
|
|||||||
// Or we pick the first active one.
|
// Or we pick the first active one.
|
||||||
|
|
||||||
// Fetch active provider first
|
// Fetch active provider first
|
||||||
const providers = await api.fetchApi('/llm/providers');
|
const providers = await api.fetchApi("/llm/providers");
|
||||||
const activeProvider = providers.find((p: any) => p.is_active);
|
const activeProvider = providers.find((p: any) => p.is_active);
|
||||||
|
|
||||||
if (!activeProvider) {
|
if (!activeProvider) {
|
||||||
toast('No active LLM provider found. Please configure one in settings.', 'error');
|
toast(
|
||||||
|
"No active LLM provider found. Please configure one in settings.",
|
||||||
|
"error",
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await api.postApi('/tasks', {
|
await api.postApi("/tasks", {
|
||||||
plugin_id: 'llm_dashboard_validation',
|
plugin_id: "llm_dashboard_validation",
|
||||||
params: {
|
params: {
|
||||||
dashboard_id: dashboard.id.toString(),
|
dashboard_id: dashboard.id.toString(),
|
||||||
environment_id: environmentId,
|
environment_id: environmentId,
|
||||||
provider_id: activeProvider.id
|
provider_id: activeProvider.id,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
toast('Validation task started', 'success');
|
toast("Validation task started", "success");
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
toast(e.message || 'Validation failed to start', 'error');
|
toast(e.message || "Validation failed to start", "error");
|
||||||
} finally {
|
} finally {
|
||||||
validatingIds.delete(dashboard.id);
|
validatingIds.delete(dashboard.id);
|
||||||
validatingIds = validatingIds;
|
validatingIds = validatingIds;
|
||||||
@@ -87,11 +89,14 @@
|
|||||||
// [/DEF:handleValidate:Function]
|
// [/DEF:handleValidate:Function]
|
||||||
|
|
||||||
// [SECTION: DERIVED]
|
// [SECTION: DERIVED]
|
||||||
$: filteredDashboards = dashboards.filter(d =>
|
let filteredDashboards = $derived(
|
||||||
d.title.toLowerCase().includes(filterText.toLowerCase())
|
dashboards.filter((d) =>
|
||||||
|
d.title.toLowerCase().includes(filterText.toLowerCase()),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
$: sortedDashboards = [...filteredDashboards].sort((a, b) => {
|
let sortedDashboards = $derived(
|
||||||
|
[...filteredDashboards].sort((a, b) => {
|
||||||
let aVal = a[sortColumn];
|
let aVal = a[sortColumn];
|
||||||
let bVal = b[sortColumn];
|
let bVal = b[sortColumn];
|
||||||
if (sortColumn === "id") {
|
if (sortColumn === "id") {
|
||||||
@@ -101,17 +106,25 @@
|
|||||||
if (aVal < bVal) return sortDirection === "asc" ? -1 : 1;
|
if (aVal < bVal) return sortDirection === "asc" ? -1 : 1;
|
||||||
if (aVal > bVal) return sortDirection === "asc" ? 1 : -1;
|
if (aVal > bVal) return sortDirection === "asc" ? 1 : -1;
|
||||||
return 0;
|
return 0;
|
||||||
});
|
}),
|
||||||
|
|
||||||
$: paginatedDashboards = sortedDashboards.slice(
|
|
||||||
currentPage * pageSize,
|
|
||||||
(currentPage + 1) * pageSize
|
|
||||||
);
|
);
|
||||||
|
|
||||||
$: totalPages = Math.ceil(sortedDashboards.length / pageSize);
|
let paginatedDashboards = $derived(
|
||||||
|
sortedDashboards.slice(
|
||||||
|
currentPage * pageSize,
|
||||||
|
(currentPage + 1) * pageSize,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
$: allSelected = paginatedDashboards.length > 0 && paginatedDashboards.every(d => selectedIds.includes(d.id));
|
let totalPages = $derived(Math.ceil(sortedDashboards.length / pageSize));
|
||||||
$: someSelected = paginatedDashboards.some(d => selectedIds.includes(d.id));
|
|
||||||
|
let allSelected = $derived(
|
||||||
|
paginatedDashboards.length > 0 &&
|
||||||
|
paginatedDashboards.every((d) => selectedIds.includes(d.id)),
|
||||||
|
);
|
||||||
|
let someSelected = $derived(
|
||||||
|
paginatedDashboards.some((d) => selectedIds.includes(d.id)),
|
||||||
|
);
|
||||||
// [/SECTION]
|
// [/SECTION]
|
||||||
|
|
||||||
// [SECTION: EVENTS]
|
// [SECTION: EVENTS]
|
||||||
@@ -141,10 +154,10 @@
|
|||||||
if (checked) {
|
if (checked) {
|
||||||
if (!newSelected.includes(id)) newSelected.push(id);
|
if (!newSelected.includes(id)) newSelected.push(id);
|
||||||
} else {
|
} else {
|
||||||
newSelected = newSelected.filter(sid => sid !== id);
|
newSelected = newSelected.filter((sid) => sid !== id);
|
||||||
}
|
}
|
||||||
selectedIds = newSelected;
|
selectedIds = newSelected;
|
||||||
dispatch('selectionChanged', newSelected);
|
dispatch("selectionChanged", newSelected);
|
||||||
}
|
}
|
||||||
// [/DEF:handleSelectionChange:Function]
|
// [/DEF:handleSelectionChange:Function]
|
||||||
|
|
||||||
@@ -155,16 +168,16 @@
|
|||||||
function handleSelectAll(checked: boolean) {
|
function handleSelectAll(checked: boolean) {
|
||||||
let newSelected = [...selectedIds];
|
let newSelected = [...selectedIds];
|
||||||
if (checked) {
|
if (checked) {
|
||||||
paginatedDashboards.forEach(d => {
|
paginatedDashboards.forEach((d) => {
|
||||||
if (!newSelected.includes(d.id)) newSelected.push(d.id);
|
if (!newSelected.includes(d.id)) newSelected.push(d.id);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
paginatedDashboards.forEach(d => {
|
paginatedDashboards.forEach((d) => {
|
||||||
newSelected = newSelected.filter(sid => sid !== d.id);
|
newSelected = newSelected.filter((sid) => sid !== d.id);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
selectedIds = newSelected;
|
selectedIds = newSelected;
|
||||||
dispatch('selectionChanged', newSelected);
|
dispatch("selectionChanged", newSelected);
|
||||||
}
|
}
|
||||||
// [/DEF:handleSelectAll:Function]
|
// [/DEF:handleSelectAll:Function]
|
||||||
|
|
||||||
@@ -189,17 +202,13 @@
|
|||||||
showGitManager = true;
|
showGitManager = true;
|
||||||
}
|
}
|
||||||
// [/DEF:openGit:Function]
|
// [/DEF:openGit:Function]
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- [SECTION: TEMPLATE] -->
|
<!-- [SECTION: TEMPLATE] -->
|
||||||
<div class="dashboard-grid">
|
<div class="dashboard-grid">
|
||||||
<!-- Filter Input -->
|
<!-- Filter Input -->
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<Input
|
<Input bind:value={filterText} placeholder={$t.dashboard.search} />
|
||||||
bind:value={filterText}
|
|
||||||
placeholder={$t.dashboard.search}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Grid/Table -->
|
<!-- Grid/Table -->
|
||||||
@@ -212,21 +221,52 @@
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={allSelected}
|
checked={allSelected}
|
||||||
indeterminate={someSelected && !allSelected}
|
indeterminate={someSelected && !allSelected}
|
||||||
on:change={(e) => handleSelectAll((e.target as HTMLInputElement).checked)}
|
on:change={(e) =>
|
||||||
|
handleSelectAll((e.target as HTMLInputElement).checked)}
|
||||||
class="h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
|
class="h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
|
||||||
/>
|
/>
|
||||||
</th>
|
</th>
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:text-gray-700 transition-colors" on:click={() => handleSort('title')}>
|
<th
|
||||||
{$t.dashboard.title} {sortColumn === 'title' ? (sortDirection === 'asc' ? '↑' : '↓') : ''}
|
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:text-gray-700 transition-colors"
|
||||||
|
on:click={() => handleSort("title")}
|
||||||
|
>
|
||||||
|
{$t.dashboard.title}
|
||||||
|
{sortColumn === "title"
|
||||||
|
? sortDirection === "asc"
|
||||||
|
? "↑"
|
||||||
|
: "↓"
|
||||||
|
: ""}
|
||||||
</th>
|
</th>
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:text-gray-700 transition-colors" on:click={() => handleSort('last_modified')}>
|
<th
|
||||||
{$t.dashboard.last_modified} {sortColumn === 'last_modified' ? (sortDirection === 'asc' ? '↑' : '↓') : ''}
|
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:text-gray-700 transition-colors"
|
||||||
|
on:click={() => handleSort("last_modified")}
|
||||||
|
>
|
||||||
|
{$t.dashboard.last_modified}
|
||||||
|
{sortColumn === "last_modified"
|
||||||
|
? sortDirection === "asc"
|
||||||
|
? "↑"
|
||||||
|
: "↓"
|
||||||
|
: ""}
|
||||||
</th>
|
</th>
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:text-gray-700 transition-colors" on:click={() => handleSort('status')}>
|
<th
|
||||||
{$t.dashboard.status} {sortColumn === 'status' ? (sortDirection === 'asc' ? '↑' : '↓') : ''}
|
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:text-gray-700 transition-colors"
|
||||||
|
on:click={() => handleSort("status")}
|
||||||
|
>
|
||||||
|
{$t.dashboard.status}
|
||||||
|
{sortColumn === "status"
|
||||||
|
? sortDirection === "asc"
|
||||||
|
? "↑"
|
||||||
|
: "↓"
|
||||||
|
: ""}
|
||||||
</th>
|
</th>
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{$t.dashboard.validation}</th>
|
<th
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{$t.dashboard.git}</th>
|
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||||
|
>{$t.dashboard.validation}</th
|
||||||
|
>
|
||||||
|
<th
|
||||||
|
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||||
|
>{$t.dashboard.git}</th
|
||||||
|
>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="bg-white divide-y divide-gray-200">
|
<tbody class="bg-white divide-y divide-gray-200">
|
||||||
@@ -236,14 +276,28 @@
|
|||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={selectedIds.includes(dashboard.id)}
|
checked={selectedIds.includes(dashboard.id)}
|
||||||
on:change={(e) => handleSelectionChange(dashboard.id, (e.target as HTMLInputElement).checked)}
|
on:change={(e) =>
|
||||||
|
handleSelectionChange(
|
||||||
|
dashboard.id,
|
||||||
|
(e.target as HTMLInputElement).checked,
|
||||||
|
)}
|
||||||
class="h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
|
class="h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{dashboard.title}</td>
|
<td
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{new Date(dashboard.last_modified).toLocaleDateString()}</td>
|
class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900"
|
||||||
|
>{dashboard.title}</td
|
||||||
|
>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500"
|
||||||
|
>{new Date(dashboard.last_modified).toLocaleDateString()}</td
|
||||||
|
>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||||
<span class="px-2 py-1 text-xs font-medium rounded-full {dashboard.status === 'published' ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'}">
|
<span
|
||||||
|
class="px-2 py-1 text-xs font-medium rounded-full {dashboard.status ===
|
||||||
|
'published'
|
||||||
|
? 'bg-green-100 text-green-800'
|
||||||
|
: 'bg-gray-100 text-gray-800'}"
|
||||||
|
>
|
||||||
{dashboard.status}
|
{dashboard.status}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
@@ -255,7 +309,7 @@
|
|||||||
disabled={validatingIds.has(dashboard.id)}
|
disabled={validatingIds.has(dashboard.id)}
|
||||||
class="text-purple-600 hover:text-purple-900"
|
class="text-purple-600 hover:text-purple-900"
|
||||||
>
|
>
|
||||||
{validatingIds.has(dashboard.id) ? 'Validating...' : 'Validate'}
|
{validatingIds.has(dashboard.id) ? "Validating..." : "Validate"}
|
||||||
</Button>
|
</Button>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||||
@@ -278,9 +332,15 @@
|
|||||||
<div class="flex items-center justify-between mt-6">
|
<div class="flex items-center justify-between mt-6">
|
||||||
<div class="text-sm text-gray-500">
|
<div class="text-sm text-gray-500">
|
||||||
{($t.dashboard?.showing || "")
|
{($t.dashboard?.showing || "")
|
||||||
.replace('{start}', (currentPage * pageSize + 1).toString())
|
.replace("{start}", (currentPage * pageSize + 1).toString())
|
||||||
.replace('{end}', Math.min((currentPage + 1) * pageSize, sortedDashboards.length).toString())
|
.replace(
|
||||||
.replace('{total}', sortedDashboards.length.toString())}
|
"{end}",
|
||||||
|
Math.min(
|
||||||
|
(currentPage + 1) * pageSize,
|
||||||
|
sortedDashboards.length,
|
||||||
|
).toString(),
|
||||||
|
)
|
||||||
|
.replace("{total}", sortedDashboards.length.toString())}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<Button
|
<Button
|
||||||
@@ -313,8 +373,4 @@
|
|||||||
|
|
||||||
<!-- [/SECTION] -->
|
<!-- [/SECTION] -->
|
||||||
|
|
||||||
<style>
|
|
||||||
/* Component styles */
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<!-- [/DEF:DashboardGrid:Component] -->
|
<!-- [/DEF:DashboardGrid:Component] -->
|
||||||
@@ -15,7 +15,10 @@
|
|||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
// [/SECTION]
|
// [/SECTION]
|
||||||
|
|
||||||
export let schema;
|
let {
|
||||||
|
schema,
|
||||||
|
} = $props();
|
||||||
|
|
||||||
let formData = {};
|
let formData = {};
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|||||||
@@ -14,9 +14,12 @@
|
|||||||
// [/SECTION]
|
// [/SECTION]
|
||||||
|
|
||||||
// [SECTION: PROPS]
|
// [SECTION: PROPS]
|
||||||
export let label: string = "Select Environment";
|
let {
|
||||||
export let selectedId: string = "";
|
label = "",
|
||||||
export let environments: Array<{id: string, name: string, url: string}> = [];
|
selectedId = "",
|
||||||
|
environments = [],
|
||||||
|
} = $props();
|
||||||
|
|
||||||
// [/SECTION]
|
// [/SECTION]
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
@@ -53,8 +56,5 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- [/SECTION] -->
|
<!-- [/SECTION] -->
|
||||||
|
|
||||||
<style>
|
|
||||||
/* Component specific styles */
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<!-- [/DEF:EnvSelector:Component] -->
|
<!-- [/DEF:EnvSelector:Component] -->
|
||||||
@@ -14,10 +14,13 @@
|
|||||||
// [/SECTION]
|
// [/SECTION]
|
||||||
|
|
||||||
// [SECTION: PROPS]
|
// [SECTION: PROPS]
|
||||||
export let sourceDatabases: Array<{uuid: string, database_name: string, engine?: string}> = [];
|
let {
|
||||||
export let targetDatabases: Array<{uuid: string, database_name: string}> = [];
|
sourceDatabases = [],
|
||||||
export let mappings: Array<{source_db_uuid: string, target_db_uuid: string}> = [];
|
targetDatabases = [],
|
||||||
export let suggestions: Array<{source_db_uuid: string, target_db_uuid: string, confidence: number}> = [];
|
mappings = [],
|
||||||
|
suggestions = [],
|
||||||
|
} = $props();
|
||||||
|
|
||||||
// [/SECTION]
|
// [/SECTION]
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
@@ -100,8 +103,5 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- [/SECTION] -->
|
<!-- [/SECTION] -->
|
||||||
|
|
||||||
<style>
|
|
||||||
/* Component specific styles */
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<!-- [/DEF:MappingTable:Component] -->
|
<!-- [/DEF:MappingTable:Component] -->
|
||||||
|
|||||||
@@ -14,10 +14,13 @@
|
|||||||
// [/SECTION]
|
// [/SECTION]
|
||||||
|
|
||||||
// [SECTION: PROPS]
|
// [SECTION: PROPS]
|
||||||
export let show: boolean = false;
|
let {
|
||||||
export let sourceDbName: string = "";
|
show = false,
|
||||||
export let sourceDbUuid: string = "";
|
sourceDbName = "",
|
||||||
export let targetDatabases: Array<{uuid: string, database_name: string}> = [];
|
sourceDbUuid = "",
|
||||||
|
targetDatabases = [],
|
||||||
|
} = $props();
|
||||||
|
|
||||||
// [/SECTION]
|
// [/SECTION]
|
||||||
|
|
||||||
let selectedTargetUuid = "";
|
let selectedTargetUuid = "";
|
||||||
@@ -111,8 +114,5 @@
|
|||||||
{/if}
|
{/if}
|
||||||
<!-- [/SECTION] -->
|
<!-- [/SECTION] -->
|
||||||
|
|
||||||
<style>
|
|
||||||
/* Modal specific styles */
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<!-- [/DEF:MissingMappingModal:Component] -->
|
<!-- [/DEF:MissingMappingModal:Component] -->
|
||||||
|
|||||||
@@ -7,11 +7,9 @@
|
|||||||
@RELATION: EMITS -> resume, cancel
|
@RELATION: EMITS -> resume, cancel
|
||||||
-->
|
-->
|
||||||
<script>
|
<script>
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from "svelte";
|
||||||
|
|
||||||
export let show = false;
|
let { show = false, databases = [], errorMessage = "" } = $props();
|
||||||
export let databases = []; // List of database names requiring passwords
|
|
||||||
export let errorMessage = "";
|
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
@@ -26,14 +24,14 @@
|
|||||||
if (submitting) return;
|
if (submitting) return;
|
||||||
|
|
||||||
// Validate all passwords entered
|
// Validate all passwords entered
|
||||||
const missing = databases.filter(db => !passwords[db]);
|
const missing = databases.filter((db) => !passwords[db]);
|
||||||
if (missing.length > 0) {
|
if (missing.length > 0) {
|
||||||
alert(`Please enter passwords for: ${missing.join(', ')}`);
|
alert(`Please enter passwords for: ${missing.join(", ")}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
submitting = true;
|
submitting = true;
|
||||||
dispatch('resume', { passwords });
|
dispatch("resume", { passwords });
|
||||||
// Reset submitting state is handled by parent or on close
|
// Reset submitting state is handled by parent or on close
|
||||||
}
|
}
|
||||||
// [/DEF:handleSubmit:Function]
|
// [/DEF:handleSubmit:Function]
|
||||||
@@ -43,54 +41,100 @@
|
|||||||
// @PRE: Modal is open.
|
// @PRE: Modal is open.
|
||||||
// @POST: 'cancel' event is dispatched and show is set to false.
|
// @POST: 'cancel' event is dispatched and show is set to false.
|
||||||
function handleCancel() {
|
function handleCancel() {
|
||||||
dispatch('cancel');
|
dispatch("cancel");
|
||||||
show = false;
|
show = false;
|
||||||
}
|
}
|
||||||
// [/DEF:handleCancel:Function]
|
// [/DEF:handleCancel:Function]
|
||||||
|
|
||||||
// Reset passwords when modal opens/closes
|
// Reset passwords when modal opens/closes
|
||||||
$: if (!show) {
|
$effect(() => {
|
||||||
|
if (!show) {
|
||||||
passwords = {};
|
passwords = {};
|
||||||
submitting = false;
|
submitting = false;
|
||||||
}
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if show}
|
{#if show}
|
||||||
<div class="fixed inset-0 z-50 overflow-y-auto" aria-labelledby="modal-title" role="dialog" aria-modal="true">
|
<div
|
||||||
<div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
class="fixed inset-0 z-50 overflow-y-auto"
|
||||||
|
aria-labelledby="modal-title"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0"
|
||||||
|
>
|
||||||
<!-- Background overlay -->
|
<!-- Background overlay -->
|
||||||
<div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" aria-hidden="true" on:click={handleCancel}></div>
|
<div
|
||||||
|
class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"
|
||||||
|
aria-hidden="true"
|
||||||
|
on:click={handleCancel}
|
||||||
|
></div>
|
||||||
|
|
||||||
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">​</span>
|
<span
|
||||||
|
class="hidden sm:inline-block sm:align-middle sm:h-screen"
|
||||||
|
aria-hidden="true">​</span
|
||||||
|
>
|
||||||
|
|
||||||
<div class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
|
<div
|
||||||
|
class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full"
|
||||||
|
>
|
||||||
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||||
<div class="sm:flex sm:items-start">
|
<div class="sm:flex sm:items-start">
|
||||||
<div class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
|
<div
|
||||||
|
class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10"
|
||||||
|
>
|
||||||
<!-- Heroicon name: outline/lock-closed -->
|
<!-- Heroicon name: outline/lock-closed -->
|
||||||
<svg class="h-6 w-6 text-red-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
<svg
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
class="h-6 w-6 text-red-600"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left w-full">
|
<div
|
||||||
<h3 class="text-lg leading-6 font-medium text-gray-900" id="modal-title">
|
class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left w-full"
|
||||||
|
>
|
||||||
|
<h3
|
||||||
|
class="text-lg leading-6 font-medium text-gray-900"
|
||||||
|
id="modal-title"
|
||||||
|
>
|
||||||
Database Password Required
|
Database Password Required
|
||||||
</h3>
|
</h3>
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
<p class="text-sm text-gray-500 mb-4">
|
<p class="text-sm text-gray-500 mb-4">
|
||||||
The migration process requires passwords for the following databases to proceed.
|
The migration process requires passwords for
|
||||||
|
the following databases to proceed.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{#if errorMessage}
|
{#if errorMessage}
|
||||||
<div class="mb-4 p-2 bg-red-50 text-red-700 text-xs rounded border border-red-200">
|
<div
|
||||||
|
class="mb-4 p-2 bg-red-50 text-red-700 text-xs rounded border border-red-200"
|
||||||
|
>
|
||||||
Error: {errorMessage}
|
Error: {errorMessage}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<form on:submit|preventDefault={handleSubmit} class="space-y-4">
|
<form
|
||||||
|
on:submit|preventDefault={handleSubmit}
|
||||||
|
class="space-y-4"
|
||||||
|
>
|
||||||
{#each databases as dbName}
|
{#each databases as dbName}
|
||||||
<div>
|
<div>
|
||||||
<label for="password-{dbName}" class="block text-sm font-medium text-gray-700">
|
<label
|
||||||
|
for="password-{dbName}"
|
||||||
|
class="block text-sm font-medium text-gray-700"
|
||||||
|
>
|
||||||
Password for {dbName}
|
Password for {dbName}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@@ -108,14 +152,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
|
<div
|
||||||
|
class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse"
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-indigo-600 text-base font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:ml-3 sm:w-auto sm:text-sm disabled:opacity-50"
|
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-indigo-600 text-base font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:ml-3 sm:w-auto sm:text-sm disabled:opacity-50"
|
||||||
on:click={handleSubmit}
|
on:click={handleSubmit}
|
||||||
disabled={submitting}
|
disabled={submitting}
|
||||||
>
|
>
|
||||||
{submitting ? 'Resuming...' : 'Resume Migration'}
|
{submitting ? "Resuming..." : "Resume Migration"}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -11,8 +11,11 @@
|
|||||||
import { formatDistanceToNow } from 'date-fns';
|
import { formatDistanceToNow } from 'date-fns';
|
||||||
import { t } from '../lib/i18n';
|
import { t } from '../lib/i18n';
|
||||||
|
|
||||||
export let tasks: Array<any> = [];
|
let {
|
||||||
export let loading: boolean = false;
|
tasks = [],
|
||||||
|
loading = false,
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
|||||||
@@ -23,25 +23,27 @@
|
|||||||
import { t } from "../lib/i18n";
|
import { t } from "../lib/i18n";
|
||||||
import TaskLogPanel from "./tasks/TaskLogPanel.svelte";
|
import TaskLogPanel from "./tasks/TaskLogPanel.svelte";
|
||||||
|
|
||||||
export let show = false;
|
let {
|
||||||
export let inline = false;
|
show = $bindable(false),
|
||||||
export let taskId = null;
|
inline = false,
|
||||||
export let taskStatus = null;
|
taskId = null,
|
||||||
export let realTimeLogs = [];
|
taskStatus = null,
|
||||||
|
realTimeLogs = [],
|
||||||
|
} = $props();
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
let logs = [];
|
let logs = $state([]);
|
||||||
let loading = false;
|
let loading = $state(false);
|
||||||
let error = "";
|
let error = $state("");
|
||||||
let interval;
|
let interval;
|
||||||
let autoScroll = true;
|
let autoScroll = $state(true);
|
||||||
|
|
||||||
$: shouldShow = inline || show;
|
let shouldShow = $derived(inline || show);
|
||||||
|
|
||||||
// [DEF:handleRealTimeLogs:Action]
|
// [DEF:handleRealTimeLogs:Action]
|
||||||
/** @PURPOSE Append real-time logs as they arrive from WebSocket, preventing duplicates */
|
$effect(() => {
|
||||||
$: if (realTimeLogs && realTimeLogs.length > 0) {
|
if (realTimeLogs && realTimeLogs.length > 0) {
|
||||||
const lastLog = realTimeLogs[realTimeLogs.length - 1];
|
const lastLog = realTimeLogs[realTimeLogs.length - 1];
|
||||||
const exists = logs.some(
|
const exists = logs.some(
|
||||||
(l) =>
|
(l) =>
|
||||||
@@ -50,33 +52,18 @@
|
|||||||
);
|
);
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
logs = [...logs, lastLog];
|
logs = [...logs, lastLog];
|
||||||
console.log(
|
|
||||||
`[TaskLogViewer][Action] Appended real-time log, total=${logs.length}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
});
|
||||||
// [/DEF:handleRealTimeLogs:Action]
|
// [/DEF:handleRealTimeLogs:Action]
|
||||||
|
|
||||||
// [DEF:fetchLogs:Function]
|
// [DEF:fetchLogs:Function]
|
||||||
/**
|
|
||||||
* @PURPOSE Fetches logs for the current task from API (polling fallback).
|
|
||||||
* @PRE taskId must be set.
|
|
||||||
* @POST logs array is updated with data from taskService.
|
|
||||||
* @SIDE_EFFECT Updates logs, loading, and error state.
|
|
||||||
*/
|
|
||||||
async function fetchLogs() {
|
async function fetchLogs() {
|
||||||
if (!taskId) return;
|
if (!taskId) return;
|
||||||
console.log(`[TaskLogViewer][Action] Fetching logs for task=${taskId}`);
|
|
||||||
try {
|
try {
|
||||||
logs = await getTaskLogs(taskId);
|
logs = await getTaskLogs(taskId);
|
||||||
console.log(
|
|
||||||
`[TaskLogViewer][Coherence:OK] Logs fetched count=${logs.length}`,
|
|
||||||
);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = e.message;
|
error = e.message;
|
||||||
console.error(
|
|
||||||
`[TaskLogViewer][Coherence:Failed] Error: ${e.message}`,
|
|
||||||
);
|
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
@@ -85,18 +72,14 @@
|
|||||||
|
|
||||||
function handleFilterChange(event) {
|
function handleFilterChange(event) {
|
||||||
const { source, level } = event.detail;
|
const { source, level } = event.detail;
|
||||||
console.log(
|
|
||||||
`[TaskLogViewer][Action] Filter changed: source=${source}, level=${level}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleRefresh() {
|
function handleRefresh() {
|
||||||
console.log(`[TaskLogViewer][Action] Manual refresh`);
|
|
||||||
fetchLogs();
|
fetchLogs();
|
||||||
}
|
}
|
||||||
|
|
||||||
// React to changes in show/taskId/taskStatus
|
$effect(() => {
|
||||||
$: if (shouldShow && taskId) {
|
if (shouldShow && taskId) {
|
||||||
if (interval) clearInterval(interval);
|
if (interval) clearInterval(interval);
|
||||||
logs = [];
|
logs = [];
|
||||||
loading = true;
|
loading = true;
|
||||||
@@ -113,6 +96,7 @@
|
|||||||
} else {
|
} else {
|
||||||
if (interval) clearInterval(interval);
|
if (interval) clearInterval(interval);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
if (interval) clearInterval(interval);
|
if (interval) clearInterval(interval);
|
||||||
@@ -121,18 +105,25 @@
|
|||||||
|
|
||||||
{#if shouldShow}
|
{#if shouldShow}
|
||||||
{#if inline}
|
{#if inline}
|
||||||
<div class="log-viewer-inline">
|
<div class="flex flex-col h-full w-full">
|
||||||
{#if loading && logs.length === 0}
|
{#if loading && logs.length === 0}
|
||||||
<div class="loading-state">
|
<div
|
||||||
<div class="loading-spinner"></div>
|
class="flex items-center justify-center gap-3 h-full text-terminal-text-subtle text-sm"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="w-5 h-5 border-2 border-terminal-border border-t-primary rounded-full animate-spin"
|
||||||
|
></div>
|
||||||
<span>{$t.tasks?.loading || "Loading logs..."}</span>
|
<span>{$t.tasks?.loading || "Loading logs..."}</span>
|
||||||
</div>
|
</div>
|
||||||
{:else if error}
|
{:else if error}
|
||||||
<div class="error-state">
|
<div
|
||||||
<span class="error-icon">⚠</span>
|
class="flex items-center justify-center gap-2 h-full text-log-error text-sm"
|
||||||
|
>
|
||||||
|
<span class="text-xl">⚠</span>
|
||||||
<span>{error}</span>
|
<span>{error}</span>
|
||||||
<button class="retry-btn" on:click={handleRefresh}
|
<button
|
||||||
>Retry</button
|
class="bg-terminal-surface text-terminal-text-subtle border border-terminal-border rounded-md px-3 py-1 text-xs cursor-pointer transition-all hover:bg-terminal-border hover:text-terminal-text-bright"
|
||||||
|
on:click={handleRefresh}>Retry</button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
@@ -156,7 +147,7 @@
|
|||||||
class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0"
|
class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"
|
class="fixed inset-0 bg-gray-500/75 transition-opacity"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
show = false;
|
show = false;
|
||||||
@@ -210,67 +201,3 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- [/DEF:TaskLogViewer:Component] -->
|
<!-- [/DEF:TaskLogViewer:Component] -->
|
||||||
|
|
||||||
<style>
|
|
||||||
.log-viewer-inline {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-state {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 0.75rem;
|
|
||||||
height: 100%;
|
|
||||||
color: #64748b;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-spinner {
|
|
||||||
width: 1.25rem;
|
|
||||||
height: 1.25rem;
|
|
||||||
border: 2px solid #334155;
|
|
||||||
border-top-color: #3b82f6;
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: spin 0.8s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-state {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
height: 100%;
|
|
||||||
color: #f87171;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-icon {
|
|
||||||
font-size: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.retry-btn {
|
|
||||||
background-color: #1e293b;
|
|
||||||
color: #94a3b8;
|
|
||||||
border: 1px solid #334155;
|
|
||||||
border-radius: 0.375rem;
|
|
||||||
padding: 0.25rem 0.75rem;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.15s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.retry-btn:hover {
|
|
||||||
background-color: #334155;
|
|
||||||
color: #e2e8f0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -19,7 +19,10 @@
|
|||||||
* @type {Backup[]}
|
* @type {Backup[]}
|
||||||
* @description Array of backup objects to display.
|
* @description Array of backup objects to display.
|
||||||
*/
|
*/
|
||||||
export let backups: Backup[] = [];
|
let {
|
||||||
|
backups = [],
|
||||||
|
} = $props();
|
||||||
|
|
||||||
// [/SECTION]
|
// [/SECTION]
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
@@ -78,7 +81,5 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- [/SECTION] -->
|
<!-- [/SECTION] -->
|
||||||
|
|
||||||
<style>
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<!-- [/DEF:BackupList:Component] -->
|
<!-- [/DEF:BackupList:Component] -->
|
||||||
@@ -19,8 +19,11 @@
|
|||||||
// [/SECTION]
|
// [/SECTION]
|
||||||
|
|
||||||
// [SECTION: PROPS]
|
// [SECTION: PROPS]
|
||||||
export let dashboardId;
|
let {
|
||||||
export let currentBranch = 'main';
|
dashboardId,
|
||||||
|
currentBranch = 'main',
|
||||||
|
} = $props();
|
||||||
|
|
||||||
// [/SECTION]
|
// [/SECTION]
|
||||||
|
|
||||||
// [SECTION: STATE]
|
// [SECTION: STATE]
|
||||||
|
|||||||
@@ -16,7 +16,10 @@
|
|||||||
// [/SECTION]
|
// [/SECTION]
|
||||||
|
|
||||||
// [SECTION: PROPS]
|
// [SECTION: PROPS]
|
||||||
export let dashboardId;
|
let {
|
||||||
|
dashboardId,
|
||||||
|
} = $props();
|
||||||
|
|
||||||
// [/SECTION]
|
// [/SECTION]
|
||||||
|
|
||||||
// [SECTION: STATE]
|
// [SECTION: STATE]
|
||||||
|
|||||||
@@ -12,22 +12,22 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
// [SECTION: IMPORTS]
|
// [SECTION: IMPORTS]
|
||||||
import { createEventDispatcher, onMount } from 'svelte';
|
import { createEventDispatcher, onMount } from "svelte";
|
||||||
import { gitService } from '../../services/gitService';
|
import { gitService } from "../../services/gitService";
|
||||||
import { addToast as toast } from '../../lib/toasts.js';
|
import { addToast as toast } from "../../lib/toasts.js";
|
||||||
import { api } from '../../lib/api';
|
import { api } from "../../lib/api";
|
||||||
// [/SECTION]
|
// [/SECTION]
|
||||||
|
|
||||||
// [SECTION: PROPS]
|
// [SECTION: PROPS]
|
||||||
export let dashboardId;
|
let { dashboardId, show = false } = $props();
|
||||||
export let show = false;
|
|
||||||
// [/SECTION]
|
// [/SECTION]
|
||||||
|
|
||||||
// [SECTION: STATE]
|
// [SECTION: STATE]
|
||||||
let message = '';
|
let message = "";
|
||||||
let committing = false;
|
let committing = false;
|
||||||
let status = null;
|
let status = null;
|
||||||
let diff = '';
|
let diff = "";
|
||||||
let loading = false;
|
let loading = false;
|
||||||
let generatingMessage = false;
|
let generatingMessage = false;
|
||||||
// [/SECTION]
|
// [/SECTION]
|
||||||
@@ -41,14 +41,18 @@
|
|||||||
async function handleGenerateMessage() {
|
async function handleGenerateMessage() {
|
||||||
generatingMessage = true;
|
generatingMessage = true;
|
||||||
try {
|
try {
|
||||||
console.log(`[CommitModal][Action] Generating commit message for dashboard ${dashboardId}`);
|
console.log(
|
||||||
|
`[CommitModal][Action] Generating commit message for dashboard ${dashboardId}`,
|
||||||
|
);
|
||||||
// postApi returns the JSON data directly or throws an error
|
// postApi returns the JSON data directly or throws an error
|
||||||
const data = await api.postApi(`/git/repositories/${dashboardId}/generate-message`);
|
const data = await api.postApi(
|
||||||
|
`/git/repositories/${dashboardId}/generate-message`,
|
||||||
|
);
|
||||||
message = data.message;
|
message = data.message;
|
||||||
toast('Commit message generated', 'success');
|
toast("Commit message generated", "success");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`[CommitModal][Coherence:Failed] ${e.message}`);
|
console.error(`[CommitModal][Coherence:Failed] ${e.message}`);
|
||||||
toast(e.message || 'Failed to generate message', 'error');
|
toast(e.message || "Failed to generate message", "error");
|
||||||
} finally {
|
} finally {
|
||||||
generatingMessage = false;
|
generatingMessage = false;
|
||||||
}
|
}
|
||||||
@@ -64,20 +68,32 @@
|
|||||||
if (!dashboardId || !show) return;
|
if (!dashboardId || !show) return;
|
||||||
loading = true;
|
loading = true;
|
||||||
try {
|
try {
|
||||||
console.log(`[CommitModal][Action] Loading status and diff for ${dashboardId}`);
|
console.log(
|
||||||
|
`[CommitModal][Action] Loading status and diff for ${dashboardId}`,
|
||||||
|
);
|
||||||
status = await gitService.getStatus(dashboardId);
|
status = await gitService.getStatus(dashboardId);
|
||||||
// Fetch both unstaged and staged diffs to show complete picture
|
// Fetch both unstaged and staged diffs to show complete picture
|
||||||
const unstagedDiff = await gitService.getDiff(dashboardId, null, false);
|
const unstagedDiff = await gitService.getDiff(
|
||||||
const stagedDiff = await gitService.getDiff(dashboardId, null, true);
|
dashboardId,
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
const stagedDiff = await gitService.getDiff(
|
||||||
|
dashboardId,
|
||||||
|
null,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
diff = "";
|
diff = "";
|
||||||
if (stagedDiff) diff += "--- STAGED CHANGES ---\n" + stagedDiff + "\n\n";
|
if (stagedDiff)
|
||||||
if (unstagedDiff) diff += "--- UNSTAGED CHANGES ---\n" + unstagedDiff;
|
diff += "--- STAGED CHANGES ---\n" + stagedDiff + "\n\n";
|
||||||
|
if (unstagedDiff)
|
||||||
|
diff += "--- UNSTAGED CHANGES ---\n" + unstagedDiff;
|
||||||
|
|
||||||
if (!diff) diff = "";
|
if (!diff) diff = "";
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`[CommitModal][Coherence:Failed] ${e.message}`);
|
console.error(`[CommitModal][Coherence:Failed] ${e.message}`);
|
||||||
toast('Failed to load changes', 'error');
|
toast("Failed to load changes", "error");
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
@@ -92,31 +108,39 @@
|
|||||||
*/
|
*/
|
||||||
async function handleCommit() {
|
async function handleCommit() {
|
||||||
if (!message) return;
|
if (!message) return;
|
||||||
console.log(`[CommitModal][Action] Committing changes for dashboard ${dashboardId}`);
|
console.log(
|
||||||
|
`[CommitModal][Action] Committing changes for dashboard ${dashboardId}`,
|
||||||
|
);
|
||||||
committing = true;
|
committing = true;
|
||||||
try {
|
try {
|
||||||
await gitService.commit(dashboardId, message, []);
|
await gitService.commit(dashboardId, message, []);
|
||||||
toast('Changes committed successfully', 'success');
|
toast("Changes committed successfully", "success");
|
||||||
dispatch('commit');
|
dispatch("commit");
|
||||||
show = false;
|
show = false;
|
||||||
message = '';
|
message = "";
|
||||||
console.log(`[CommitModal][Coherence:OK] Committed`);
|
console.log(`[CommitModal][Coherence:OK] Committed`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`[CommitModal][Coherence:Failed] ${e.message}`);
|
console.error(`[CommitModal][Coherence:Failed] ${e.message}`);
|
||||||
toast(e.message, 'error');
|
toast(e.message, "error");
|
||||||
} finally {
|
} finally {
|
||||||
committing = false;
|
committing = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// [/DEF:handleCommit:Function]
|
// [/DEF:handleCommit:Function]
|
||||||
|
|
||||||
$: if (show) loadStatus();
|
$effect(() => {
|
||||||
|
if (show) loadStatus();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- [SECTION: TEMPLATE] -->
|
<!-- [SECTION: TEMPLATE] -->
|
||||||
{#if show}
|
{#if show}
|
||||||
<div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
<div
|
||||||
<div class="bg-white p-6 rounded-lg shadow-xl w-full max-w-4xl max-h-[90vh] flex flex-col">
|
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="bg-white p-6 rounded-lg shadow-xl w-full max-w-4xl max-h-[90vh] flex flex-col"
|
||||||
|
>
|
||||||
<h2 class="text-xl font-bold mb-4">Commit Changes</h2>
|
<h2 class="text-xl font-bold mb-4">Commit Changes</h2>
|
||||||
|
|
||||||
<div class="flex flex-col md:flex-row gap-4 flex-1 overflow-hidden">
|
<div class="flex flex-col md:flex-row gap-4 flex-1 overflow-hidden">
|
||||||
@@ -124,7 +148,10 @@
|
|||||||
<div class="w-full md:w-1/3 flex flex-col">
|
<div class="w-full md:w-1/3 flex flex-col">
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<div class="flex justify-between items-center mb-1">
|
<div class="flex justify-between items-center mb-1">
|
||||||
<label class="block text-sm font-medium text-gray-700">Commit Message</label>
|
<label
|
||||||
|
class="block text-sm font-medium text-gray-700"
|
||||||
|
>Commit Message</label
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
on:click={handleGenerateMessage}
|
on:click={handleGenerateMessage}
|
||||||
disabled={generatingMessage || loading}
|
disabled={generatingMessage || loading}
|
||||||
@@ -146,21 +173,37 @@
|
|||||||
|
|
||||||
{#if status}
|
{#if status}
|
||||||
<div class="flex-1 overflow-y-auto">
|
<div class="flex-1 overflow-y-auto">
|
||||||
<h3 class="text-sm font-bold text-gray-500 uppercase mb-2">Changed Files</h3>
|
<h3
|
||||||
|
class="text-sm font-bold text-gray-500 uppercase mb-2"
|
||||||
|
>
|
||||||
|
Changed Files
|
||||||
|
</h3>
|
||||||
<ul class="text-xs space-y-1">
|
<ul class="text-xs space-y-1">
|
||||||
{#each status.staged_files as file}
|
{#each status.staged_files as file}
|
||||||
<li class="text-green-600 flex items-center font-semibold" title="Staged">
|
<li
|
||||||
<span class="mr-2">S</span> {file}
|
class="text-green-600 flex items-center font-semibold"
|
||||||
|
title="Staged"
|
||||||
|
>
|
||||||
|
<span class="mr-2">S</span>
|
||||||
|
{file}
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
{#each status.modified_files as file}
|
{#each status.modified_files as file}
|
||||||
<li class="text-yellow-600 flex items-center" title="Modified (Unstaged)">
|
<li
|
||||||
<span class="mr-2">M</span> {file}
|
class="text-yellow-600 flex items-center"
|
||||||
|
title="Modified (Unstaged)"
|
||||||
|
>
|
||||||
|
<span class="mr-2">M</span>
|
||||||
|
{file}
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
{#each status.untracked_files as file}
|
{#each status.untracked_files as file}
|
||||||
<li class="text-blue-600 flex items-center" title="Untracked">
|
<li
|
||||||
<span class="mr-2">?</span> {file}
|
class="text-blue-600 flex items-center"
|
||||||
|
title="Untracked"
|
||||||
|
>
|
||||||
|
<span class="mr-2">?</span>
|
||||||
|
{file}
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
@@ -169,15 +212,30 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Right: Diff Viewer -->
|
<!-- Right: Diff Viewer -->
|
||||||
<div class="w-full md:w-2/3 flex flex-col overflow-hidden border rounded bg-gray-50">
|
<div
|
||||||
<div class="bg-gray-200 px-3 py-1 text-xs font-bold text-gray-600 border-b">Changes Preview</div>
|
class="w-full md:w-2/3 flex flex-col overflow-hidden border rounded bg-gray-50"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="bg-gray-200 px-3 py-1 text-xs font-bold text-gray-600 border-b"
|
||||||
|
>
|
||||||
|
Changes Preview
|
||||||
|
</div>
|
||||||
<div class="flex-1 overflow-auto p-2">
|
<div class="flex-1 overflow-auto p-2">
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<div class="flex items-center justify-center h-full text-gray-500">Loading diff...</div>
|
<div
|
||||||
|
class="flex items-center justify-center h-full text-gray-500"
|
||||||
|
>
|
||||||
|
Loading diff...
|
||||||
|
</div>
|
||||||
{:else if diff}
|
{:else if diff}
|
||||||
<pre class="text-xs font-mono whitespace-pre-wrap">{diff}</pre>
|
<pre
|
||||||
|
class="text-xs font-mono whitespace-pre-wrap">{diff}</pre>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="flex items-center justify-center h-full text-gray-500 italic">No changes detected</div>
|
<div
|
||||||
|
class="flex items-center justify-center h-full text-gray-500 italic"
|
||||||
|
>
|
||||||
|
No changes detected
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -185,17 +243,21 @@
|
|||||||
|
|
||||||
<div class="flex justify-end space-x-3 mt-6 pt-4 border-t">
|
<div class="flex justify-end space-x-3 mt-6 pt-4 border-t">
|
||||||
<button
|
<button
|
||||||
on:click={() => show = false}
|
on:click={() => (show = false)}
|
||||||
class="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded"
|
class="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
on:click={handleCommit}
|
on:click={handleCommit}
|
||||||
disabled={committing || !message || loading || (!status?.is_dirty && status?.staged_files?.length === 0)}
|
disabled={committing ||
|
||||||
|
!message ||
|
||||||
|
loading ||
|
||||||
|
(!status?.is_dirty &&
|
||||||
|
status?.staged_files?.length === 0)}
|
||||||
class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50"
|
class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{committing ? 'Committing...' : 'Commit'}
|
{committing ? "Committing..." : "Commit"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -203,10 +265,4 @@
|
|||||||
{/if}
|
{/if}
|
||||||
<!-- [/SECTION] -->
|
<!-- [/SECTION] -->
|
||||||
|
|
||||||
<style>
|
|
||||||
pre {
|
|
||||||
tab-size: 4;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<!-- [/DEF:CommitModal:Component] -->
|
<!-- [/DEF:CommitModal:Component] -->
|
||||||
@@ -10,14 +10,17 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
// [SECTION: IMPORTS]
|
// [SECTION: IMPORTS]
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from "svelte";
|
||||||
import { addToast as toast } from '../../lib/toasts.js';
|
import { addToast as toast } from "../../lib/toasts.js";
|
||||||
// [/SECTION]
|
// [/SECTION]
|
||||||
|
|
||||||
// [SECTION: PROPS]
|
// [SECTION: PROPS]
|
||||||
/** @type {Array<{file_path: string, mine: string, theirs: string}>} */
|
/** @type {Array<{file_path: string, mine: string, theirs: string}>} */
|
||||||
export let conflicts = [];
|
let {
|
||||||
export let show = false;
|
conflicts = [],
|
||||||
|
show = false,
|
||||||
|
} = $props();
|
||||||
|
|
||||||
// [/SECTION]
|
// [/SECTION]
|
||||||
|
|
||||||
// [SECTION: STATE]
|
// [SECTION: STATE]
|
||||||
@@ -36,7 +39,9 @@
|
|||||||
* @side_effect Updates resolutions state.
|
* @side_effect Updates resolutions state.
|
||||||
*/
|
*/
|
||||||
function resolve(file, strategy) {
|
function resolve(file, strategy) {
|
||||||
console.log(`[ConflictResolver][Action] Resolving ${file} with ${strategy}`);
|
console.log(
|
||||||
|
`[ConflictResolver][Action] Resolving ${file} with ${strategy}`,
|
||||||
|
);
|
||||||
resolutions[file] = strategy;
|
resolutions[file] = strategy;
|
||||||
resolutions = { ...resolutions }; // Trigger update
|
resolutions = { ...resolutions }; // Trigger update
|
||||||
}
|
}
|
||||||
@@ -51,16 +56,21 @@
|
|||||||
*/
|
*/
|
||||||
function handleSave() {
|
function handleSave() {
|
||||||
// 1. Guard Clause (@PRE)
|
// 1. Guard Clause (@PRE)
|
||||||
const unresolved = conflicts.filter(c => !resolutions[c.file_path]);
|
const unresolved = conflicts.filter((c) => !resolutions[c.file_path]);
|
||||||
if (unresolved.length > 0) {
|
if (unresolved.length > 0) {
|
||||||
console.warn(`[ConflictResolver][Coherence:Failed] ${unresolved.length} unresolved conflicts`);
|
console.warn(
|
||||||
toast(`Please resolve all conflicts first. (${unresolved.length} remaining)`, 'error');
|
`[ConflictResolver][Coherence:Failed] ${unresolved.length} unresolved conflicts`,
|
||||||
|
);
|
||||||
|
toast(
|
||||||
|
`Please resolve all conflicts first. (${unresolved.length} remaining)`,
|
||||||
|
"error",
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Implementation
|
// 2. Implementation
|
||||||
console.log(`[ConflictResolver][Coherence:OK] All conflicts resolved`);
|
console.log(`[ConflictResolver][Coherence:OK] All conflicts resolved`);
|
||||||
dispatch('resolve', resolutions);
|
dispatch("resolve", resolutions);
|
||||||
show = false;
|
show = false;
|
||||||
}
|
}
|
||||||
// [/DEF:handleSave:Function]
|
// [/DEF:handleSave:Function]
|
||||||
@@ -68,43 +78,78 @@
|
|||||||
|
|
||||||
<!-- [SECTION: TEMPLATE] -->
|
<!-- [SECTION: TEMPLATE] -->
|
||||||
{#if show}
|
{#if show}
|
||||||
<div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
<div
|
||||||
<div class="bg-white p-6 rounded-lg shadow-xl w-full max-w-5xl max-h-[90vh] flex flex-col">
|
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4"
|
||||||
<h2 class="text-xl font-bold mb-4 text-red-600">Merge Conflicts Detected</h2>
|
>
|
||||||
<p class="text-gray-600 mb-4">The following files have conflicts. Please choose how to resolve them.</p>
|
<div
|
||||||
|
class="bg-white p-6 rounded-lg shadow-xl w-full max-w-5xl max-h-[90vh] flex flex-col"
|
||||||
|
>
|
||||||
|
<h2 class="text-xl font-bold mb-4 text-red-600">
|
||||||
|
Merge Conflicts Detected
|
||||||
|
</h2>
|
||||||
|
<p class="text-gray-600 mb-4">
|
||||||
|
The following files have conflicts. Please choose how to resolve
|
||||||
|
them.
|
||||||
|
</p>
|
||||||
|
|
||||||
<div class="flex-1 overflow-y-auto space-y-6 mb-4 pr-2">
|
<div class="flex-1 overflow-y-auto space-y-6 mb-4 pr-2">
|
||||||
{#each conflicts as conflict}
|
{#each conflicts as conflict}
|
||||||
<div class="border rounded-lg overflow-hidden">
|
<div class="border rounded-lg overflow-hidden">
|
||||||
<div class="bg-gray-100 px-4 py-2 font-medium border-b flex justify-between items-center">
|
<div
|
||||||
|
class="bg-gray-100 px-4 py-2 font-medium border-b flex justify-between items-center"
|
||||||
|
>
|
||||||
<span>{conflict.file_path}</span>
|
<span>{conflict.file_path}</span>
|
||||||
{#if resolutions[conflict.file_path]}
|
{#if resolutions[conflict.file_path]}
|
||||||
<span class="text-xs bg-blue-100 text-blue-700 px-2 py-0.5 rounded-full uppercase font-bold">
|
<span
|
||||||
|
class="text-xs bg-blue-100 text-blue-700 px-2 py-0.5 rounded-full uppercase font-bold"
|
||||||
|
>
|
||||||
Resolved: {resolutions[conflict.file_path]}
|
Resolved: {resolutions[conflict.file_path]}
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-0 divide-x">
|
<div
|
||||||
|
class="grid grid-cols-1 md:grid-cols-2 gap-0 divide-x"
|
||||||
|
>
|
||||||
<div class="p-0 flex flex-col">
|
<div class="p-0 flex flex-col">
|
||||||
<div class="bg-blue-50 px-4 py-1 text-[10px] font-bold text-blue-600 uppercase border-b">Your Changes (Mine)</div>
|
<div
|
||||||
|
class="bg-blue-50 px-4 py-1 text-[10px] font-bold text-blue-600 uppercase border-b"
|
||||||
|
>
|
||||||
|
Your Changes (Mine)
|
||||||
|
</div>
|
||||||
<div class="p-4 bg-white flex-1 overflow-auto">
|
<div class="p-4 bg-white flex-1 overflow-auto">
|
||||||
<pre class="text-xs font-mono whitespace-pre">{conflict.mine}</pre>
|
<pre
|
||||||
|
class="text-xs font-mono whitespace-pre">{conflict.mine}</pre>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
class="w-full py-2 text-sm font-medium border-t transition-colors {resolutions[conflict.file_path] === 'mine' ? 'bg-blue-600 text-white' : 'bg-gray-50 hover:bg-blue-50 text-blue-600'}"
|
class="w-full py-2 text-sm font-medium border-t transition-colors {resolutions[
|
||||||
on:click={() => resolve(conflict.file_path, 'mine')}
|
conflict.file_path
|
||||||
|
] === 'mine'
|
||||||
|
? 'bg-blue-600 text-white'
|
||||||
|
: 'bg-gray-50 hover:bg-blue-50 text-blue-600'}"
|
||||||
|
on:click={() =>
|
||||||
|
resolve(conflict.file_path, "mine")}
|
||||||
>
|
>
|
||||||
Keep Mine
|
Keep Mine
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="p-0 flex flex-col">
|
<div class="p-0 flex flex-col">
|
||||||
<div class="bg-green-50 px-4 py-1 text-[10px] font-bold text-green-600 uppercase border-b">Remote Changes (Theirs)</div>
|
<div
|
||||||
|
class="bg-green-50 px-4 py-1 text-[10px] font-bold text-green-600 uppercase border-b"
|
||||||
|
>
|
||||||
|
Remote Changes (Theirs)
|
||||||
|
</div>
|
||||||
<div class="p-4 bg-white flex-1 overflow-auto">
|
<div class="p-4 bg-white flex-1 overflow-auto">
|
||||||
<pre class="text-xs font-mono whitespace-pre">{conflict.theirs}</pre>
|
<pre
|
||||||
|
class="text-xs font-mono whitespace-pre">{conflict.theirs}</pre>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
class="w-full py-2 text-sm font-medium border-t transition-colors {resolutions[conflict.file_path] === 'theirs' ? 'bg-green-600 text-white' : 'bg-gray-50 hover:bg-green-50 text-green-600'}"
|
class="w-full py-2 text-sm font-medium border-t transition-colors {resolutions[
|
||||||
on:click={() => resolve(conflict.file_path, 'theirs')}
|
conflict.file_path
|
||||||
|
] === 'theirs'
|
||||||
|
? 'bg-green-600 text-white'
|
||||||
|
: 'bg-gray-50 hover:bg-green-50 text-green-600'}"
|
||||||
|
on:click={() =>
|
||||||
|
resolve(conflict.file_path, "theirs")}
|
||||||
>
|
>
|
||||||
Keep Theirs
|
Keep Theirs
|
||||||
</button>
|
</button>
|
||||||
@@ -116,7 +161,7 @@
|
|||||||
|
|
||||||
<div class="flex justify-end space-x-3 pt-4 border-t">
|
<div class="flex justify-end space-x-3 pt-4 border-t">
|
||||||
<button
|
<button
|
||||||
on:click={() => show = false}
|
on:click={() => (show = false)}
|
||||||
class="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded transition-colors"
|
class="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded transition-colors"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
@@ -133,10 +178,4 @@
|
|||||||
{/if}
|
{/if}
|
||||||
<!-- [/SECTION] -->
|
<!-- [/SECTION] -->
|
||||||
|
|
||||||
<style>
|
|
||||||
pre {
|
|
||||||
tab-size: 4;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<!-- [/DEF:ConflictResolver:Component] -->
|
<!-- [/DEF:ConflictResolver:Component] -->
|
||||||
@@ -11,19 +11,19 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
// [SECTION: IMPORTS]
|
// [SECTION: IMPORTS]
|
||||||
import { onMount, createEventDispatcher } from 'svelte';
|
import { onMount, createEventDispatcher } from "svelte";
|
||||||
import { gitService } from '../../services/gitService';
|
import { gitService } from "../../services/gitService";
|
||||||
import { addToast as toast } from '../../lib/toasts.js';
|
import { addToast as toast } from "../../lib/toasts.js";
|
||||||
// [/SECTION]
|
// [/SECTION]
|
||||||
|
|
||||||
// [SECTION: PROPS]
|
// [SECTION: PROPS]
|
||||||
export let dashboardId;
|
let { dashboardId, show = false } = $props();
|
||||||
export let show = false;
|
|
||||||
// [/SECTION]
|
// [/SECTION]
|
||||||
|
|
||||||
// [SECTION: STATE]
|
// [SECTION: STATE]
|
||||||
let environments = [];
|
let environments = [];
|
||||||
let selectedEnv = '';
|
let selectedEnv = "";
|
||||||
let loading = false;
|
let loading = false;
|
||||||
let deploying = false;
|
let deploying = false;
|
||||||
// [/SECTION]
|
// [/SECTION]
|
||||||
@@ -31,7 +31,9 @@
|
|||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
// [DEF:loadStatus:Watcher]
|
// [DEF:loadStatus:Watcher]
|
||||||
$: if (show) loadEnvironments();
|
$effect(() => {
|
||||||
|
if (show) loadEnvironments();
|
||||||
|
});
|
||||||
// [/DEF:loadStatus:Watcher]
|
// [/DEF:loadStatus:Watcher]
|
||||||
|
|
||||||
// [DEF:loadEnvironments:Function]
|
// [DEF:loadEnvironments:Function]
|
||||||
@@ -48,10 +50,12 @@
|
|||||||
if (environments.length > 0) {
|
if (environments.length > 0) {
|
||||||
selectedEnv = environments[0].id;
|
selectedEnv = environments[0].id;
|
||||||
}
|
}
|
||||||
console.log(`[DeploymentModal][Coherence:OK] Loaded ${environments.length} environments`);
|
console.log(
|
||||||
|
`[DeploymentModal][Coherence:OK] Loaded ${environments.length} environments`,
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`[DeploymentModal][Coherence:Failed] ${e.message}`);
|
console.error(`[DeploymentModal][Coherence:Failed] ${e.message}`);
|
||||||
toast('Failed to load environments', 'error');
|
toast("Failed to load environments", "error");
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
@@ -71,13 +75,16 @@
|
|||||||
deploying = true;
|
deploying = true;
|
||||||
try {
|
try {
|
||||||
const result = await gitService.deploy(dashboardId, selectedEnv);
|
const result = await gitService.deploy(dashboardId, selectedEnv);
|
||||||
toast(result.message || 'Deployment triggered successfully', 'success');
|
toast(
|
||||||
dispatch('deploy');
|
result.message || "Deployment triggered successfully",
|
||||||
|
"success",
|
||||||
|
);
|
||||||
|
dispatch("deploy");
|
||||||
show = false;
|
show = false;
|
||||||
console.log(`[DeploymentModal][Coherence:OK] Deployment triggered`);
|
console.log(`[DeploymentModal][Coherence:OK] Deployment triggered`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`[DeploymentModal][Coherence:Failed] ${e.message}`);
|
console.error(`[DeploymentModal][Coherence:Failed] ${e.message}`);
|
||||||
toast(e.message, 'error');
|
toast(e.message, "error");
|
||||||
} finally {
|
} finally {
|
||||||
deploying = false;
|
deploying = false;
|
||||||
}
|
}
|
||||||
@@ -87,17 +94,21 @@
|
|||||||
|
|
||||||
<!-- [SECTION: TEMPLATE] -->
|
<!-- [SECTION: TEMPLATE] -->
|
||||||
{#if show}
|
{#if show}
|
||||||
<div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
<div
|
||||||
|
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
|
||||||
|
>
|
||||||
<div class="bg-white p-6 rounded-lg shadow-xl w-96">
|
<div class="bg-white p-6 rounded-lg shadow-xl w-96">
|
||||||
<h2 class="text-xl font-bold mb-4">Deploy Dashboard</h2>
|
<h2 class="text-xl font-bold mb-4">Deploy Dashboard</h2>
|
||||||
|
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<p class="text-gray-500">Loading environments...</p>
|
<p class="text-gray-500">Loading environments...</p>
|
||||||
{:else if environments.length === 0}
|
{:else if environments.length === 0}
|
||||||
<p class="text-red-500 mb-4">No deployment environments configured.</p>
|
<p class="text-red-500 mb-4">
|
||||||
|
No deployment environments configured.
|
||||||
|
</p>
|
||||||
<div class="flex justify-end">
|
<div class="flex justify-end">
|
||||||
<button
|
<button
|
||||||
on:click={() => show = false}
|
on:click={() => (show = false)}
|
||||||
class="px-4 py-2 bg-gray-200 text-gray-800 rounded hover:bg-gray-300"
|
class="px-4 py-2 bg-gray-200 text-gray-800 rounded hover:bg-gray-300"
|
||||||
>
|
>
|
||||||
Close
|
Close
|
||||||
@@ -105,20 +116,24 @@
|
|||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">Select Target Environment</label>
|
<label class="block text-sm font-medium text-gray-700 mb-2"
|
||||||
|
>Select Target Environment</label
|
||||||
|
>
|
||||||
<select
|
<select
|
||||||
bind:value={selectedEnv}
|
bind:value={selectedEnv}
|
||||||
class="w-full border rounded p-2 focus:ring-2 focus:ring-blue-500 outline-none bg-white"
|
class="w-full border rounded p-2 focus:ring-2 focus:ring-blue-500 outline-none bg-white"
|
||||||
>
|
>
|
||||||
{#each environments as env}
|
{#each environments as env}
|
||||||
<option value={env.id}>{env.name} ({env.superset_url})</option>
|
<option value={env.id}
|
||||||
|
>{env.name} ({env.superset_url})</option
|
||||||
|
>
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-end space-x-3">
|
<div class="flex justify-end space-x-3">
|
||||||
<button
|
<button
|
||||||
on:click={() => show = false}
|
on:click={() => (show = false)}
|
||||||
class="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded"
|
class="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
@@ -129,9 +144,25 @@
|
|||||||
class="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700 disabled:opacity-50 flex items-center"
|
class="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700 disabled:opacity-50 flex items-center"
|
||||||
>
|
>
|
||||||
{#if deploying}
|
{#if deploying}
|
||||||
<svg class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
<svg
|
||||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
class="animate-spin -ml-1 mr-2 h-4 w-4 text-white"
|
||||||
<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>
|
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>
|
</svg>
|
||||||
Deploying...
|
Deploying...
|
||||||
{:else}
|
{:else}
|
||||||
|
|||||||
@@ -26,9 +26,12 @@
|
|||||||
// [/SECTION]
|
// [/SECTION]
|
||||||
|
|
||||||
// [SECTION: PROPS]
|
// [SECTION: PROPS]
|
||||||
export let dashboardId;
|
let {
|
||||||
export let dashboardTitle = "";
|
dashboardId,
|
||||||
export let show = false;
|
dashboardTitle = "",
|
||||||
|
show = false,
|
||||||
|
} = $props();
|
||||||
|
|
||||||
// [/SECTION]
|
// [/SECTION]
|
||||||
|
|
||||||
// [SECTION: STATE]
|
// [SECTION: STATE]
|
||||||
|
|||||||
@@ -10,9 +10,12 @@
|
|||||||
import { t } from '../../lib/i18n';
|
import { t } from '../../lib/i18n';
|
||||||
|
|
||||||
/** @type {Object} */
|
/** @type {Object} */
|
||||||
export let documentation = null;
|
let {
|
||||||
export let onSave = async (doc) => {};
|
content = "",
|
||||||
export let onCancel = () => {};
|
type = 'markdown',
|
||||||
|
format = 'text',
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
|
||||||
let isSaving = false;
|
let isSaving = false;
|
||||||
|
|
||||||
|
|||||||
@@ -12,8 +12,11 @@
|
|||||||
import { requestApi } from '../../lib/api';
|
import { requestApi } from '../../lib/api';
|
||||||
|
|
||||||
/** @type {Array} */
|
/** @type {Array} */
|
||||||
export let providers = [];
|
let {
|
||||||
export let onSave = () => {};
|
provider,
|
||||||
|
config = {},
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
|
||||||
let editingProvider = null;
|
let editingProvider = null;
|
||||||
let showForm = false;
|
let showForm = false;
|
||||||
|
|||||||
@@ -3,7 +3,10 @@
|
|||||||
<!-- @PURPOSE: Displays the results of an LLM-based dashboard validation task. -->
|
<!-- @PURPOSE: Displays the results of an LLM-based dashboard validation task. -->
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export let result = null;
|
let {
|
||||||
|
report,
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
|
||||||
function getStatusColor(status) {
|
function getStatusColor(status) {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
|
|||||||
@@ -17,7 +17,10 @@
|
|||||||
import { t } from '../../lib/i18n';
|
import { t } from '../../lib/i18n';
|
||||||
// [/SECTION: IMPORTS]
|
// [/SECTION: IMPORTS]
|
||||||
|
|
||||||
export let files = [];
|
let {
|
||||||
|
files = [],
|
||||||
|
} = $props();
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
// [DEF:isDirectory:Function]
|
// [DEF:isDirectory:Function]
|
||||||
@@ -137,8 +140,5 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- [/SECTION: TEMPLATE] -->
|
<!-- [/SECTION: TEMPLATE] -->
|
||||||
|
|
||||||
<style>
|
|
||||||
/* ... */
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<!-- [/DEF:FileList:Component] -->
|
<!-- [/DEF:FileList:Component] -->
|
||||||
@@ -26,8 +26,11 @@
|
|||||||
*/
|
*/
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
let fileInput;
|
let fileInput;
|
||||||
export let category = 'backups';
|
let {
|
||||||
export let path = '';
|
category = 'backups',
|
||||||
|
path = '',
|
||||||
|
} = $props();
|
||||||
|
|
||||||
let isUploading = false;
|
let isUploading = false;
|
||||||
let dragOver = false;
|
let dragOver = false;
|
||||||
|
|
||||||
@@ -128,8 +131,5 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- [/SECTION: TEMPLATE] -->
|
<!-- [/SECTION: TEMPLATE] -->
|
||||||
|
|
||||||
<style>
|
|
||||||
/* ... */
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<!-- [/DEF:FileUpload:Component] -->
|
<!-- [/DEF:FileUpload:Component] -->
|
||||||
@@ -8,14 +8,10 @@
|
|||||||
-->
|
-->
|
||||||
<script>
|
<script>
|
||||||
/** @type {Object} log - The log entry object */
|
/** @type {Object} log - The log entry object */
|
||||||
export let log;
|
let { log, showSource = true } = $props();
|
||||||
/** @type {boolean} showSource - Whether to show the source tag */
|
|
||||||
export let showSource = true;
|
|
||||||
|
|
||||||
// [DEF:formatTime:Function]
|
// [DEF:formatTime:Function]
|
||||||
/** @PURPOSE Format ISO timestamp to HH:MM:SS */
|
/** @PURPOSE Format ISO timestamp to HH:MM:SS */
|
||||||
$: formattedTime = formatTime(log.timestamp);
|
|
||||||
|
|
||||||
function formatTime(timestamp) {
|
function formatTime(timestamp) {
|
||||||
if (!timestamp) return "";
|
if (!timestamp) return "";
|
||||||
const date = new Date(timestamp);
|
const date = new Date(timestamp);
|
||||||
@@ -28,180 +24,77 @@
|
|||||||
}
|
}
|
||||||
// [/DEF:formatTime:Function]
|
// [/DEF:formatTime:Function]
|
||||||
|
|
||||||
$: levelClass = getLevelClass(log.level);
|
let formattedTime = $derived(formatTime(log.timestamp));
|
||||||
|
|
||||||
function getLevelClass(level) {
|
const levelStyles = {
|
||||||
switch (level?.toUpperCase()) {
|
DEBUG: "text-log-debug bg-log-debug/15",
|
||||||
case "DEBUG":
|
INFO: "text-log-info bg-log-info/10",
|
||||||
return "level-debug";
|
WARNING: "text-log-warning bg-log-warning/10",
|
||||||
case "INFO":
|
ERROR: "text-log-error bg-log-error/10",
|
||||||
return "level-info";
|
};
|
||||||
case "WARNING":
|
|
||||||
return "level-warning";
|
|
||||||
case "ERROR":
|
|
||||||
return "level-error";
|
|
||||||
default:
|
|
||||||
return "level-info";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$: sourceClass = getSourceClass(log.source);
|
const sourceStyles = {
|
||||||
|
plugin: "bg-source-plugin/10 text-source-plugin",
|
||||||
|
"superset-api": "bg-source-api/10 text-source-api",
|
||||||
|
superset_api: "bg-source-api/10 text-source-api",
|
||||||
|
git: "bg-source-git/10 text-source-git",
|
||||||
|
system: "bg-source-system/10 text-source-system",
|
||||||
|
};
|
||||||
|
|
||||||
function getSourceClass(source) {
|
let levelClass = $derived(
|
||||||
if (!source) return "source-default";
|
levelStyles[log.level?.toUpperCase()] || levelStyles.INFO,
|
||||||
return `source-${source.toLowerCase().replace(/[^a-z0-9]/g, "-")}`;
|
);
|
||||||
}
|
let sourceClass = $derived(
|
||||||
|
sourceStyles[log.source?.toLowerCase()] ||
|
||||||
|
"bg-log-debug/15 text-terminal-text-subtle",
|
||||||
|
);
|
||||||
|
|
||||||
$: hasProgress = log.metadata?.progress !== undefined;
|
let hasProgress = $derived(log.metadata?.progress !== undefined);
|
||||||
$: progressPercent = log.metadata?.progress || 0;
|
let progressPercent = $derived(log.metadata?.progress || 0);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="log-row">
|
<div
|
||||||
|
class="py-2 px-3 border-b border-terminal-surface/60 transition-colors hover:bg-terminal-surface/50"
|
||||||
|
>
|
||||||
<!-- Meta line: time + level + source -->
|
<!-- Meta line: time + level + source -->
|
||||||
<div class="log-meta">
|
<div class="flex items-center gap-2 mb-1">
|
||||||
<span class="log-time">{formattedTime}</span>
|
<span class="font-mono text-[0.6875rem] text-terminal-text-muted shrink-0"
|
||||||
<span class="log-level {levelClass}">{log.level || "INFO"}</span>
|
>{formattedTime}</span
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="font-mono font-semibold uppercase text-[0.625rem] px-1.5 py-px rounded-sm tracking-wider shrink-0 {levelClass}"
|
||||||
|
>{log.level || "INFO"}</span
|
||||||
|
>
|
||||||
{#if showSource && log.source}
|
{#if showSource && log.source}
|
||||||
<span class="log-source {sourceClass}">{log.source}</span>
|
<span
|
||||||
|
class="text-[0.625rem] px-1.5 py-px rounded-sm shrink-0 {sourceClass}"
|
||||||
|
>{log.source}</span
|
||||||
|
>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Message -->
|
<!-- Message -->
|
||||||
<div class="log-message">{log.message}</div>
|
<div
|
||||||
|
class="font-mono text-[0.8125rem] leading-relaxed text-terminal-text break-words whitespace-pre-wrap"
|
||||||
|
>
|
||||||
|
{log.message}
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Progress bar (if applicable) -->
|
<!-- Progress bar (if applicable) -->
|
||||||
{#if hasProgress}
|
{#if hasProgress}
|
||||||
<div class="progress-container">
|
<div class="flex items-center gap-2 mt-1.5">
|
||||||
<div class="progress-track">
|
<div
|
||||||
<div class="progress-fill" style="width: {progressPercent}%"></div>
|
class="flex-1 h-1.5 bg-terminal-surface rounded-full overflow-hidden"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="h-full bg-gradient-to-r from-primary to-purple-500 rounded-full transition-[width] duration-300 ease-out"
|
||||||
|
style="width: {progressPercent}%"
|
||||||
|
></div>
|
||||||
</div>
|
</div>
|
||||||
<span class="progress-text">{progressPercent.toFixed(0)}%</span>
|
<span class="font-mono text-[0.625rem] text-terminal-text-subtle shrink-0"
|
||||||
|
>{progressPercent.toFixed(0)}%</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<!-- [/DEF:LogEntryRow:Component] -->
|
<!-- [/DEF:LogEntryRow:Component] -->
|
||||||
|
|
||||||
<style>
|
|
||||||
.log-row {
|
|
||||||
padding: 0.5rem 0.75rem;
|
|
||||||
border-bottom: 1px solid rgba(30, 41, 59, 0.6);
|
|
||||||
transition: background-color 0.1s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.log-row:hover {
|
|
||||||
background-color: rgba(30, 41, 59, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.log-meta {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
margin-bottom: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.log-time {
|
|
||||||
font-family: "JetBrains Mono", "Fira Code", monospace;
|
|
||||||
font-size: 0.6875rem;
|
|
||||||
color: #475569;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.log-level {
|
|
||||||
font-family: "JetBrains Mono", "Fira Code", monospace;
|
|
||||||
font-weight: 600;
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-size: 0.625rem;
|
|
||||||
padding: 0.0625rem 0.375rem;
|
|
||||||
border-radius: 0.1875rem;
|
|
||||||
letter-spacing: 0.03em;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.level-debug {
|
|
||||||
color: #64748b;
|
|
||||||
background-color: rgba(100, 116, 139, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
.level-info {
|
|
||||||
color: #38bdf8;
|
|
||||||
background-color: rgba(56, 189, 248, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.level-warning {
|
|
||||||
color: #fbbf24;
|
|
||||||
background-color: rgba(251, 191, 36, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.level-error {
|
|
||||||
color: #f87171;
|
|
||||||
background-color: rgba(248, 113, 113, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.log-source {
|
|
||||||
font-size: 0.625rem;
|
|
||||||
padding: 0.0625rem 0.375rem;
|
|
||||||
border-radius: 0.1875rem;
|
|
||||||
background-color: rgba(100, 116, 139, 0.15);
|
|
||||||
color: #64748b;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.source-plugin {
|
|
||||||
background-color: rgba(34, 197, 94, 0.1);
|
|
||||||
color: #4ade80;
|
|
||||||
}
|
|
||||||
|
|
||||||
.source-superset-api,
|
|
||||||
.source-superset_api {
|
|
||||||
background-color: rgba(168, 85, 247, 0.1);
|
|
||||||
color: #c084fc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.source-git {
|
|
||||||
background-color: rgba(249, 115, 22, 0.1);
|
|
||||||
color: #fb923c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.source-system {
|
|
||||||
background-color: rgba(56, 189, 248, 0.1);
|
|
||||||
color: #38bdf8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.log-message {
|
|
||||||
font-family: "JetBrains Mono", "Fira Code", monospace;
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
line-height: 1.5;
|
|
||||||
color: #cbd5e1;
|
|
||||||
word-break: break-word;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-container {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
margin-top: 0.375rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-track {
|
|
||||||
flex: 1;
|
|
||||||
height: 0.375rem;
|
|
||||||
background-color: #1e293b;
|
|
||||||
border-radius: 9999px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-fill {
|
|
||||||
height: 100%;
|
|
||||||
background: linear-gradient(90deg, #3b82f6, #8b5cf6);
|
|
||||||
border-radius: 9999px;
|
|
||||||
transition: width 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-text {
|
|
||||||
font-family: "JetBrains Mono", "Fira Code", monospace;
|
|
||||||
font-size: 0.625rem;
|
|
||||||
color: #64748b;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -10,14 +10,12 @@
|
|||||||
<script>
|
<script>
|
||||||
import { createEventDispatcher } from "svelte";
|
import { createEventDispatcher } from "svelte";
|
||||||
|
|
||||||
/** @type {string[]} availableSources - List of available source options */
|
let {
|
||||||
export let availableSources = [];
|
availableSources = [],
|
||||||
/** @type {string} selectedLevel - Currently selected log level filter */
|
selectedLevel = $bindable(""),
|
||||||
export let selectedLevel = "";
|
selectedSource = $bindable(""),
|
||||||
/** @type {string} selectedSource - Currently selected source filter */
|
searchText = $bindable(""),
|
||||||
export let selectedSource = "";
|
} = $props();
|
||||||
/** @type {string} searchText - Current search text */
|
|
||||||
export let searchText = "";
|
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
@@ -63,13 +61,18 @@
|
|||||||
dispatch("filter-change", { level: "", source: "", search: "" });
|
dispatch("filter-change", { level: "", source: "", search: "" });
|
||||||
}
|
}
|
||||||
|
|
||||||
$: hasActiveFilters = selectedLevel || selectedSource || searchText;
|
let hasActiveFilters = $derived(
|
||||||
|
selectedLevel || selectedSource || searchText,
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="filter-bar">
|
<div
|
||||||
<div class="filter-controls">
|
class="flex items-center gap-1.5 px-3 py-2 bg-terminal-bg border-b border-terminal-surface"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-1.5 flex-1 min-w-0">
|
||||||
<select
|
<select
|
||||||
class="filter-select"
|
class="bg-terminal-surface text-terminal-text-subtle border border-terminal-border rounded px-2 py-[0.3125rem] text-xs cursor-pointer shrink-0 appearance-none pr-6 focus:outline-none focus:border-primary-ring"
|
||||||
|
style="background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%2364748b' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 0.375rem center;"
|
||||||
value={selectedLevel}
|
value={selectedLevel}
|
||||||
on:change={handleLevelChange}
|
on:change={handleLevelChange}
|
||||||
aria-label="Filter by level"
|
aria-label="Filter by level"
|
||||||
@@ -80,7 +83,8 @@
|
|||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select
|
<select
|
||||||
class="filter-select"
|
class="bg-terminal-surface text-terminal-text-subtle border border-terminal-border rounded px-2 py-[0.3125rem] text-xs cursor-pointer shrink-0 appearance-none pr-6 focus:outline-none focus:border-primary-ring"
|
||||||
|
style="background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%2364748b' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 0.375rem center;"
|
||||||
value={selectedSource}
|
value={selectedSource}
|
||||||
on:change={handleSourceChange}
|
on:change={handleSourceChange}
|
||||||
aria-label="Filter by source"
|
aria-label="Filter by source"
|
||||||
@@ -91,9 +95,9 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<div class="search-wrapper">
|
<div class="relative flex-1 min-w-0">
|
||||||
<svg
|
<svg
|
||||||
class="search-icon"
|
class="absolute left-2 top-1/2 -translate-y-1/2 text-terminal-text-muted pointer-events-none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
width="14"
|
width="14"
|
||||||
height="14"
|
height="14"
|
||||||
@@ -107,7 +111,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="search-input"
|
class="w-full bg-terminal-surface text-terminal-text-bright border border-terminal-border rounded py-[0.3125rem] px-2 pl-7 text-xs placeholder:text-terminal-text-muted focus:outline-none focus:border-primary-ring"
|
||||||
placeholder="Search..."
|
placeholder="Search..."
|
||||||
value={searchText}
|
value={searchText}
|
||||||
on:input={handleSearchChange}
|
on:input={handleSearchChange}
|
||||||
@@ -118,7 +122,7 @@
|
|||||||
|
|
||||||
{#if hasActiveFilters}
|
{#if hasActiveFilters}
|
||||||
<button
|
<button
|
||||||
class="clear-btn"
|
class="flex items-center justify-center p-[0.3125rem] bg-transparent border border-terminal-border rounded text-terminal-text-subtle shrink-0 cursor-pointer transition-all hover:text-log-error hover:border-log-error hover:bg-log-error/10"
|
||||||
on:click={clearFilters}
|
on:click={clearFilters}
|
||||||
aria-label="Clear filters"
|
aria-label="Clear filters"
|
||||||
>
|
>
|
||||||
@@ -137,98 +141,3 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<!-- [/DEF:LogFilterBar:Component] -->
|
<!-- [/DEF:LogFilterBar:Component] -->
|
||||||
|
|
||||||
<style>
|
|
||||||
.filter-bar {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.375rem;
|
|
||||||
padding: 0.5rem 0.75rem;
|
|
||||||
background-color: #0f172a;
|
|
||||||
border-bottom: 1px solid #1e293b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-controls {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.375rem;
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-select {
|
|
||||||
background-color: #1e293b;
|
|
||||||
color: #94a3b8;
|
|
||||||
border: 1px solid #334155;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
padding: 0.3125rem 0.5rem;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
cursor: pointer;
|
|
||||||
flex-shrink: 0;
|
|
||||||
appearance: none;
|
|
||||||
-webkit-appearance: none;
|
|
||||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%2364748b' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-position: right 0.375rem center;
|
|
||||||
padding-right: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-select:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: #3b82f6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-wrapper {
|
|
||||||
position: relative;
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-icon {
|
|
||||||
position: absolute;
|
|
||||||
left: 0.5rem;
|
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
color: #475569;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-input {
|
|
||||||
width: 100%;
|
|
||||||
background-color: #1e293b;
|
|
||||||
color: #e2e8f0;
|
|
||||||
border: 1px solid #334155;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
padding: 0.3125rem 0.5rem 0.3125rem 1.75rem;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-input::placeholder {
|
|
||||||
color: #475569;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-input:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: #3b82f6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.clear-btn {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 0.3125rem;
|
|
||||||
background: none;
|
|
||||||
border: 1px solid #334155;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
color: #64748b;
|
|
||||||
cursor: pointer;
|
|
||||||
flex-shrink: 0;
|
|
||||||
transition: all 0.15s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.clear-btn:hover {
|
|
||||||
color: #f87171;
|
|
||||||
border-color: #f87171;
|
|
||||||
background-color: rgba(248, 113, 113, 0.1);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -12,25 +12,21 @@
|
|||||||
@UX_STATE: AutoScroll -> Automatically scrolls to bottom on new logs
|
@UX_STATE: AutoScroll -> Automatically scrolls to bottom on new logs
|
||||||
-->
|
-->
|
||||||
<script>
|
<script>
|
||||||
import { createEventDispatcher, onMount, afterUpdate } from "svelte";
|
import { createEventDispatcher, onMount, tick } from "svelte";
|
||||||
import LogFilterBar from "./LogFilterBar.svelte";
|
import LogFilterBar from "./LogFilterBar.svelte";
|
||||||
import LogEntryRow from "./LogEntryRow.svelte";
|
import LogEntryRow from "./LogEntryRow.svelte";
|
||||||
|
|
||||||
/**
|
let { logs = [], autoScroll = $bindable(true) } = $props();
|
||||||
* @PURPOSE Component properties and state.
|
|
||||||
* @PRE logs is an array of LogEntry objects.
|
|
||||||
*/
|
|
||||||
export let logs = [];
|
|
||||||
export let autoScroll = true;
|
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
let scrollContainer;
|
let scrollContainer;
|
||||||
let selectedSource = "all";
|
let selectedSource = $state("all");
|
||||||
let selectedLevel = "all";
|
let selectedLevel = $state("all");
|
||||||
let searchText = "";
|
let searchText = $state("");
|
||||||
|
|
||||||
// Filtered logs based on current filters
|
let filteredLogs = $derived(
|
||||||
$: filteredLogs = filterLogs(logs, selectedLevel, selectedSource, searchText);
|
filterLogs(logs, selectedLevel, selectedSource, searchText),
|
||||||
|
);
|
||||||
|
|
||||||
function filterLogs(allLogs, level, source, search) {
|
function filterLogs(allLogs, level, source, search) {
|
||||||
return allLogs.filter((log) => {
|
return allLogs.filter((log) => {
|
||||||
@@ -52,17 +48,15 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract unique sources from logs
|
let availableSources = $derived([
|
||||||
$: availableSources = [...new Set(logs.map((l) => l.source).filter(Boolean))];
|
...new Set(logs.map((l) => l.source).filter(Boolean)),
|
||||||
|
]);
|
||||||
|
|
||||||
function handleFilterChange(event) {
|
function handleFilterChange(event) {
|
||||||
const { source, level, search } = event.detail;
|
const { source, level, search } = event.detail;
|
||||||
selectedSource = source || "all";
|
selectedSource = source || "all";
|
||||||
selectedLevel = level || "all";
|
selectedLevel = level || "all";
|
||||||
searchText = search || "";
|
searchText = search || "";
|
||||||
console.log(
|
|
||||||
`[TaskLogPanel][Action] Filter: level=${selectedLevel}, source=${selectedSource}, search=${searchText}`,
|
|
||||||
);
|
|
||||||
dispatch("filterChange", { source, level });
|
dispatch("filterChange", { source, level });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,8 +71,11 @@
|
|||||||
if (autoScroll) scrollToBottom();
|
if (autoScroll) scrollToBottom();
|
||||||
}
|
}
|
||||||
|
|
||||||
afterUpdate(() => {
|
// Use $effect instead of afterUpdate for runes mode
|
||||||
scrollToBottom();
|
$effect(() => {
|
||||||
|
// Track filteredLogs length to trigger scroll
|
||||||
|
filteredLogs.length;
|
||||||
|
tick().then(scrollToBottom);
|
||||||
});
|
});
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
@@ -86,14 +83,19 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="log-panel">
|
<div class="flex flex-col h-full bg-terminal-bg overflow-hidden">
|
||||||
<!-- Filter Bar -->
|
<!-- Filter Bar -->
|
||||||
<LogFilterBar {availableSources} on:filter-change={handleFilterChange} />
|
<LogFilterBar {availableSources} on:filter-change={handleFilterChange} />
|
||||||
|
|
||||||
<!-- Log List -->
|
<!-- Log List -->
|
||||||
<div bind:this={scrollContainer} class="log-list">
|
<div
|
||||||
|
bind:this={scrollContainer}
|
||||||
|
class="flex-1 overflow-y-auto overflow-x-hidden"
|
||||||
|
>
|
||||||
{#if filteredLogs.length === 0}
|
{#if filteredLogs.length === 0}
|
||||||
<div class="empty-logs">
|
<div
|
||||||
|
class="flex flex-col items-center justify-center py-12 px-4 text-terminal-border gap-3"
|
||||||
|
>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
width="32"
|
width="32"
|
||||||
@@ -109,7 +111,9 @@
|
|||||||
<line x1="16" y1="17" x2="8" y2="17" />
|
<line x1="16" y1="17" x2="8" y2="17" />
|
||||||
<polyline points="10 9 9 9 8 9" />
|
<polyline points="10 9 9 9 8 9" />
|
||||||
</svg>
|
</svg>
|
||||||
<span>No logs available</span>
|
<span class="text-[0.8125rem] text-terminal-text-muted"
|
||||||
|
>No logs available</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
{#each filteredLogs as log}
|
{#each filteredLogs as log}
|
||||||
@@ -119,129 +123,27 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Footer Stats -->
|
<!-- Footer Stats -->
|
||||||
<div class="log-footer">
|
<div
|
||||||
<span class="log-count">
|
class="flex items-center justify-between py-1.5 px-3 border-t border-terminal-surface bg-terminal-bg"
|
||||||
|
>
|
||||||
|
<span class="font-mono text-[0.6875rem] text-terminal-text-muted">
|
||||||
{filteredLogs.length}{filteredLogs.length !== logs.length
|
{filteredLogs.length}{filteredLogs.length !== logs.length
|
||||||
? ` / ${logs.length}`
|
? ` / ${logs.length}`
|
||||||
: ""} entries
|
: ""} entries
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
class="autoscroll-btn"
|
class="flex items-center gap-1.5 bg-transparent border-none text-terminal-text-muted text-[0.6875rem] cursor-pointer py-px px-1.5 rounded transition-all hover:bg-terminal-surface hover:text-terminal-text-subtle
|
||||||
class:active={autoScroll}
|
{autoScroll ? 'text-terminal-accent' : ''}"
|
||||||
on:click={toggleAutoScroll}
|
on:click={toggleAutoScroll}
|
||||||
aria-label="Toggle auto-scroll"
|
aria-label="Toggle auto-scroll"
|
||||||
>
|
>
|
||||||
{#if autoScroll}
|
{#if autoScroll}
|
||||||
<span class="pulse-dot"></span>
|
<span
|
||||||
|
class="inline-block w-[5px] h-[5px] rounded-full bg-terminal-accent animate-pulse"
|
||||||
|
></span>
|
||||||
{/if}
|
{/if}
|
||||||
Auto-scroll {autoScroll ? "on" : "off"}
|
Auto-scroll {autoScroll ? "on" : "off"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- [/DEF:TaskLogPanel:Component] -->
|
<!-- [/DEF:TaskLogPanel:Component] -->
|
||||||
|
|
||||||
<style>
|
|
||||||
.log-panel {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100%;
|
|
||||||
background-color: #0f172a;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.log-list {
|
|
||||||
flex: 1;
|
|
||||||
overflow-y: auto;
|
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Custom scrollbar */
|
|
||||||
.log-list::-webkit-scrollbar {
|
|
||||||
width: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.log-list::-webkit-scrollbar-track {
|
|
||||||
background: #0f172a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.log-list::-webkit-scrollbar-thumb {
|
|
||||||
background: #334155;
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.log-list::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: #475569;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-logs {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 3rem 1rem;
|
|
||||||
color: #334155;
|
|
||||||
gap: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-logs span {
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
color: #475569;
|
|
||||||
}
|
|
||||||
|
|
||||||
.log-footer {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 0.375rem 0.75rem;
|
|
||||||
border-top: 1px solid #1e293b;
|
|
||||||
background-color: #0f172a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.log-count {
|
|
||||||
font-family: "JetBrains Mono", "Fira Code", monospace;
|
|
||||||
font-size: 0.6875rem;
|
|
||||||
color: #475569;
|
|
||||||
}
|
|
||||||
|
|
||||||
.autoscroll-btn {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.375rem;
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: #475569;
|
|
||||||
font-size: 0.6875rem;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 0.125rem 0.375rem;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
transition: all 0.15s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.autoscroll-btn:hover {
|
|
||||||
background-color: #1e293b;
|
|
||||||
color: #94a3b8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.autoscroll-btn.active {
|
|
||||||
color: #22d3ee;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pulse-dot {
|
|
||||||
display: inline-block;
|
|
||||||
width: 5px;
|
|
||||||
height: 5px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background-color: #22d3ee;
|
|
||||||
animation: pulse 2s infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pulse {
|
|
||||||
0%,
|
|
||||||
100% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
opacity: 0.3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -12,13 +12,15 @@
|
|||||||
* @UX_RECOVERY: Click breadcrumb to navigate
|
* @UX_RECOVERY: Click breadcrumb to navigate
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { page } from '$app/stores';
|
import { page } from "$app/stores";
|
||||||
import { t, _ } from '$lib/i18n';
|
import { t, _ } from "$lib/i18n";
|
||||||
|
|
||||||
export let maxVisible = 3;
|
let { maxVisible = 3 } = $props();
|
||||||
|
|
||||||
// Breadcrumb items derived from current path
|
// Breadcrumb items derived from current path
|
||||||
$: breadcrumbItems = getBreadcrumbs($page?.url?.pathname || '/', maxVisible);
|
let breadcrumbItems = $derived(
|
||||||
|
getBreadcrumbs($page?.url?.pathname || "/", maxVisible),
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate breadcrumb items from path
|
* Generate breadcrumb items from path
|
||||||
@@ -26,42 +28,26 @@
|
|||||||
* @returns {Array} Array of breadcrumb items
|
* @returns {Array} Array of breadcrumb items
|
||||||
*/
|
*/
|
||||||
function getBreadcrumbs(pathname, maxVisible = 3) {
|
function getBreadcrumbs(pathname, maxVisible = 3) {
|
||||||
const segments = pathname.split('/').filter(Boolean);
|
const segments = pathname.split("/").filter(Boolean);
|
||||||
const allItems = [
|
const allItems = [{ label: "Home", path: "/" }];
|
||||||
{ label: 'Home', path: '/' }
|
|
||||||
];
|
|
||||||
|
|
||||||
let currentPath = '';
|
let currentPath = "";
|
||||||
segments.forEach((segment, index) => {
|
segments.forEach((segment, index) => {
|
||||||
currentPath += `/${segment}`;
|
currentPath += `/${segment}`;
|
||||||
// Convert segment to readable label
|
|
||||||
const label = formatBreadcrumbLabel(segment);
|
const label = formatBreadcrumbLabel(segment);
|
||||||
allItems.push({
|
allItems.push({
|
||||||
label,
|
label,
|
||||||
path: currentPath,
|
path: currentPath,
|
||||||
isLast: index === segments.length - 1
|
isLast: index === segments.length - 1,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle truncation if too many items
|
|
||||||
// If we have more than maxVisible items, we truncate the middle ones
|
|
||||||
// Always show Home (first) and Current (last)
|
|
||||||
if (allItems.length > maxVisible) {
|
if (allItems.length > maxVisible) {
|
||||||
const firstItem = allItems[0];
|
const firstItem = allItems[0];
|
||||||
const lastItem = allItems[allItems.length - 1];
|
|
||||||
|
|
||||||
// Calculate how many items we can show in the middle
|
|
||||||
// We reserve 1 for first, 1 for last, and 1 for ellipsis
|
|
||||||
// But ellipsis isn't a real item in terms of logic, it just replaces hidden ones
|
|
||||||
// Actually, let's keep it simple: First ... [Last - (maxVisible - 2) .. Last]
|
|
||||||
|
|
||||||
const itemsToShow = [];
|
const itemsToShow = [];
|
||||||
itemsToShow.push(firstItem);
|
itemsToShow.push(firstItem);
|
||||||
itemsToShow.push({ isEllipsis: true });
|
itemsToShow.push({ isEllipsis: true });
|
||||||
|
|
||||||
// Add the last (maxVisible - 2) items
|
|
||||||
// e.g. if maxVisible is 3, we show Start ... End
|
|
||||||
// if maxVisible is 4, we show Start ... SecondLast End
|
|
||||||
const startFromIndex = allItems.length - (maxVisible - 1);
|
const startFromIndex = allItems.length - (maxVisible - 1);
|
||||||
for (let i = startFromIndex; i < allItems.length; i++) {
|
for (let i = startFromIndex; i < allItems.length; i++) {
|
||||||
itemsToShow.push(allItems[i]);
|
itemsToShow.push(allItems[i]);
|
||||||
@@ -78,63 +64,46 @@
|
|||||||
* @returns {string} Formatted label
|
* @returns {string} Formatted label
|
||||||
*/
|
*/
|
||||||
function formatBreadcrumbLabel(segment) {
|
function formatBreadcrumbLabel(segment) {
|
||||||
// Handle special cases
|
|
||||||
const specialCases = {
|
const specialCases = {
|
||||||
'dashboards': 'nav.dashboard',
|
dashboards: "nav.dashboard",
|
||||||
'datasets': 'nav.tools_mapper',
|
datasets: "nav.tools_mapper",
|
||||||
'storage': 'nav.tools_storage',
|
storage: "nav.tools_storage",
|
||||||
'admin': 'nav.admin',
|
admin: "nav.admin",
|
||||||
'settings': 'nav.settings',
|
settings: "nav.settings",
|
||||||
'git': 'nav.git'
|
git: "nav.git",
|
||||||
};
|
};
|
||||||
|
|
||||||
if (specialCases[segment]) {
|
if (specialCases[segment]) {
|
||||||
return _(specialCases[segment]) || segment;
|
return _(specialCases[segment]) || segment;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default: capitalize and replace hyphens with spaces
|
|
||||||
return segment
|
return segment
|
||||||
.split('-')
|
.split("-")
|
||||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||||
.join(' ');
|
.join(" ");
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<nav
|
||||||
.breadcrumbs {
|
class="flex items-center space-x-2 text-sm text-gray-600"
|
||||||
@apply flex items-center space-x-2 text-sm text-gray-600;
|
aria-label="Breadcrumb navigation"
|
||||||
}
|
>
|
||||||
|
|
||||||
.breadcrumb-item {
|
|
||||||
@apply flex items-center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.breadcrumb-link {
|
|
||||||
@apply hover:text-blue-600 hover:underline cursor-pointer transition-colors;
|
|
||||||
}
|
|
||||||
|
|
||||||
.breadcrumb-current {
|
|
||||||
@apply text-gray-900 font-medium;
|
|
||||||
}
|
|
||||||
|
|
||||||
.breadcrumb-separator {
|
|
||||||
@apply text-gray-400;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<nav class="breadcrumbs" aria-label="Breadcrumb navigation">
|
|
||||||
{#each breadcrumbItems as item, index}
|
{#each breadcrumbItems as item, index}
|
||||||
<div class="breadcrumb-item">
|
<div class="flex items-center">
|
||||||
{#if item.isEllipsis}
|
{#if item.isEllipsis}
|
||||||
<span class="breadcrumb-separator">...</span>
|
<span class="text-gray-400">...</span>
|
||||||
{:else if item.isLast}
|
{:else if item.isLast}
|
||||||
<span class="breadcrumb-current">{item.label}</span>
|
<span class="text-gray-900 font-medium">{item.label}</span>
|
||||||
{:else}
|
{:else}
|
||||||
<a href={item.path} class="breadcrumb-link">{item.label}</a>
|
<a
|
||||||
|
href={item.path}
|
||||||
|
class="hover:text-primary hover:underline cursor-pointer transition-colors"
|
||||||
|
>{item.label}</a
|
||||||
|
>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if index < breadcrumbItems.length - 1}
|
{#if index < breadcrumbItems.length - 1}
|
||||||
<span class="breadcrumb-separator">/</span>
|
<span class="text-gray-400">/</span>
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
{
|
{
|
||||||
id: "dashboards",
|
id: "dashboards",
|
||||||
label: $t.nav?.dashboards || "DASHBOARDS",
|
label: $t.nav?.dashboards || "DASHBOARDS",
|
||||||
icon: "M3 3h18v18H3V3zm16 16V5H5v14h14z", // Grid icon
|
icon: "M3 3h18v18H3V3zm16 16V5H5v14h14z",
|
||||||
path: "/dashboards",
|
path: "/dashboards",
|
||||||
subItems: [
|
subItems: [
|
||||||
{ label: $t.nav?.overview || "Overview", path: "/dashboards" },
|
{ label: $t.nav?.overview || "Overview", path: "/dashboards" },
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
{
|
{
|
||||||
id: "datasets",
|
id: "datasets",
|
||||||
label: $t.nav?.datasets || "DATASETS",
|
label: $t.nav?.datasets || "DATASETS",
|
||||||
icon: "M3 3h18v18H3V3zm2 2v14h14V5H5zm2 2h10v2H7V7zm0 4h10v2H7v-2zm0 4h6v2H7v-2z", // List icon
|
icon: "M3 3h18v18H3V3zm2 2v14h14V5H5zm2 2h10v2H7V7zm0 4h10v2H7v-2zm0 4h6v2H7v-2z",
|
||||||
path: "/datasets",
|
path: "/datasets",
|
||||||
subItems: [
|
subItems: [
|
||||||
{ label: $t.nav?.all_datasets || "All Datasets", path: "/datasets" },
|
{ label: $t.nav?.all_datasets || "All Datasets", path: "/datasets" },
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
{
|
{
|
||||||
id: "storage",
|
id: "storage",
|
||||||
label: $t.nav?.storage || "STORAGE",
|
label: $t.nav?.storage || "STORAGE",
|
||||||
icon: "M4 4h16v16H4V4zm2 2v12h12V6H6zm2 2h8v2H8V8zm0 4h8v2H8v-2zm0 4h5v2H8v-2z", // Folder icon
|
icon: "M4 4h16v16H4V4zm2 2v12h12V6H6zm2 2h8v2H8V8zm0 4h8v2H8v-2zm0 4h5v2H8v-2z",
|
||||||
path: "/storage",
|
path: "/storage",
|
||||||
subItems: [
|
subItems: [
|
||||||
{ label: $t.nav?.backups || "Backups", path: "/storage/backups" },
|
{ label: $t.nav?.backups || "Backups", path: "/storage/backups" },
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
{
|
{
|
||||||
id: "admin",
|
id: "admin",
|
||||||
label: $t.nav?.admin || "ADMIN",
|
label: $t.nav?.admin || "ADMIN",
|
||||||
icon: "M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 3c1.66 0 3 1.34 3 3s-1.34 3-3 3-3-1.34-3-3 1.34-3 3-3zm0 14.2c-2.5 0-4.71-1.28-6-3.22.03-1.99 4-3.08 6-3.08 1.99 0 5.97 1.09 6 3.08-1.29 1.94-3.5 3.22-6 3.22z", // User icon
|
icon: "M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 3c1.66 0 3 1.34 3 3s-1.34 3-3 3-3-1.34-3-3 1.34-3 3-3zm0 14.2c-2.5 0-4.71-1.28-6-3.22.03-1.99 4-3.08 6-3.08 1.99 0 5.97 1.09 6 3.08-1.29 1.94-3.5 3.22-6 3.22z",
|
||||||
path: "/admin",
|
path: "/admin",
|
||||||
subItems: [
|
subItems: [
|
||||||
{ label: $t.nav?.admin_users || "Users", path: "/admin/users" },
|
{ label: $t.nav?.admin_users || "Users", path: "/admin/users" },
|
||||||
@@ -75,7 +75,7 @@
|
|||||||
let activeCategory = "dashboards";
|
let activeCategory = "dashboards";
|
||||||
let activeItem = "/dashboards";
|
let activeItem = "/dashboards";
|
||||||
let isMobileOpen = false;
|
let isMobileOpen = false;
|
||||||
let expandedCategories = new Set(["dashboards"]); // Track expanded categories
|
let expandedCategories = new Set(["dashboards"]);
|
||||||
|
|
||||||
// Subscribe to sidebar store
|
// Subscribe to sidebar store
|
||||||
$: if ($sidebarStore) {
|
$: if ($sidebarStore) {
|
||||||
@@ -90,7 +90,7 @@
|
|||||||
{
|
{
|
||||||
id: "dashboards",
|
id: "dashboards",
|
||||||
label: $t.nav?.dashboards || "DASHBOARDS",
|
label: $t.nav?.dashboards || "DASHBOARDS",
|
||||||
icon: "M3 3h18v18H3V3zm16 16V5H5v14h14z", // Grid icon
|
icon: "M3 3h18v18H3V3zm16 16V5H5v14h14z",
|
||||||
path: "/dashboards",
|
path: "/dashboards",
|
||||||
subItems: [
|
subItems: [
|
||||||
{ label: $t.nav?.overview || "Overview", path: "/dashboards" },
|
{ label: $t.nav?.overview || "Overview", path: "/dashboards" },
|
||||||
@@ -99,7 +99,7 @@
|
|||||||
{
|
{
|
||||||
id: "datasets",
|
id: "datasets",
|
||||||
label: $t.nav?.datasets || "DATASETS",
|
label: $t.nav?.datasets || "DATASETS",
|
||||||
icon: "M3 3h18v18H3V3zm2 2v14h14V5H5zm2 2h10v2H7V7zm0 4h10v2H7v-2zm0 4h6v2H7v-2z", // List icon
|
icon: "M3 3h18v18H3V3zm2 2v14h14V5H5zm2 2h10v2H7V7zm0 4h10v2H7v-2zm0 4h6v2H7v-2z",
|
||||||
path: "/datasets",
|
path: "/datasets",
|
||||||
subItems: [
|
subItems: [
|
||||||
{ label: $t.nav?.all_datasets || "All Datasets", path: "/datasets" },
|
{ label: $t.nav?.all_datasets || "All Datasets", path: "/datasets" },
|
||||||
@@ -108,7 +108,7 @@
|
|||||||
{
|
{
|
||||||
id: "storage",
|
id: "storage",
|
||||||
label: $t.nav?.storage || "STORAGE",
|
label: $t.nav?.storage || "STORAGE",
|
||||||
icon: "M4 4h16v16H4V4zm2 2v12h12V6H6zm2 2h8v2H8V8zm0 4h8v2H8v-2zm0 4h5v2H8v-2z", // Folder icon
|
icon: "M4 4h16v16H4V4zm2 2v12h12V6H6zm2 2h8v2H8V8zm0 4h8v2H8v-2zm0 4h5v2H8v-2z",
|
||||||
path: "/storage",
|
path: "/storage",
|
||||||
subItems: [
|
subItems: [
|
||||||
{ label: $t.nav?.backups || "Backups", path: "/storage/backups" },
|
{ label: $t.nav?.backups || "Backups", path: "/storage/backups" },
|
||||||
@@ -121,7 +121,7 @@
|
|||||||
{
|
{
|
||||||
id: "admin",
|
id: "admin",
|
||||||
label: $t.nav?.admin || "ADMIN",
|
label: $t.nav?.admin || "ADMIN",
|
||||||
icon: "M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 3c1.66 0 3 1.34 3 3s-1.34 3-3 3-3-1.34-3-3 1.34-3 3-3zm0 14.2c-2.5 0-4.71-1.28-6-3.22.03-1.99 4-3.08 6-3.08 1.99 0 5.97 1.09 6 3.08-1.29 1.94-3.5 3.22-6 3.22z", // User icon
|
icon: "M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 3c1.66 0 3 1.34 3 3s-1.34 3-3 3-3-1.34-3-3 1.34-3 3-3zm0 14.2c-2.5 0-4.71-1.28-6-3.22.03-1.99 4-3.08 6-3.08 1.99 0 5.97 1.09 6 3.08-1.29 1.94-3.5 3.22-6 3.22z",
|
||||||
path: "/admin",
|
path: "/admin",
|
||||||
subItems: [
|
subItems: [
|
||||||
{ label: $t.nav?.admin_users || "Users", path: "/admin/users" },
|
{ label: $t.nav?.admin_users || "Users", path: "/admin/users" },
|
||||||
@@ -133,7 +133,6 @@
|
|||||||
|
|
||||||
// Update active item when page changes
|
// Update active item when page changes
|
||||||
$: if ($page && $page.url.pathname !== activeItem) {
|
$: if ($page && $page.url.pathname !== activeItem) {
|
||||||
// Find matching category
|
|
||||||
const matched = categories.find((cat) =>
|
const matched = categories.find((cat) =>
|
||||||
$page.url.pathname.startsWith(cat.path),
|
$page.url.pathname.startsWith(cat.path),
|
||||||
);
|
);
|
||||||
@@ -143,7 +142,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle click on sidebar item
|
|
||||||
function handleItemClick(category) {
|
function handleItemClick(category) {
|
||||||
console.log(`[Sidebar][Action] Clicked category ${category.id}`);
|
console.log(`[Sidebar][Action] Clicked category ${category.id}`);
|
||||||
setActiveItem(category.id, category.path);
|
setActiveItem(category.id, category.path);
|
||||||
@@ -153,7 +151,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle click on category header to toggle expansion
|
|
||||||
function handleCategoryToggle(categoryId, event) {
|
function handleCategoryToggle(categoryId, event) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
@@ -173,28 +170,24 @@
|
|||||||
} else {
|
} else {
|
||||||
expandedCategories.add(categoryId);
|
expandedCategories.add(categoryId);
|
||||||
}
|
}
|
||||||
expandedCategories = expandedCategories; // Trigger reactivity
|
expandedCategories = expandedCategories;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle click on sub-item
|
|
||||||
function handleSubItemClick(categoryId, path) {
|
function handleSubItemClick(categoryId, path) {
|
||||||
console.log(`[Sidebar][Action] Clicked sub-item ${path}`);
|
console.log(`[Sidebar][Action] Clicked sub-item ${path}`);
|
||||||
setActiveItem(categoryId, path);
|
setActiveItem(categoryId, path);
|
||||||
closeMobile();
|
closeMobile();
|
||||||
// Force navigation if it's a link
|
|
||||||
if (browser) {
|
if (browser) {
|
||||||
window.location.href = path;
|
window.location.href = path;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle toggle button click
|
|
||||||
function handleToggleClick(event) {
|
function handleToggleClick(event) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
console.log("[Sidebar][Action] Toggle sidebar");
|
console.log("[Sidebar][Action] Toggle sidebar");
|
||||||
toggleSidebar();
|
toggleSidebar();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle mobile overlay click
|
|
||||||
function handleOverlayClick() {
|
function handleOverlayClick() {
|
||||||
console.log("[Sidebar][Action] Close mobile overlay");
|
console.log("[Sidebar][Action] Close mobile overlay");
|
||||||
closeMobile();
|
closeMobile();
|
||||||
@@ -209,7 +202,7 @@
|
|||||||
<!-- Mobile overlay (only on mobile) -->
|
<!-- Mobile overlay (only on mobile) -->
|
||||||
{#if isMobileOpen}
|
{#if isMobileOpen}
|
||||||
<div
|
<div
|
||||||
class="mobile-overlay"
|
class="fixed inset-0 bg-black/50 z-20 md:hidden"
|
||||||
on:click={handleOverlayClick}
|
on:click={handleOverlayClick}
|
||||||
on:keydown={(e) => e.key === "Escape" && handleOverlayClick()}
|
on:keydown={(e) => e.key === "Escape" && handleOverlayClick()}
|
||||||
role="presentation"
|
role="presentation"
|
||||||
@@ -218,12 +211,18 @@
|
|||||||
|
|
||||||
<!-- Sidebar -->
|
<!-- Sidebar -->
|
||||||
<div
|
<div
|
||||||
class="sidebar {isExpanded ? 'expanded' : 'collapsed'} {isMobileOpen
|
class="bg-white border-r border-gray-200 flex flex-col h-screen fixed left-0 top-0 z-30 transition-[width] duration-200 ease-in-out
|
||||||
? 'mobile'
|
{isExpanded ? 'w-sidebar' : 'w-sidebar-collapsed'}
|
||||||
: 'mobile-hidden'}"
|
{isMobileOpen
|
||||||
|
? 'translate-x-0 w-sidebar'
|
||||||
|
: '-translate-x-full md:translate-x-0'}"
|
||||||
|
>
|
||||||
|
<!-- Header -->
|
||||||
|
<div
|
||||||
|
class="flex items-center p-4 border-b border-gray-200 {isExpanded
|
||||||
|
? 'justify-between'
|
||||||
|
: 'justify-center'}"
|
||||||
>
|
>
|
||||||
<!-- Header (simplified, toggle moved to footer) -->
|
|
||||||
<div class="sidebar-header {isExpanded ? '' : 'collapsed'}">
|
|
||||||
{#if isExpanded}
|
{#if isExpanded}
|
||||||
<span class="font-semibold text-gray-800">Menu</span>
|
<span class="font-semibold text-gray-800">Menu</span>
|
||||||
{:else}
|
{:else}
|
||||||
@@ -232,13 +231,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Navigation items -->
|
<!-- Navigation items -->
|
||||||
<nav class="nav-section">
|
<nav class="flex-1 overflow-y-auto py-2">
|
||||||
{#each categories as category}
|
{#each categories as category}
|
||||||
<div class="category">
|
<div>
|
||||||
<!-- Category Header -->
|
<!-- Category Header -->
|
||||||
<div
|
<div
|
||||||
class="category-header {activeCategory === category.id
|
class="flex items-center justify-between px-4 py-3 cursor-pointer transition-colors hover:bg-gray-100
|
||||||
? 'active'
|
{activeCategory === category.id
|
||||||
|
? 'bg-primary-light text-primary md:border-r-2 md:border-primary'
|
||||||
: ''}"
|
: ''}"
|
||||||
on:click={(e) => handleCategoryToggle(category.id, e)}
|
on:click={(e) => handleCategoryToggle(category.id, e)}
|
||||||
on:keydown={(e) =>
|
on:keydown={(e) =>
|
||||||
@@ -251,7 +251,7 @@
|
|||||||
>
|
>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<svg
|
<svg
|
||||||
class="nav-icon"
|
class="w-5 h-5 shrink-0"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
@@ -261,13 +261,17 @@
|
|||||||
<path d={category.icon} />
|
<path d={category.icon} />
|
||||||
</svg>
|
</svg>
|
||||||
{#if isExpanded}
|
{#if isExpanded}
|
||||||
<span class="nav-label">{category.label}</span>
|
<span class="ml-3 text-sm font-medium truncate"
|
||||||
|
>{category.label}</span
|
||||||
|
>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if isExpanded}
|
{#if isExpanded}
|
||||||
<svg
|
<svg
|
||||||
class="category-toggle {expandedCategories.has(category.id)
|
class="text-gray-400 transition-transform duration-200 {expandedCategories.has(
|
||||||
? 'expanded'
|
category.id,
|
||||||
|
)
|
||||||
|
? 'rotate-180'
|
||||||
: ''}"
|
: ''}"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
width="16"
|
width="16"
|
||||||
@@ -284,10 +288,13 @@
|
|||||||
|
|
||||||
<!-- Sub Items (only when expanded) -->
|
<!-- Sub Items (only when expanded) -->
|
||||||
{#if isExpanded && expandedCategories.has(category.id)}
|
{#if isExpanded && expandedCategories.has(category.id)}
|
||||||
<div class="sub-items">
|
<div class="bg-gray-50 overflow-hidden transition-all duration-200">
|
||||||
{#each category.subItems as subItem}
|
{#each category.subItems as subItem}
|
||||||
<div
|
<div
|
||||||
class="sub-item {activeItem === subItem.path ? 'active' : ''}"
|
class="flex items-center px-4 py-2 pl-12 cursor-pointer transition-colors text-sm text-gray-600 hover:bg-gray-100 hover:text-gray-900
|
||||||
|
{activeItem === subItem.path
|
||||||
|
? 'bg-primary-light text-primary'
|
||||||
|
: ''}"
|
||||||
on:click={() => handleSubItemClick(category.id, subItem.path)}
|
on:click={() => handleSubItemClick(category.id, subItem.path)}
|
||||||
on:keydown={(e) =>
|
on:keydown={(e) =>
|
||||||
(e.key === "Enter" || e.key === " ") &&
|
(e.key === "Enter" || e.key === " ") &&
|
||||||
@@ -306,8 +313,11 @@
|
|||||||
|
|
||||||
<!-- Footer with Collapse button -->
|
<!-- Footer with Collapse button -->
|
||||||
{#if isExpanded}
|
{#if isExpanded}
|
||||||
<div class="sidebar-footer">
|
<div class="border-t border-gray-200 p-4">
|
||||||
<button class="collapse-btn" on:click={handleToggleClick}>
|
<button
|
||||||
|
class="flex items-center justify-center w-full px-4 py-2 text-sm text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
|
||||||
|
on:click={handleToggleClick}
|
||||||
|
>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
width="16"
|
width="16"
|
||||||
@@ -324,8 +334,12 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="sidebar-footer">
|
<div class="border-t border-gray-200 p-4">
|
||||||
<button class="collapse-btn" on:click={handleToggleClick} aria-label="Expand sidebar">
|
<button
|
||||||
|
class="flex items-center justify-center w-full px-4 py-2 text-sm text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
|
||||||
|
on:click={handleToggleClick}
|
||||||
|
aria-label="Expand sidebar"
|
||||||
|
>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
width="16"
|
width="16"
|
||||||
@@ -337,101 +351,10 @@
|
|||||||
>
|
>
|
||||||
<path d="M9 18l6-6-6-6" />
|
<path d="M9 18l6-6-6-6" />
|
||||||
</svg>
|
</svg>
|
||||||
<span class="collapse-btn-text">Expand</span>
|
<span class="ml-2">Expand</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- [/DEF:Sidebar:Component] -->
|
<!-- [/DEF:Sidebar:Component] -->
|
||||||
|
|
||||||
<style>
|
|
||||||
.sidebar {
|
|
||||||
@apply bg-white border-r border-gray-200 flex flex-col h-screen fixed left-0 top-0 z-30;
|
|
||||||
transition: width 0.2s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar.expanded {
|
|
||||||
width: 240px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar.collapsed {
|
|
||||||
width: 64px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar.mobile {
|
|
||||||
@apply translate-x-0;
|
|
||||||
width: 240px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar.mobile-hidden {
|
|
||||||
@apply -translate-x-full md:translate-x-0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-header {
|
|
||||||
@apply flex items-center justify-between p-4 border-b border-gray-200;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-header.collapsed {
|
|
||||||
@apply justify-center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-icon {
|
|
||||||
@apply w-5 h-5 flex-shrink-0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-label {
|
|
||||||
@apply ml-3 text-sm font-medium truncate;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-header {
|
|
||||||
@apply flex items-center justify-between px-4 py-3 cursor-pointer transition-colors hover:bg-gray-100;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-header.active {
|
|
||||||
@apply bg-blue-50 text-blue-600 md:border-r-2 md:border-blue-600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-toggle {
|
|
||||||
@apply text-gray-400 transition-transform duration-200;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-toggle.expanded {
|
|
||||||
@apply rotate-180;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sub-items {
|
|
||||||
@apply bg-gray-50 overflow-hidden transition-all duration-200;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sub-item {
|
|
||||||
@apply flex items-center px-4 py-2 pl-12 cursor-pointer transition-colors text-sm text-gray-600 hover:bg-gray-100 hover:text-gray-900;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sub-item.active {
|
|
||||||
@apply bg-blue-50 text-blue-600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-footer {
|
|
||||||
@apply border-t border-gray-200 p-4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.collapse-btn {
|
|
||||||
@apply flex items-center justify-center w-full px-4 py-2 text-sm text-gray-600 hover:bg-gray-100 rounded-lg transition-colors;
|
|
||||||
}
|
|
||||||
|
|
||||||
.collapse-btn-text {
|
|
||||||
@apply ml-2;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Mobile overlay */
|
|
||||||
.mobile-overlay {
|
|
||||||
@apply fixed inset-0 bg-black bg-opacity-50 z-20;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
.mobile-overlay {
|
|
||||||
@apply hidden;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -31,54 +31,38 @@
|
|||||||
let showUserMenu = false;
|
let showUserMenu = false;
|
||||||
let isSearchFocused = false;
|
let isSearchFocused = false;
|
||||||
|
|
||||||
// Subscribe to sidebar store for responsive layout
|
|
||||||
$: isExpanded = $sidebarStore?.isExpanded ?? true;
|
$: isExpanded = $sidebarStore?.isExpanded ?? true;
|
||||||
|
|
||||||
// Subscribe to activity store
|
|
||||||
$: activeCount = $activityStore?.activeCount || 0;
|
$: activeCount = $activityStore?.activeCount || 0;
|
||||||
$: recentTasks = $activityStore?.recentTasks || [];
|
$: recentTasks = $activityStore?.recentTasks || [];
|
||||||
|
|
||||||
// Get user from auth store
|
|
||||||
$: user = $auth?.user || null;
|
$: user = $auth?.user || null;
|
||||||
|
|
||||||
// Toggle user menu
|
|
||||||
function toggleUserMenu(event) {
|
function toggleUserMenu(event) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
showUserMenu = !showUserMenu;
|
showUserMenu = !showUserMenu;
|
||||||
console.log(`[TopNavbar][Action] Toggle user menu: ${showUserMenu}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close user menu
|
|
||||||
function closeUserMenu() {
|
function closeUserMenu() {
|
||||||
showUserMenu = false;
|
showUserMenu = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle logout
|
|
||||||
function handleLogout() {
|
function handleLogout() {
|
||||||
console.log("[TopNavbar][Action] Logout");
|
|
||||||
auth.logout();
|
auth.logout();
|
||||||
closeUserMenu();
|
closeUserMenu();
|
||||||
// Navigate to login
|
|
||||||
window.location.href = "/login";
|
window.location.href = "/login";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle activity indicator click - open Task Drawer with most recent task
|
|
||||||
function handleActivityClick() {
|
function handleActivityClick() {
|
||||||
console.log("[TopNavbar][Action] Activity indicator clicked");
|
|
||||||
// Open drawer with the most recent running task, or list mode
|
|
||||||
const runningTask = recentTasks.find((t) => t.status === "RUNNING");
|
const runningTask = recentTasks.find((t) => t.status === "RUNNING");
|
||||||
if (runningTask) {
|
if (runningTask) {
|
||||||
openDrawerForTask(runningTask.taskId);
|
openDrawerForTask(runningTask.taskId);
|
||||||
} else if (recentTasks.length > 0) {
|
} else if (recentTasks.length > 0) {
|
||||||
openDrawerForTask(recentTasks[recentTasks.length - 1].taskId);
|
openDrawerForTask(recentTasks[recentTasks.length - 1].taskId);
|
||||||
} else {
|
} else {
|
||||||
// No tracked tasks — open in list mode to show recent tasks from API
|
|
||||||
openDrawer();
|
openDrawer();
|
||||||
}
|
}
|
||||||
dispatch("activityClick");
|
dispatch("activityClick");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle search focus
|
|
||||||
function handleSearchFocus() {
|
function handleSearchFocus() {
|
||||||
isSearchFocused = true;
|
isSearchFocused = true;
|
||||||
}
|
}
|
||||||
@@ -87,34 +71,31 @@
|
|||||||
isSearchFocused = false;
|
isSearchFocused = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close dropdowns when clicking outside
|
|
||||||
function handleDocumentClick(event) {
|
function handleDocumentClick(event) {
|
||||||
if (!event.target.closest(".user-menu-container")) {
|
if (!event.target.closest(".user-menu-container")) {
|
||||||
closeUserMenu();
|
closeUserMenu();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Listen for document clicks
|
|
||||||
if (typeof document !== "undefined") {
|
if (typeof document !== "undefined") {
|
||||||
document.addEventListener("click", handleDocumentClick);
|
document.addEventListener("click", handleDocumentClick);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle hamburger menu click for mobile
|
|
||||||
function handleHamburgerClick(event) {
|
function handleHamburgerClick(event) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
console.log("[TopNavbar][Action] Toggle mobile sidebar");
|
|
||||||
toggleMobileSidebar();
|
toggleMobileSidebar();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<nav
|
<nav
|
||||||
class="navbar {isExpanded ? 'with-sidebar' : 'with-collapsed-sidebar'} mobile"
|
class="bg-white border-b border-gray-200 fixed top-0 right-0 left-0 h-16 flex items-center justify-between px-4 z-40
|
||||||
|
{isExpanded ? 'md:left-[240px]' : 'md:left-16'}"
|
||||||
>
|
>
|
||||||
<!-- Left section: Hamburger (mobile) + Logo -->
|
<!-- Left section: Hamburger (mobile) + Logo -->
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<!-- Hamburger Menu (mobile only) -->
|
<!-- Hamburger Menu (mobile only) -->
|
||||||
<button
|
<button
|
||||||
class="hamburger-btn"
|
class="p-2 rounded-lg hover:bg-gray-100 text-gray-600 md:hidden"
|
||||||
on:click={handleHamburgerClick}
|
on:click={handleHamburgerClick}
|
||||||
aria-label="Toggle menu"
|
aria-label="Toggle menu"
|
||||||
>
|
>
|
||||||
@@ -134,9 +115,12 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Logo/Brand -->
|
<!-- Logo/Brand -->
|
||||||
<a href="/" class="logo-link">
|
<a
|
||||||
|
href="/"
|
||||||
|
class="flex items-center text-xl font-bold text-gray-800 hover:text-primary transition-colors"
|
||||||
|
>
|
||||||
<svg
|
<svg
|
||||||
class="logo-icon"
|
class="w-8 h-8 mr-2 text-primary"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
@@ -148,10 +132,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Search placeholder (non-functional for now) -->
|
<!-- Search placeholder (non-functional for now) -->
|
||||||
<div class="search-container">
|
<div class="flex-1 max-w-xl mx-4 hidden md:block">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="search-input {isSearchFocused ? 'focused' : ''}"
|
class="w-full px-4 py-2 bg-gray-100 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-ring transition-all
|
||||||
|
{isSearchFocused ? 'bg-white border border-primary-ring' : ''}"
|
||||||
placeholder={$t.common.search || "Search..."}
|
placeholder={$t.common.search || "Search..."}
|
||||||
on:focus={handleSearchFocus}
|
on:focus={handleSearchFocus}
|
||||||
on:blur={handleSearchBlur}
|
on:blur={handleSearchBlur}
|
||||||
@@ -159,10 +144,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Nav Actions -->
|
<!-- Nav Actions -->
|
||||||
<div class="nav-actions">
|
<div class="flex items-center space-x-4">
|
||||||
<!-- Activity Indicator -->
|
<!-- Activity Indicator -->
|
||||||
<div
|
<div
|
||||||
class="activity-indicator"
|
class="relative cursor-pointer p-2 rounded-lg hover:bg-gray-100 transition-colors"
|
||||||
on:click={handleActivityClick}
|
on:click={handleActivityClick}
|
||||||
on:keydown={(e) =>
|
on:keydown={(e) =>
|
||||||
(e.key === "Enter" || e.key === " ") && handleActivityClick()}
|
(e.key === "Enter" || e.key === " ") && handleActivityClick()}
|
||||||
@@ -171,7 +156,7 @@
|
|||||||
aria-label="Activity"
|
aria-label="Activity"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
class="activity-icon"
|
class="w-6 h-6 text-gray-600"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
@@ -183,14 +168,17 @@
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
{#if activeCount > 0}
|
{#if activeCount > 0}
|
||||||
<span class="activity-badge">{activeCount}</span>
|
<span
|
||||||
|
class="absolute -top-1 -right-1 bg-destructive text-white text-xs font-bold rounded-full w-5 h-5 flex items-center justify-center"
|
||||||
|
>{activeCount}</span
|
||||||
|
>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- User Menu -->
|
<!-- User Menu -->
|
||||||
<div class="user-menu-container">
|
<div class="user-menu-container relative">
|
||||||
<div
|
<div
|
||||||
class="user-avatar"
|
class="w-8 h-8 rounded-full bg-primary text-white flex items-center justify-center cursor-pointer hover:bg-primary-hover transition-colors"
|
||||||
on:click={toggleUserMenu}
|
on:click={toggleUserMenu}
|
||||||
on:keydown={(e) =>
|
on:keydown={(e) =>
|
||||||
(e.key === "Enter" || e.key === " ") && toggleUserMenu(e)}
|
(e.key === "Enter" || e.key === " ") && toggleUserMenu(e)}
|
||||||
@@ -208,13 +196,17 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- User Dropdown -->
|
<!-- User Dropdown -->
|
||||||
<div class="user-dropdown {showUserMenu ? '' : 'hidden'}">
|
<div
|
||||||
<div class="dropdown-item">
|
class="absolute right-0 mt-2 w-48 bg-white rounded-lg shadow-lg border border-gray-200 py-1 z-50 {showUserMenu
|
||||||
|
? ''
|
||||||
|
: 'hidden'}"
|
||||||
|
>
|
||||||
|
<div class="px-4 py-2 text-sm text-gray-700">
|
||||||
<strong>{user?.username || "User"}</strong>
|
<strong>{user?.username || "User"}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="dropdown-divider"></div>
|
<div class="border-t border-gray-200 my-1"></div>
|
||||||
<div
|
<div
|
||||||
class="dropdown-item"
|
class="px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 cursor-pointer"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
window.location.href = "/settings";
|
window.location.href = "/settings";
|
||||||
}}
|
}}
|
||||||
@@ -227,7 +219,7 @@
|
|||||||
{$t.nav?.settings || "Settings"}
|
{$t.nav?.settings || "Settings"}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="dropdown-item danger"
|
class="px-4 py-2 text-sm text-destructive hover:bg-destructive-light cursor-pointer"
|
||||||
on:click={handleLogout}
|
on:click={handleLogout}
|
||||||
on:keydown={(e) =>
|
on:keydown={(e) =>
|
||||||
(e.key === "Enter" || e.key === " ") && handleLogout()}
|
(e.key === "Enter" || e.key === " ") && handleLogout()}
|
||||||
@@ -242,96 +234,3 @@
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<!-- [/DEF:TopNavbar:Component] -->
|
<!-- [/DEF:TopNavbar:Component] -->
|
||||||
|
|
||||||
<style>
|
|
||||||
.navbar {
|
|
||||||
@apply bg-white border-b border-gray-200 fixed top-0 right-0 left-0 h-16 flex items-center justify-between px-4 z-40;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar.with-sidebar {
|
|
||||||
@apply md:left-64;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar.with-collapsed-sidebar {
|
|
||||||
@apply md:left-16;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar.mobile {
|
|
||||||
@apply left-0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo-link {
|
|
||||||
@apply flex items-center text-xl font-bold text-gray-800 hover:text-blue-600 transition-colors;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo-icon {
|
|
||||||
@apply w-8 h-8 mr-2 text-blue-600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-container {
|
|
||||||
@apply flex-1 max-w-xl mx-4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-input {
|
|
||||||
@apply w-full px-4 py-2 bg-gray-100 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-input.focused {
|
|
||||||
@apply bg-white border border-blue-500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-actions {
|
|
||||||
@apply flex items-center space-x-4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hamburger-btn {
|
|
||||||
@apply p-2 rounded-lg hover:bg-gray-100 text-gray-600 md:hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.activity-indicator {
|
|
||||||
@apply relative cursor-pointer p-2 rounded-lg hover:bg-gray-100 transition-colors;
|
|
||||||
}
|
|
||||||
|
|
||||||
.activity-badge {
|
|
||||||
@apply absolute -top-1 -right-1 bg-red-500 text-white text-xs font-bold rounded-full w-5 h-5 flex items-center justify-center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.activity-icon {
|
|
||||||
@apply w-6 h-6 text-gray-600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-menu-container {
|
|
||||||
@apply relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-avatar {
|
|
||||||
@apply w-8 h-8 rounded-full bg-blue-600 text-white flex items-center justify-center cursor-pointer hover:bg-blue-700 transition-colors;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-dropdown {
|
|
||||||
@apply absolute right-0 mt-2 w-48 bg-white rounded-lg shadow-lg border border-gray-200 py-1 z-50;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-dropdown.hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-item {
|
|
||||||
@apply px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 cursor-pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-item.danger {
|
|
||||||
@apply text-red-600 hover:bg-red-50;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-divider {
|
|
||||||
@apply border-t border-gray-200 my-1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Mobile responsive */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.search-container {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -9,45 +9,51 @@
|
|||||||
@INVARIANT: Supports accessible labels and keyboard navigation.
|
@INVARIANT: Supports accessible labels and keyboard navigation.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<script lang="ts">
|
<script>
|
||||||
// [SECTION: IMPORTS]
|
// [SECTION: IMPORTS]
|
||||||
|
import { cn } from '$lib/utils.js';
|
||||||
// [/SECTION: IMPORTS]
|
// [/SECTION: IMPORTS]
|
||||||
|
|
||||||
// [SECTION: PROPS]
|
// [SECTION: PROPS]
|
||||||
/**
|
/**
|
||||||
* @purpose Define component interface and default values.
|
* @purpose Define component interface and default values (Svelte 5 Runes).
|
||||||
*/
|
*/
|
||||||
export let variant: 'primary' | 'secondary' | 'danger' | 'ghost' = 'primary';
|
let {
|
||||||
export let size: 'sm' | 'md' | 'lg' = 'md';
|
variant = 'primary',
|
||||||
export let isLoading: boolean = false;
|
size = 'md',
|
||||||
export let disabled: boolean = false;
|
isLoading = false,
|
||||||
export let type: 'button' | 'submit' | 'reset' = 'button';
|
disabled = false,
|
||||||
let className: string = "";
|
type = 'button',
|
||||||
export { className as class };
|
class: className = '',
|
||||||
|
children,
|
||||||
|
onclick,
|
||||||
|
...rest
|
||||||
|
} = $props();
|
||||||
// [/SECTION: PROPS]
|
// [/SECTION: PROPS]
|
||||||
|
|
||||||
const baseStyles = "inline-flex items-center justify-center font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 rounded-md";
|
const baseStyles = 'inline-flex items-center justify-center font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 rounded-md';
|
||||||
|
|
||||||
const variants = {
|
const variants = {
|
||||||
primary: "bg-blue-600 text-white hover:bg-blue-700 focus-visible:ring-blue-500",
|
primary: 'bg-primary text-white hover:bg-primary-hover focus-visible:ring-primary-ring',
|
||||||
secondary: "bg-gray-100 text-gray-900 hover:bg-gray-200 focus-visible:ring-gray-500",
|
secondary: 'bg-secondary text-secondary-text hover:bg-secondary-hover focus-visible:ring-secondary-ring',
|
||||||
danger: "bg-red-600 text-white hover:bg-red-700 focus-visible:ring-red-500",
|
danger: 'bg-destructive text-white hover:bg-destructive-hover focus-visible:ring-destructive-ring',
|
||||||
ghost: "bg-transparent hover:bg-gray-100 text-gray-700 focus-visible:ring-gray-500"
|
ghost: 'bg-transparent hover:bg-ghost-hover text-ghost-text focus-visible:ring-ghost-ring',
|
||||||
};
|
};
|
||||||
|
|
||||||
const sizes = {
|
const sizes = {
|
||||||
sm: "h-8 px-3 text-xs",
|
sm: 'h-8 px-3 text-xs',
|
||||||
md: "h-10 px-4 py-2 text-sm",
|
md: 'h-10 px-4 py-2 text-sm',
|
||||||
lg: "h-12 px-6 text-base"
|
lg: 'h-12 px-6 text-base',
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- [SECTION: TEMPLATE] -->
|
<!-- [SECTION: TEMPLATE] -->
|
||||||
<button
|
<button
|
||||||
{type}
|
{type}
|
||||||
class="{baseStyles} {variants[variant]} {sizes[size]} {className}"
|
class={cn(baseStyles, variants[variant], sizes[size], className)}
|
||||||
disabled={disabled || isLoading}
|
disabled={disabled || isLoading}
|
||||||
on:click
|
{onclick}
|
||||||
|
{...rest}
|
||||||
>
|
>
|
||||||
{#if isLoading}
|
{#if isLoading}
|
||||||
<svg class="animate-spin -ml-1 mr-2 h-4 w-4 text-current" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
<svg class="animate-spin -ml-1 mr-2 h-4 w-4 text-current" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||||
@@ -55,7 +61,7 @@
|
|||||||
<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>
|
<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>
|
</svg>
|
||||||
{/if}
|
{/if}
|
||||||
<slot />
|
{@render children?.()}
|
||||||
</button>
|
</button>
|
||||||
<!-- [/SECTION: TEMPLATE] -->
|
<!-- [/SECTION: TEMPLATE] -->
|
||||||
|
|
||||||
|
|||||||
@@ -6,29 +6,44 @@
|
|||||||
@LAYER: Atom
|
@LAYER: Atom
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<script lang="ts">
|
<script>
|
||||||
|
// [SECTION: IMPORTS]
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
// [/SECTION: IMPORTS]
|
||||||
|
|
||||||
// [SECTION: PROPS]
|
// [SECTION: PROPS]
|
||||||
export let title: string = "";
|
let {
|
||||||
export let padding: 'none' | 'sm' | 'md' | 'lg' = 'md';
|
title = "",
|
||||||
|
padding = "md",
|
||||||
|
class: className = "",
|
||||||
|
children,
|
||||||
|
...rest
|
||||||
|
} = $props();
|
||||||
// [/SECTION: PROPS]
|
// [/SECTION: PROPS]
|
||||||
|
|
||||||
const paddings = {
|
const paddings = {
|
||||||
none: "p-0",
|
none: "p-0",
|
||||||
sm: "p-3",
|
sm: "p-3",
|
||||||
md: "p-6",
|
md: "p-6",
|
||||||
lg: "p-8"
|
lg: "p-8",
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- [SECTION: TEMPLATE] -->
|
<!-- [SECTION: TEMPLATE] -->
|
||||||
<div class="rounded-lg border border-gray-200 bg-white text-gray-950 shadow-sm">
|
<div
|
||||||
|
class={cn(
|
||||||
|
"rounded-lg border border-gray-200 bg-white text-gray-950 shadow-sm",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
{#if title}
|
{#if title}
|
||||||
<div class="flex flex-col space-y-1.5 p-6 border-b border-gray-100">
|
<div class="flex flex-col space-y-1.5 p-6 border-b border-gray-100">
|
||||||
<h3 class="text-lg font-semibold leading-none tracking-tight">{title}</h3>
|
<h3 class="text-lg font-semibold leading-none tracking-tight">{title}</h3>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="{paddings[padding]}">
|
<div class={paddings[padding]}>
|
||||||
<slot />
|
{@render children?.()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- [/SECTION: TEMPLATE] -->
|
<!-- [/SECTION: TEMPLATE] -->
|
||||||
|
|||||||
@@ -8,14 +8,22 @@
|
|||||||
@INVARIANT: Consistent spacing and focus states.
|
@INVARIANT: Consistent spacing and focus states.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<script lang="ts">
|
<script>
|
||||||
|
// [SECTION: IMPORTS]
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
// [/SECTION: IMPORTS]
|
||||||
|
|
||||||
// [SECTION: PROPS]
|
// [SECTION: PROPS]
|
||||||
export let label: string = "";
|
let {
|
||||||
export let value: string = "";
|
label = "",
|
||||||
export let placeholder: string = "";
|
value = $bindable(""),
|
||||||
export let error: string = "";
|
placeholder = "",
|
||||||
export let disabled: boolean = false;
|
error = "",
|
||||||
export let type: 'text' | 'password' | 'email' | 'number' = 'text';
|
disabled = false,
|
||||||
|
type = "text",
|
||||||
|
class: className = "",
|
||||||
|
...rest
|
||||||
|
} = $props();
|
||||||
// [/SECTION: PROPS]
|
// [/SECTION: PROPS]
|
||||||
|
|
||||||
let id = "input-" + Math.random().toString(36).substr(2, 9);
|
let id = "input-" + Math.random().toString(36).substr(2, 9);
|
||||||
@@ -35,11 +43,16 @@
|
|||||||
{placeholder}
|
{placeholder}
|
||||||
{disabled}
|
{disabled}
|
||||||
bind:value
|
bind:value
|
||||||
class="flex h-10 w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm ring-offset-white file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-gray-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 {error ? 'border-red-500' : ''}"
|
class={cn(
|
||||||
|
"flex h-10 w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm ring-offset-white file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-gray-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
error ? "border-destructive" : "",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...rest}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{#if error}
|
{#if error}
|
||||||
<span class="text-xs text-red-500">{error}</span>
|
<span class="text-xs text-destructive">{error}</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<!-- [/SECTION: TEMPLATE] -->
|
<!-- [/SECTION: TEMPLATE] -->
|
||||||
|
|||||||
@@ -7,24 +7,21 @@
|
|||||||
@RELATION: BINDS_TO -> i18n.locale
|
@RELATION: BINDS_TO -> i18n.locale
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<script lang="ts">
|
<script>
|
||||||
// [SECTION: IMPORTS]
|
// [SECTION: IMPORTS]
|
||||||
import { locale } from '$lib/i18n';
|
import { locale } from "$lib/i18n";
|
||||||
import Select from './Select.svelte';
|
import Select from "./Select.svelte";
|
||||||
// [/SECTION: IMPORTS]
|
// [/SECTION: IMPORTS]
|
||||||
|
|
||||||
const options = [
|
const options = [
|
||||||
{ value: 'ru', label: 'Русский' },
|
{ value: "ru", label: "Русский" },
|
||||||
{ value: 'en', label: 'English' }
|
{ value: "en", label: "English" },
|
||||||
];
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- [SECTION: TEMPLATE] -->
|
<!-- [SECTION: TEMPLATE] -->
|
||||||
<div class="w-32">
|
<div class="w-32">
|
||||||
<Select
|
<Select bind:value={$locale} {options} />
|
||||||
bind:value={$locale}
|
|
||||||
{options}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<!-- [/SECTION: TEMPLATE] -->
|
<!-- [/SECTION: TEMPLATE] -->
|
||||||
|
|
||||||
|
|||||||
@@ -6,20 +6,33 @@
|
|||||||
@LAYER: Atom
|
@LAYER: Atom
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<script lang="ts">
|
<script>
|
||||||
|
// [SECTION: IMPORTS]
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
// [/SECTION: IMPORTS]
|
||||||
|
|
||||||
// [SECTION: PROPS]
|
// [SECTION: PROPS]
|
||||||
export let title: string = "";
|
let {
|
||||||
|
title = "",
|
||||||
|
class: className = "",
|
||||||
|
subtitle,
|
||||||
|
actions,
|
||||||
|
...rest
|
||||||
|
} = $props();
|
||||||
// [/SECTION: PROPS]
|
// [/SECTION: PROPS]
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- [SECTION: TEMPLATE] -->
|
<!-- [SECTION: TEMPLATE] -->
|
||||||
<header class="flex items-center justify-between mb-8">
|
<header
|
||||||
|
class={cn("flex items-center justify-between mb-8", className)}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
<h1 class="text-3xl font-bold tracking-tight text-gray-900">{title}</h1>
|
<h1 class="text-3xl font-bold tracking-tight text-gray-900">{title}</h1>
|
||||||
<slot name="subtitle" />
|
{@render subtitle?.()}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<slot name="actions" />
|
{@render actions?.()}
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<!-- [/SECTION: TEMPLATE] -->
|
<!-- [/SECTION: TEMPLATE] -->
|
||||||
|
|||||||
@@ -6,12 +6,20 @@
|
|||||||
@LAYER: Atom
|
@LAYER: Atom
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<script lang="ts">
|
<script>
|
||||||
|
// [SECTION: IMPORTS]
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
// [/SECTION: IMPORTS]
|
||||||
|
|
||||||
// [SECTION: PROPS]
|
// [SECTION: PROPS]
|
||||||
export let label: string = "";
|
let {
|
||||||
export let value: string | number = "";
|
label = "",
|
||||||
export let options: Array<{ value: string | number, label: string }> = [];
|
value = $bindable(""),
|
||||||
export let disabled: boolean = false;
|
options = [],
|
||||||
|
disabled = false,
|
||||||
|
class: className = "",
|
||||||
|
...rest
|
||||||
|
} = $props();
|
||||||
// [/SECTION: PROPS]
|
// [/SECTION: PROPS]
|
||||||
|
|
||||||
let id = "select-" + Math.random().toString(36).substr(2, 9);
|
let id = "select-" + Math.random().toString(36).substr(2, 9);
|
||||||
@@ -29,7 +37,11 @@
|
|||||||
{id}
|
{id}
|
||||||
{disabled}
|
{disabled}
|
||||||
bind:value
|
bind:value
|
||||||
class="flex h-10 w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm ring-offset-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
class={cn(
|
||||||
|
"flex h-10 w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm ring-offset-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...rest}
|
||||||
>
|
>
|
||||||
{#each options as option}
|
{#each options as option}
|
||||||
<option value={option.value}>{option.label}</option>
|
<option value={option.value}>{option.label}</option>
|
||||||
|
|||||||
8
frontend/src/lib/utils.js
Normal file
8
frontend/src/lib/utils.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* Merges class names into a single string.
|
||||||
|
* @param {...(string | undefined | null | false)} inputs
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function cn(...inputs) {
|
||||||
|
return inputs.filter(Boolean).join(" ");
|
||||||
|
}
|
||||||
@@ -10,25 +10,35 @@
|
|||||||
* @UX_FEEDBACK: Redirects to /dashboards
|
* @UX_FEEDBACK: Redirects to /dashboards
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from "svelte";
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from "$app/navigation";
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
// Redirect to Dashboard Hub as per UX requirements
|
// Redirect to Dashboard Hub as per UX requirements
|
||||||
goto('/dashboards', { replaceState: true });
|
goto("/dashboards", { replaceState: true });
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<div class="flex items-center justify-center min-h-screen">
|
||||||
.loading {
|
<svg
|
||||||
@apply flex items-center justify-center min-h-screen;
|
class="animate-spin h-8 w-8 text-blue-600"
|
||||||
}
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
</style>
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
<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
|
||||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
class="opacity-25"
|
||||||
<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>
|
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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -230,7 +230,5 @@
|
|||||||
<!-- [/SECTION: TEMPLATE] -->
|
<!-- [/SECTION: TEMPLATE] -->
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
|
|
||||||
<style>
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<!-- [/DEF:AdminRolesPage:Component] -->
|
<!-- [/DEF:AdminRolesPage:Component] -->
|
||||||
@@ -346,7 +346,5 @@
|
|||||||
<!-- [/SECTION: TEMPLATE] -->
|
<!-- [/SECTION: TEMPLATE] -->
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
|
|
||||||
<style>
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<!-- [/DEF:AdminSettingsPage:Component] -->
|
<!-- [/DEF:AdminSettingsPage:Component] -->
|
||||||
@@ -278,7 +278,5 @@
|
|||||||
<!-- [/SECTION: TEMPLATE] -->
|
<!-- [/SECTION: TEMPLATE] -->
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
|
|
||||||
<style>
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<!-- [/DEF:AdminUsersPage:Component] -->
|
<!-- [/DEF:AdminUsersPage:Component] -->
|
||||||
|
|||||||
@@ -14,16 +14,16 @@
|
|||||||
* @UX_RECOVERY: Refresh button reloads dataset details
|
* @UX_RECOVERY: Refresh button reloads dataset details
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from "svelte";
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from "$app/navigation";
|
||||||
import { page } from '$app/stores';
|
import { page } from "$app/stores";
|
||||||
import { t } from '$lib/i18n';
|
import { t } from "$lib/i18n";
|
||||||
import { api } from '$lib/api.js';
|
import { api } from "$lib/api.js";
|
||||||
import { openDrawerForTask } from '$lib/stores/taskDrawer.js';
|
import { openDrawerForTask } from "$lib/stores/taskDrawer.js";
|
||||||
|
|
||||||
// Get dataset ID from URL params
|
// Get dataset ID from URL params
|
||||||
$: datasetId = $page.params.id;
|
$: datasetId = $page.params.id;
|
||||||
$: envId = $page.url.searchParams.get('env_id') || '';
|
$: envId = $page.url.searchParams.get("env_id") || "";
|
||||||
|
|
||||||
// State
|
// State
|
||||||
let dataset = null;
|
let dataset = null;
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
// Load dataset details from API
|
// Load dataset details from API
|
||||||
async function loadDatasetDetail() {
|
async function loadDatasetDetail() {
|
||||||
if (!datasetId || !envId) {
|
if (!datasetId || !envId) {
|
||||||
error = 'Missing dataset ID or environment ID';
|
error = "Missing dataset ID or environment ID";
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -49,8 +49,8 @@
|
|||||||
const response = await api.getDatasetDetail(envId, datasetId);
|
const response = await api.getDatasetDetail(envId, datasetId);
|
||||||
dataset = response;
|
dataset = response;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error = err.message || 'Failed to load dataset details';
|
error = err.message || "Failed to load dataset details";
|
||||||
console.error('[DatasetDetail][Coherence:Failed]', err);
|
console.error("[DatasetDetail][Coherence:Failed]", err);
|
||||||
} finally {
|
} finally {
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
}
|
}
|
||||||
@@ -68,290 +68,241 @@
|
|||||||
|
|
||||||
// Get column type icon/color
|
// Get column type icon/color
|
||||||
function getColumnTypeClass(type) {
|
function getColumnTypeClass(type) {
|
||||||
if (!type) return 'text-gray-500';
|
if (!type) return "text-gray-500";
|
||||||
const lowerType = type.toLowerCase();
|
const lowerType = type.toLowerCase();
|
||||||
if (lowerType.includes('int') || lowerType.includes('float') || lowerType.includes('num')) {
|
if (
|
||||||
return 'text-blue-600 bg-blue-50';
|
lowerType.includes("int") ||
|
||||||
} else if (lowerType.includes('date') || lowerType.includes('time')) {
|
lowerType.includes("float") ||
|
||||||
return 'text-green-600 bg-green-50';
|
lowerType.includes("num")
|
||||||
} else if (lowerType.includes('str') || lowerType.includes('text') || lowerType.includes('char')) {
|
) {
|
||||||
return 'text-purple-600 bg-purple-50';
|
return "text-blue-600 bg-blue-50";
|
||||||
} else if (lowerType.includes('bool')) {
|
} else if (lowerType.includes("date") || lowerType.includes("time")) {
|
||||||
return 'text-orange-600 bg-orange-50';
|
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
|
// Get mapping progress percentage
|
||||||
function getMappingProgress(column) {
|
function getMappingProgress(column) {
|
||||||
// Placeholder: In real implementation, this would check if column has mapping
|
|
||||||
return column.description ? 100 : 0;
|
return column.description ? 100 : 0;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<div class="max-w-7xl mx-auto px-4 py-6">
|
||||||
.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">
|
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="header">
|
<div class="flex items-center justify-between mb-6">
|
||||||
<div>
|
<div>
|
||||||
<button class="back-btn" on:click={goBack}>
|
<button
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
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" />
|
<path d="M19 12H5M12 19l-7-7 7-7" />
|
||||||
</svg>
|
</svg>
|
||||||
{$t.common?.back || 'Back to Datasets'}
|
{$t.common?.back || "Back to Datasets"}
|
||||||
</button>
|
</button>
|
||||||
{#if dataset}
|
{#if dataset}
|
||||||
<h1 class="title mt-4">{dataset.table_name}</h1>
|
<h1 class="text-2xl font-bold text-gray-900 mt-4">
|
||||||
<p class="subtitle">{dataset.schema} • {dataset.database}</p>
|
{dataset.table_name}
|
||||||
|
</h1>
|
||||||
|
<p class="text-sm text-gray-500 mt-1">
|
||||||
|
{dataset.schema} • {dataset.database}
|
||||||
|
</p>
|
||||||
{:else if !isLoading}
|
{: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}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<button class="retry-btn" on:click={loadDatasetDetail}>
|
<button
|
||||||
{$t.common?.refresh || 'Refresh'}
|
class="px-4 py-2 bg-destructive text-white rounded hover:bg-destructive-hover transition-colors"
|
||||||
|
on:click={loadDatasetDetail}
|
||||||
|
>
|
||||||
|
{$t.common?.refresh || "Refresh"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Error Banner -->
|
<!-- Error Banner -->
|
||||||
{#if error}
|
{#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>
|
<span>{error}</span>
|
||||||
<button class="retry-btn" on:click={loadDatasetDetail}>
|
<button
|
||||||
{$t.common?.retry || 'Retry'}
|
class="px-4 py-2 bg-destructive text-white rounded hover:bg-destructive-hover transition-colors"
|
||||||
|
on:click={loadDatasetDetail}
|
||||||
|
>
|
||||||
|
{$t.common?.retry || "Retry"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Loading State -->
|
<!-- Loading State -->
|
||||||
{#if isLoading}
|
{#if isLoading}
|
||||||
<div class="detail-grid">
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
<div class="detail-card">
|
<div class="bg-white border border-gray-200 rounded-lg p-6">
|
||||||
<div class="skeleton h-6 w-1/2 mb-4"></div>
|
<div class="animate-pulse bg-gray-200 rounded h-6 w-1/2 mb-4"></div>
|
||||||
{#each Array(5) as _}
|
{#each Array(5) as _}
|
||||||
<div class="info-row">
|
<div
|
||||||
<div class="skeleton h-4 w-20"></div>
|
class="flex justify-between py-2 border-b border-gray-100 last:border-0"
|
||||||
<div class="skeleton h-4 w-32"></div>
|
>
|
||||||
|
<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>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
<div class="detail-card columns-section">
|
<div class="bg-white border border-gray-200 rounded-lg p-6 lg:col-span-2">
|
||||||
<div class="skeleton h-6 w-1/3 mb-4"></div>
|
<div class="animate-pulse bg-gray-200 rounded h-6 w-1/3 mb-4"></div>
|
||||||
<div class="columns-grid">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||||
{#each Array(4) as _}
|
{#each Array(4) as _}
|
||||||
<div class="column-item">
|
<div class="p-3 border border-gray-200 rounded-lg">
|
||||||
<div class="skeleton h-4 w-full mb-2"></div>
|
<div
|
||||||
<div class="skeleton h-3 w-16"></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>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else if dataset}
|
{:else if dataset}
|
||||||
<div class="detail-grid">
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
<!-- Dataset Info Card -->
|
<!-- Dataset Info Card -->
|
||||||
<div class="detail-card">
|
<div class="bg-white border border-gray-200 rounded-lg p-6">
|
||||||
<h2 class="card-title">{$t.datasets?.info || 'Dataset Information'}</h2>
|
<h2 class="text-lg font-semibold text-gray-900 mb-4">
|
||||||
<div class="info-row">
|
{$t.datasets?.info || "Dataset Information"}
|
||||||
<span class="info-label">{$t.datasets?.table_name || 'Table Name'}</span>
|
</h2>
|
||||||
<span class="info-value">{dataset.table_name}</span>
|
<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>
|
||||||
<div class="info-row">
|
<div class="flex justify-between py-2 border-b border-gray-100">
|
||||||
<span class="info-label">{$t.datasets?.schema || 'Schema'}</span>
|
<span class="text-sm text-gray-500"
|
||||||
<span class="info-value">{dataset.schema || '-'}</span>
|
>{$t.datasets?.schema || "Schema"}</span
|
||||||
|
>
|
||||||
|
<span class="text-sm font-medium text-gray-900"
|
||||||
|
>{dataset.schema || "-"}</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-row">
|
<div class="flex justify-between py-2 border-b border-gray-100">
|
||||||
<span class="info-label">{$t.datasets?.database || 'Database'}</span>
|
<span class="text-sm text-gray-500"
|
||||||
<span class="info-value">{dataset.database}</span>
|
>{$t.datasets?.database || "Database"}</span
|
||||||
|
>
|
||||||
|
<span class="text-sm font-medium text-gray-900"
|
||||||
|
>{dataset.database}</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-row">
|
<div class="flex justify-between py-2 border-b border-gray-100">
|
||||||
<span class="info-label">{$t.datasets?.columns_count || 'Columns'}</span>
|
<span class="text-sm text-gray-500"
|
||||||
<span class="info-value">{dataset.column_count}</span>
|
>{$t.datasets?.columns_count || "Columns"}</span
|
||||||
|
>
|
||||||
|
<span class="text-sm font-medium text-gray-900"
|
||||||
|
>{dataset.column_count}</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-row">
|
<div class="flex justify-between py-2 border-b border-gray-100">
|
||||||
<span class="info-label">{$t.datasets?.linked_dashboards || 'Linked Dashboards'}</span>
|
<span class="text-sm text-gray-500"
|
||||||
<span class="info-value">{dataset.linked_dashboard_count}</span>
|
>{$t.datasets?.linked_dashboards || "Linked Dashboards"}</span
|
||||||
|
>
|
||||||
|
<span class="text-sm font-medium text-gray-900"
|
||||||
|
>{dataset.linked_dashboard_count}</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
{#if dataset.is_sqllab_view}
|
{#if dataset.is_sqllab_view}
|
||||||
<div class="info-row">
|
<div class="flex justify-between py-2 border-b border-gray-100">
|
||||||
<span class="info-label">{$t.datasets?.type || 'Type'}</span>
|
<span class="text-sm text-gray-500"
|
||||||
<span class="info-value">SQL Lab View</span>
|
>{$t.datasets?.type || "Type"}</span
|
||||||
|
>
|
||||||
|
<span class="text-sm font-medium text-gray-900">SQL Lab View</span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if dataset.created_on}
|
{#if dataset.created_on}
|
||||||
<div class="info-row">
|
<div class="flex justify-between py-2 border-b border-gray-100">
|
||||||
<span class="info-label">{$t.datasets?.created || 'Created'}</span>
|
<span class="text-sm text-gray-500"
|
||||||
<span class="info-value">{new Date(dataset.created_on).toLocaleDateString()}</span>
|
>{$t.datasets?.created || "Created"}</span
|
||||||
|
>
|
||||||
|
<span class="text-sm font-medium text-gray-900"
|
||||||
|
>{new Date(dataset.created_on).toLocaleDateString()}</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if dataset.changed_on}
|
{#if dataset.changed_on}
|
||||||
<div class="info-row">
|
<div class="flex justify-between py-2">
|
||||||
<span class="info-label">{$t.datasets?.updated || 'Updated'}</span>
|
<span class="text-sm text-gray-500"
|
||||||
<span class="info-value">{new Date(dataset.changed_on).toLocaleDateString()}</span>
|
>{$t.datasets?.updated || "Updated"}</span
|
||||||
|
>
|
||||||
|
<span class="text-sm font-medium text-gray-900"
|
||||||
|
>{new Date(dataset.changed_on).toLocaleDateString()}</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Linked Dashboards Card -->
|
<!-- Linked Dashboards Card -->
|
||||||
{#if dataset.linked_dashboards && dataset.linked_dashboards.length > 0}
|
{#if dataset.linked_dashboards && dataset.linked_dashboards.length > 0}
|
||||||
<div class="detail-card">
|
<div class="bg-white border border-gray-200 rounded-lg p-6">
|
||||||
<h2 class="card-title">{$t.datasets?.linked_dashboards || 'Linked Dashboards'} ({dataset.linked_dashboard_count})</h2>
|
<h2 class="text-lg font-semibold text-gray-900 mb-4">
|
||||||
<div class="linked-dashboards-list">
|
{$t.datasets?.linked_dashboards || "Linked Dashboards"} ({dataset.linked_dashboard_count})
|
||||||
|
</h2>
|
||||||
|
<div class="space-y-2">
|
||||||
{#each dataset.linked_dashboards as dashboard}
|
{#each dataset.linked_dashboards as dashboard}
|
||||||
<div
|
<div
|
||||||
class="linked-dashboard-item"
|
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)}
|
on:click={() => navigateToDashboard(dashboard.id)}
|
||||||
role="button"
|
role="button"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
>
|
>
|
||||||
<div class="dashboard-icon">
|
<div
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
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" />
|
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
|
||||||
<line x1="3" y1="9" x2="21" y2="9" />
|
<line x1="3" y1="9" x2="21" y2="9" />
|
||||||
<line x1="9" y1="21" x2="9" y2="9" />
|
<line x1="9" y1="21" x2="9" y2="9" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class="dashboard-info">
|
<div class="flex-1">
|
||||||
<div class="dashboard-title">{dashboard.title}</div>
|
<div class="font-medium text-gray-900">{dashboard.title}</div>
|
||||||
<div class="dashboard-id">ID: {dashboard.id}{#if dashboard.slug} • {dashboard.slug}{/if}</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">
|
</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" />
|
<path d="M9 18l6-6-6-6" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
@@ -361,56 +312,80 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Columns Card -->
|
<!-- Columns Card -->
|
||||||
<div class="detail-card columns-section">
|
<div class="bg-white border border-gray-200 rounded-lg p-6 lg:col-span-2">
|
||||||
<h2 class="card-title">{$t.datasets?.columns || 'Columns'} ({dataset.column_count})</h2>
|
<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}
|
{#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}
|
{#each dataset.columns as column}
|
||||||
<div class="column-item">
|
<div
|
||||||
<div class="column-header">
|
class="p-3 border border-gray-200 rounded-lg hover:border-blue-300 transition-colors"
|
||||||
<span class="column-name">{column.name}</span>
|
>
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<span class="font-medium text-gray-900">{column.name}</span>
|
||||||
{#if column.type}
|
{#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}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="column-meta">
|
<div class="flex items-center gap-2 text-xs text-gray-500">
|
||||||
{#if column.is_dttm}
|
{#if column.is_dttm}
|
||||||
<span class="text-xs text-green-600">📅 Date/Time</span>
|
<span class="text-xs text-green-600">📅 Date/Time</span>
|
||||||
{/if}
|
{/if}
|
||||||
{#if !column.is_active}
|
{#if !column.is_active}
|
||||||
<span class="text-xs text-gray-400">(Inactive)</span>
|
<span class="text-xs text-gray-400">(Inactive)</span>
|
||||||
{/if}
|
{/if}
|
||||||
<span class="mapping-badge {column.description ? 'mapped' : 'unmapped'}">
|
<span
|
||||||
{column.description ? '✓ Mapped' : 'Unmapped'}
|
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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{#if column.description}
|
{#if column.description}
|
||||||
<p class="column-description">{column.description}</p>
|
<p class="text-sm text-gray-600 mt-2">{column.description}</p>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="empty-state">
|
<div class="py-8 text-center text-gray-500">
|
||||||
{$t.datasets?.no_columns || 'No columns found'}
|
{$t.datasets?.no_columns || "No columns found"}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- SQL Section (for SQL Lab views) -->
|
<!-- SQL Section (for SQL Lab views) -->
|
||||||
{#if dataset.sql}
|
{#if dataset.sql}
|
||||||
<div class="detail-card sql-section lg:col-span-3">
|
<div
|
||||||
<h2 class="card-title">{$t.datasets?.sql_query || 'SQL Query'}</h2>
|
class="bg-white border border-gray-200 rounded-lg p-6 mt-6 lg:col-span-3"
|
||||||
<pre class="sql-code">{dataset.sql}</pre>
|
>
|
||||||
|
<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>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="empty-state">
|
<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">
|
<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" />
|
<path d="M3 3h18v18H3V3zm16 16V5H5v14h14z" />
|
||||||
</svg>
|
</svg>
|
||||||
<p>{$t.datasets?.not_found || 'Dataset not found'}</p>
|
<p>{$t.datasets?.not_found || "Dataset not found"}</p>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -154,8 +154,5 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- [/SECTION: TEMPLATE] -->
|
<!-- [/SECTION: TEMPLATE] -->
|
||||||
|
|
||||||
<style>
|
|
||||||
/* No additional styles needed, using Tailwind */
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<!-- [/DEF:LoginPage:Component] -->
|
<!-- [/DEF:LoginPage:Component] -->
|
||||||
@@ -384,8 +384,5 @@
|
|||||||
|
|
||||||
<!-- [/SECTION] -->
|
<!-- [/SECTION] -->
|
||||||
|
|
||||||
<style>
|
|
||||||
/* Page specific styles */
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<!-- [/DEF:MigrationDashboard:Component] -->
|
<!-- [/DEF:MigrationDashboard:Component] -->
|
||||||
|
|||||||
@@ -173,8 +173,5 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- [/SECTION] -->
|
<!-- [/SECTION] -->
|
||||||
|
|
||||||
<style>
|
|
||||||
/* Page specific styles */
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<!-- [/DEF:MappingManagement:Component] -->
|
<!-- [/DEF:MappingManagement:Component] -->
|
||||||
|
|||||||
@@ -14,14 +14,14 @@
|
|||||||
* @UX_RECOVERY: Refresh button reloads settings data
|
* @UX_RECOVERY: Refresh button reloads settings data
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from "svelte";
|
||||||
import { t } from '$lib/i18n';
|
import { t } from "$lib/i18n";
|
||||||
import { api } from '$lib/api.js';
|
import { api } from "$lib/api.js";
|
||||||
import { addToast } from '$lib/toasts';
|
import { addToast } from "$lib/toasts";
|
||||||
import ProviderConfig from '../../components/llm/ProviderConfig.svelte';
|
import ProviderConfig from "../../components/llm/ProviderConfig.svelte";
|
||||||
|
|
||||||
// State
|
// State
|
||||||
let activeTab = 'environments';
|
let activeTab = "environments";
|
||||||
let settings = null;
|
let settings = null;
|
||||||
let isLoading = true;
|
let isLoading = true;
|
||||||
let error = null;
|
let error = null;
|
||||||
@@ -30,16 +30,16 @@
|
|||||||
let editingEnvId = null;
|
let editingEnvId = null;
|
||||||
let isAddingEnv = false;
|
let isAddingEnv = false;
|
||||||
let newEnv = {
|
let newEnv = {
|
||||||
id: '',
|
id: "",
|
||||||
name: '',
|
name: "",
|
||||||
url: '',
|
url: "",
|
||||||
username: '',
|
username: "",
|
||||||
password: '',
|
password: "",
|
||||||
is_default: false,
|
is_default: false,
|
||||||
backup_schedule: {
|
backup_schedule: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
cron_expression: '0 0 * * *'
|
cron_expression: "0 0 * * *",
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Load settings on mount
|
// Load settings on mount
|
||||||
@@ -55,8 +55,8 @@
|
|||||||
const response = await api.getConsolidatedSettings();
|
const response = await api.getConsolidatedSettings();
|
||||||
settings = response;
|
settings = response;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error = err.message || 'Failed to load settings';
|
error = err.message || "Failed to load settings";
|
||||||
console.error('[SettingsPage][Coherence:Failed]', err);
|
console.error("[SettingsPage][Coherence:Failed]", err);
|
||||||
} finally {
|
} finally {
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
}
|
}
|
||||||
@@ -70,39 +70,42 @@
|
|||||||
// Get tab class
|
// Get tab class
|
||||||
function getTabClass(tab) {
|
function getTabClass(tab) {
|
||||||
return activeTab === tab
|
return activeTab === tab
|
||||||
? 'text-blue-600 border-b-2 border-blue-600'
|
? "text-blue-600 border-b-2 border-blue-600"
|
||||||
: 'text-gray-600 hover:text-gray-800 border-transparent hover:border-gray-300';
|
: "text-gray-600 hover:text-gray-800 border-transparent hover:border-gray-300";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle global settings save (Logging, Storage)
|
// Handle global settings save (Logging, Storage)
|
||||||
async function handleSave() {
|
async function handleSave() {
|
||||||
console.log('[SettingsPage][Action] Saving settings');
|
console.log("[SettingsPage][Action] Saving settings");
|
||||||
try {
|
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.
|
// but updateConsolidatedSettings expects full object or we can use specific endpoints.
|
||||||
// For now we use the consolidated update.
|
// For now we use the consolidated update.
|
||||||
await api.updateConsolidatedSettings(settings);
|
await api.updateConsolidatedSettings(settings);
|
||||||
addToast($t.settings?.save_success || 'Settings saved', 'success');
|
addToast($t.settings?.save_success || "Settings saved", "success");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[SettingsPage][Coherence:Failed]', err);
|
console.error("[SettingsPage][Coherence:Failed]", err);
|
||||||
addToast($t.settings?.save_failed || 'Failed to save settings', 'error');
|
addToast($t.settings?.save_failed || "Failed to save settings", "error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle environment actions
|
// Handle environment actions
|
||||||
async function handleTestEnv(id) {
|
async function handleTestEnv(id) {
|
||||||
console.log(`[SettingsPage][Action] Test environment ${id}`);
|
console.log(`[SettingsPage][Action] Test environment ${id}`);
|
||||||
addToast('Testing connection...', 'info');
|
addToast("Testing connection...", "info");
|
||||||
try {
|
try {
|
||||||
const result = await api.testEnvironmentConnection(id);
|
const result = await api.testEnvironmentConnection(id);
|
||||||
if (result.status === 'success') {
|
if (result.status === "success") {
|
||||||
addToast('Connection successful', 'success');
|
addToast("Connection successful", "success");
|
||||||
} else {
|
} else {
|
||||||
addToast(`Connection failed: ${result.message}`, 'error');
|
addToast(`Connection failed: ${result.message}`, "error");
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[SettingsPage][Coherence:Failed] Error testing connection:', err);
|
console.error(
|
||||||
addToast('Failed to test connection', '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
|
newEnv = JSON.parse(JSON.stringify(env)); // Deep copy
|
||||||
// Ensure backup_schedule exists
|
// Ensure backup_schedule exists
|
||||||
if (!newEnv.backup_schedule) {
|
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;
|
editingEnvId = env.id;
|
||||||
isAddingEnv = false;
|
isAddingEnv = false;
|
||||||
@@ -119,36 +122,38 @@
|
|||||||
|
|
||||||
function resetEnvForm() {
|
function resetEnvForm() {
|
||||||
newEnv = {
|
newEnv = {
|
||||||
id: '',
|
id: "",
|
||||||
name: '',
|
name: "",
|
||||||
url: '',
|
url: "",
|
||||||
username: '',
|
username: "",
|
||||||
password: '',
|
password: "",
|
||||||
is_default: false,
|
is_default: false,
|
||||||
backup_schedule: {
|
backup_schedule: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
cron_expression: '0 0 * * *'
|
cron_expression: "0 0 * * *",
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
editingEnvId = null;
|
editingEnvId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleAddOrUpdateEnv() {
|
async function handleAddOrUpdateEnv() {
|
||||||
try {
|
try {
|
||||||
console.log(`[SettingsPage][Action] ${editingEnvId ? 'Updating' : 'Adding'} environment.`);
|
console.log(
|
||||||
|
`[SettingsPage][Action] ${editingEnvId ? "Updating" : "Adding"} environment.`,
|
||||||
|
);
|
||||||
|
|
||||||
// Basic validation
|
// Basic validation
|
||||||
if (!newEnv.id || !newEnv.name || !newEnv.url) {
|
if (!newEnv.id || !newEnv.name || !newEnv.url) {
|
||||||
addToast('Please fill in all required fields (ID, Name, URL)', 'error');
|
addToast("Please fill in all required fields (ID, Name, URL)", "error");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (editingEnvId) {
|
if (editingEnvId) {
|
||||||
await api.updateEnvironment(editingEnvId, newEnv);
|
await api.updateEnvironment(editingEnvId, newEnv);
|
||||||
addToast('Environment updated', 'success');
|
addToast("Environment updated", "success");
|
||||||
} else {
|
} else {
|
||||||
await api.addEnvironment(newEnv);
|
await api.addEnvironment(newEnv);
|
||||||
addToast('Environment added', 'success');
|
addToast("Environment added", "success");
|
||||||
}
|
}
|
||||||
|
|
||||||
resetEnvForm();
|
resetEnvForm();
|
||||||
@@ -156,148 +161,138 @@
|
|||||||
isAddingEnv = false;
|
isAddingEnv = false;
|
||||||
await loadSettings();
|
await loadSettings();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[SettingsPage][Coherence:Failed] Failed to save environment:", error);
|
console.error(
|
||||||
addToast(error.message || 'Failed to save environment', 'error');
|
"[SettingsPage][Coherence:Failed] Failed to save environment:",
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
addToast(error.message || "Failed to save environment", "error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleDeleteEnv(id) {
|
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}`);
|
console.log(`[SettingsPage][Action] Delete environment ${id}`);
|
||||||
try {
|
try {
|
||||||
await api.deleteEnvironment(id);
|
await api.deleteEnvironment(id);
|
||||||
addToast('Environment deleted', 'success');
|
addToast("Environment deleted", "success");
|
||||||
await loadSettings();
|
await loadSettings();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[SettingsPage][Coherence:Failed] Failed to delete environment:", error);
|
console.error(
|
||||||
addToast('Failed to delete environment', 'error');
|
"[SettingsPage][Coherence:Failed] Failed to delete environment:",
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
addToast("Failed to delete environment", "error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<div class="max-w-7xl mx-auto px-4 py-6">
|
||||||
.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">
|
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="header">
|
<div class="flex items-center justify-between mb-6">
|
||||||
<h1 class="title">{$t.settings?.title || 'Settings'}</h1>
|
<h1 class="text-2xl font-bold text-gray-900">
|
||||||
<button class="refresh-btn" on:click={loadSettings}>
|
{$t.settings?.title || "Settings"}
|
||||||
{$t.common?.refresh || 'Refresh'}
|
</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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Error Banner -->
|
<!-- Error Banner -->
|
||||||
{#if error}
|
{#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>
|
<span>{error}</span>
|
||||||
<button class="retry-btn" on:click={loadSettings}>
|
<button
|
||||||
{$t.common?.retry || 'Retry'}
|
class="px-4 py-2 bg-destructive text-white rounded hover:bg-destructive-hover transition-colors"
|
||||||
|
on:click={loadSettings}
|
||||||
|
>
|
||||||
|
{$t.common?.retry || "Retry"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Loading State -->
|
<!-- Loading State -->
|
||||||
{#if isLoading}
|
{#if isLoading}
|
||||||
<div class="tab-content">
|
<div class="bg-white rounded-lg p-6 border border-gray-200">
|
||||||
<div class="skeleton h-8"></div>
|
<div class="animate-pulse bg-gray-200 rounded h-8"></div>
|
||||||
<div class="skeleton h-8"></div>
|
<div class="animate-pulse bg-gray-200 rounded h-8"></div>
|
||||||
<div class="skeleton h-8"></div>
|
<div class="animate-pulse bg-gray-200 rounded h-8"></div>
|
||||||
<div class="skeleton h-8"></div>
|
<div class="animate-pulse bg-gray-200 rounded h-8"></div>
|
||||||
<div class="skeleton h-8"></div>
|
<div class="animate-pulse bg-gray-200 rounded h-8"></div>
|
||||||
</div>
|
</div>
|
||||||
{:else if settings}
|
{:else if settings}
|
||||||
<!-- Tabs -->
|
<!-- Tabs -->
|
||||||
<div class="tabs">
|
<div class="border-b border-gray-200 mb-6">
|
||||||
<button
|
<button
|
||||||
class="tab-btn {getTabClass('environments')}"
|
class="px-4 py-2 text-sm font-medium transition-colors focus:outline-none {getTabClass(
|
||||||
on:click={() => handleTabChange('environments')}
|
'environments',
|
||||||
|
)}"
|
||||||
|
on:click={() => handleTabChange("environments")}
|
||||||
>
|
>
|
||||||
{$t.settings?.environments || 'Environments'}
|
{$t.settings?.environments || "Environments"}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="tab-btn {getTabClass('logging')}"
|
class="px-4 py-2 text-sm font-medium transition-colors focus:outline-none {getTabClass(
|
||||||
on:click={() => handleTabChange('logging')}
|
'logging',
|
||||||
|
)}"
|
||||||
|
on:click={() => handleTabChange("logging")}
|
||||||
>
|
>
|
||||||
{$t.settings?.logging || 'Logging'}
|
{$t.settings?.logging || "Logging"}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="tab-btn {getTabClass('connections')}"
|
class="px-4 py-2 text-sm font-medium transition-colors focus:outline-none {getTabClass(
|
||||||
on:click={() => handleTabChange('connections')}
|
'connections',
|
||||||
|
)}"
|
||||||
|
on:click={() => handleTabChange("connections")}
|
||||||
>
|
>
|
||||||
{$t.settings?.connections || 'Connections'}
|
{$t.settings?.connections || "Connections"}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="tab-btn {getTabClass('llm')}"
|
class="px-4 py-2 text-sm font-medium transition-colors focus:outline-none {getTabClass(
|
||||||
on:click={() => handleTabChange('llm')}
|
'llm',
|
||||||
|
)}"
|
||||||
|
on:click={() => handleTabChange("llm")}
|
||||||
>
|
>
|
||||||
{$t.settings?.llm || 'LLM'}
|
{$t.settings?.llm || "LLM"}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="tab-btn {getTabClass('storage')}"
|
class="px-4 py-2 text-sm font-medium transition-colors focus:outline-none {getTabClass(
|
||||||
on:click={() => handleTabChange('storage')}
|
'storage',
|
||||||
|
)}"
|
||||||
|
on:click={() => handleTabChange("storage")}
|
||||||
>
|
>
|
||||||
{$t.settings?.storage || 'Storage'}
|
{$t.settings?.storage || "Storage"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tab Content -->
|
<!-- Tab Content -->
|
||||||
<div class="tab-content">
|
<div class="bg-white rounded-lg p-6 border border-gray-200">
|
||||||
{#if activeTab === 'environments'}
|
{#if activeTab === "environments"}
|
||||||
<!-- Environments Tab -->
|
<!-- Environments Tab -->
|
||||||
<div class="text-lg font-medium mb-4">
|
<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">
|
<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>
|
</p>
|
||||||
|
|
||||||
{#if !editingEnvId && !isAddingEnv}
|
{#if !editingEnvId && !isAddingEnv}
|
||||||
<div class="flex justify-end mb-6">
|
<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"
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -305,10 +300,15 @@
|
|||||||
{#if editingEnvId || isAddingEnv}
|
{#if editingEnvId || isAddingEnv}
|
||||||
<!-- Add/Edit Environment Form -->
|
<!-- Add/Edit Environment Form -->
|
||||||
<div class="bg-gray-50 p-6 rounded-lg mb-6 border border-gray-200">
|
<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 class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label for="env_id" class="block text-sm font-medium text-gray-700">ID</label>
|
<label
|
||||||
|
for="env_id"
|
||||||
|
class="block text-sm font-medium text-gray-700">ID</label
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="env_id"
|
id="env_id"
|
||||||
@@ -318,43 +318,111 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="env_name" class="block text-sm font-medium text-gray-700">Name</label>
|
<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" />
|
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>
|
||||||
<div>
|
<div>
|
||||||
<label for="env_url" class="block text-sm font-medium text-gray-700">URL</label>
|
<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" />
|
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>
|
||||||
<div>
|
<div>
|
||||||
<label for="env_user" class="block text-sm font-medium text-gray-700">Username</label>
|
<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" />
|
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>
|
||||||
<div>
|
<div>
|
||||||
<label for="env_pass" class="block text-sm font-medium text-gray-700">Password</label>
|
<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" />
|
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>
|
||||||
<div class="flex items-center mt-6">
|
<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" />
|
<input
|
||||||
<label for="env_default" class="ml-2 block text-sm text-gray-900">Default Environment</label>
|
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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 class="text-lg font-medium mb-4 mt-6">Backup Schedule</h3>
|
<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="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div class="flex items-center">
|
<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" />
|
<input
|
||||||
<label for="backup_enabled" class="ml-2 block text-sm text-gray-900">Enable Automatic Backups</label>
|
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>
|
||||||
<div>
|
<div>
|
||||||
<label for="cron_expression" class="block text-sm font-medium text-gray-700">Cron Expression</label>
|
<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" />
|
for="cron_expression"
|
||||||
<p class="text-xs text-gray-500 mt-1">Example: 0 0 * * * (daily at midnight)</p>
|
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>
|
</div>
|
||||||
|
|
||||||
<div class="mt-6 flex gap-2 justify-end">
|
<div class="mt-6 flex gap-2 justify-end">
|
||||||
<button
|
<button
|
||||||
on:click={() => { isAddingEnv = false; editingEnvId = null; resetEnvForm(); }}
|
on:click={() => {
|
||||||
|
isAddingEnv = false;
|
||||||
|
editingEnvId = null;
|
||||||
|
resetEnvForm();
|
||||||
|
}}
|
||||||
class="bg-gray-200 text-gray-700 px-4 py-2 rounded hover:bg-gray-300"
|
class="bg-gray-200 text-gray-700 px-4 py-2 rounded hover:bg-gray-300"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
@@ -363,7 +431,7 @@
|
|||||||
on:click={handleAddOrUpdateEnv}
|
on:click={handleAddOrUpdateEnv}
|
||||||
class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"
|
class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"
|
||||||
>
|
>
|
||||||
{editingEnvId ? 'Update' : 'Add'} Environment
|
{editingEnvId ? "Update" : "Add"} Environment
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -374,11 +442,26 @@
|
|||||||
<table class="min-w-full divide-y divide-gray-200">
|
<table class="min-w-full divide-y divide-gray-200">
|
||||||
<thead class="bg-gray-50">
|
<thead class="bg-gray-50">
|
||||||
<tr>
|
<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
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">URL</th>
|
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{$t.connections?.user || "Username"}</th>
|
>{$t.connections?.name || "Name"}</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"
|
||||||
|
>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>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="bg-white divide-y divide-gray-200">
|
<tbody class="bg-white divide-y divide-gray-200">
|
||||||
@@ -386,24 +469,38 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td class="px-6 py-4 whitespace-nowrap">{env.name}</td>
|
<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.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">
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
{#if env.is_default}
|
{#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
|
Yes
|
||||||
</span>
|
</span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="text-gray-500">No</span>
|
<span class="text-gray-500">No</span>
|
||||||
{/if}
|
{/if}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
<td
|
||||||
<button class="text-green-600 hover:text-green-900 mr-4" on:click={() => handleTestEnv(env.id)}>
|
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"}
|
{$t.settings?.env_test || "Test"}
|
||||||
</button>
|
</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"}
|
{$t.common.edit || "Edit"}
|
||||||
</button>
|
</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"}
|
{$t.settings?.env_delete || "Delete"}
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
@@ -413,25 +510,41 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{:else if !isAddingEnv}
|
{: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 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>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{:else if activeTab === 'logging'}
|
{:else if activeTab === "logging"}
|
||||||
<!-- Logging Tab -->
|
<!-- Logging Tab -->
|
||||||
<div class="text-lg font-medium mb-4">
|
<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">
|
<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>
|
</p>
|
||||||
|
|
||||||
<div class="bg-gray-50 p-6 rounded-lg border border-gray-200">
|
<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 class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label for="log_level" class="block text-sm font-medium text-gray-700">Log Level</label>
|
<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">
|
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="DEBUG">DEBUG</option>
|
||||||
<option value="INFO">INFO</option>
|
<option value="INFO">INFO</option>
|
||||||
<option value="WARNING">WARNING</option>
|
<option value="WARNING">WARNING</option>
|
||||||
@@ -440,8 +553,16 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="task_log_level" class="block text-sm font-medium text-gray-700">Task Log Level</label>
|
<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">
|
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="DEBUG">DEBUG</option>
|
||||||
<option value="INFO">INFO</option>
|
<option value="INFO">INFO</option>
|
||||||
<option value="WARNING">WARNING</option>
|
<option value="WARNING">WARNING</option>
|
||||||
@@ -450,10 +571,19 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="md:col-span-2">
|
<div class="md:col-span-2">
|
||||||
<label class="flex items-center">
|
<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" />
|
<input
|
||||||
<span class="ml-2 block text-sm text-gray-900">Enable Belief State Logging (Beta)</span>
|
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>
|
</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>
|
</div>
|
||||||
|
|
||||||
@@ -467,57 +597,103 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else if activeTab === 'connections'}
|
{:else if activeTab === "connections"}
|
||||||
<!-- Connections Tab -->
|
<!-- Connections Tab -->
|
||||||
<div class="text-lg font-medium mb-4">
|
<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">
|
<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>
|
</p>
|
||||||
|
|
||||||
{#if settings.connections && settings.connections.length > 0}
|
{#if settings.connections && settings.connections.length > 0}
|
||||||
<!-- Connections list would go here -->
|
<!-- Connections list would go here -->
|
||||||
<p class="text-gray-500 italic">No additional connections configured. Superset database connections are used by default.</p>
|
<p class="text-gray-500 italic">
|
||||||
|
No additional connections configured. Superset database
|
||||||
|
connections are used by default.
|
||||||
|
</p>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="text-center py-8 bg-gray-50 rounded-lg border border-dashed border-gray-300">
|
<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>
|
<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">
|
<button
|
||||||
|
class="mt-4 px-4 py-2 border border-blue-600 text-blue-600 rounded hover:bg-blue-50"
|
||||||
|
>
|
||||||
Add Connection
|
Add Connection
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{:else if activeTab === 'llm'}
|
{:else if activeTab === "llm"}
|
||||||
<!-- LLM Tab -->
|
<!-- LLM Tab -->
|
||||||
<div class="text-lg font-medium mb-4">
|
<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">
|
<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>
|
</p>
|
||||||
|
|
||||||
<ProviderConfig providers={settings.llm_providers || []} onSave={loadSettings} />
|
<ProviderConfig
|
||||||
|
providers={settings.llm_providers || []}
|
||||||
|
onSave={loadSettings}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{:else if activeTab === 'storage'}
|
{:else if activeTab === "storage"}
|
||||||
<!-- Storage Tab -->
|
<!-- Storage Tab -->
|
||||||
<div class="text-lg font-medium mb-4">
|
<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">
|
<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>
|
</p>
|
||||||
|
|
||||||
<div class="bg-gray-50 p-6 rounded-lg border border-gray-200">
|
<div class="bg-gray-50 p-6 rounded-lg border border-gray-200">
|
||||||
<div class="grid grid-cols-1 gap-4">
|
<div class="grid grid-cols-1 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label for="storage_path" class="block text-sm font-medium text-gray-700">Root Path</label>
|
<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" />
|
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>
|
||||||
<div>
|
<div>
|
||||||
<label for="backup_path" class="block text-sm font-medium text-gray-700">Backup Path</label>
|
<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" />
|
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>
|
||||||
<div>
|
<div>
|
||||||
<label for="repo_path" class="block text-sm font-medium text-gray-700">Repository Path</label>
|
<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" />
|
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>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -175,8 +175,5 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- [/SECTION: TEMPLATE] -->
|
<!-- [/SECTION: TEMPLATE] -->
|
||||||
|
|
||||||
<style>
|
|
||||||
/* Styles are handled by Tailwind */
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<!-- [/DEF:GitSettingsPage:Component] -->
|
<!-- [/DEF:GitSettingsPage:Component] -->
|
||||||
@@ -218,8 +218,5 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- [/SECTION: TEMPLATE] -->
|
<!-- [/SECTION: TEMPLATE] -->
|
||||||
|
|
||||||
<style>
|
|
||||||
/* ... */
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<!-- [/DEF:StoragePage:Component] -->
|
<!-- [/DEF:StoragePage:Component] -->
|
||||||
@@ -5,7 +5,69 @@ export default {
|
|||||||
"./src/**/*.{svelte,js,ts,jsx,tsx}",
|
"./src/**/*.{svelte,js,ts,jsx,tsx}",
|
||||||
],
|
],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {},
|
extend: {
|
||||||
|
colors: {
|
||||||
|
// Semantic UI colors (light surfaces)
|
||||||
|
primary: {
|
||||||
|
DEFAULT: '#2563eb', // blue-600
|
||||||
|
hover: '#1d4ed8', // blue-700
|
||||||
|
ring: '#3b82f6', // blue-500
|
||||||
|
light: '#eff6ff', // blue-50
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
DEFAULT: '#f3f4f6', // gray-100
|
||||||
|
hover: '#e5e7eb', // gray-200
|
||||||
|
text: '#111827', // gray-900
|
||||||
|
ring: '#6b7280', // gray-500
|
||||||
|
},
|
||||||
|
destructive: {
|
||||||
|
DEFAULT: '#dc2626', // red-600
|
||||||
|
hover: '#b91c1c', // red-700
|
||||||
|
ring: '#ef4444', // red-500
|
||||||
|
light: '#fef2f2', // red-50
|
||||||
|
},
|
||||||
|
ghost: {
|
||||||
|
hover: '#f3f4f6', // gray-100
|
||||||
|
text: '#374151', // gray-700
|
||||||
|
ring: '#6b7280', // gray-500
|
||||||
|
},
|
||||||
|
// Dark terminal palette (log viewer, task drawer)
|
||||||
|
terminal: {
|
||||||
|
bg: '#0f172a', // slate-900
|
||||||
|
surface: '#1e293b', // slate-800
|
||||||
|
border: '#334155', // slate-700
|
||||||
|
text: {
|
||||||
|
DEFAULT: '#cbd5e1', // slate-300
|
||||||
|
muted: '#475569', // slate-600
|
||||||
|
subtle: '#64748b', // slate-500
|
||||||
|
bright: '#e2e8f0', // slate-200
|
||||||
|
heading: '#f1f5f9', // slate-100
|
||||||
|
},
|
||||||
|
accent: '#22d3ee', // cyan-400
|
||||||
|
},
|
||||||
|
// Log level palette
|
||||||
|
log: {
|
||||||
|
debug: '#64748b',
|
||||||
|
info: '#38bdf8',
|
||||||
|
warning: '#fbbf24',
|
||||||
|
error: '#f87171',
|
||||||
|
},
|
||||||
|
// Log source palette
|
||||||
|
source: {
|
||||||
|
plugin: '#4ade80',
|
||||||
|
api: '#c084fc',
|
||||||
|
git: '#fb923c',
|
||||||
|
system: '#38bdf8',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
width: {
|
||||||
|
sidebar: '240px',
|
||||||
|
'sidebar-collapsed': '64px',
|
||||||
|
},
|
||||||
|
fontFamily: {
|
||||||
|
mono: ['"JetBrains Mono"', '"Fira Code"', 'monospace'],
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
plugins: [],
|
plugins: [],
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user