- Delete legacy .ts stores (auth, activity, assistantChat, datasetReview, environmentContext, health, sidebar, taskDrawer, translationRun) - Create new .svelte.ts runes-based stores using reactive primitives - Migrate i18n: /i18n → /i18n/index.svelte.js - Migrate toasts: /toasts → /toasts.svelte.js - Update all component imports across 180+ files: components, pages, routes, lib - Remove fromStore() wrappers — use store.value directly with Svelte 5 runes - Update test mocks for new import paths - Add @DEPRECATED annotation to legacy alias
699 lines
24 KiB
Svelte
699 lines
24 KiB
Svelte
<!-- #region RepositoryDashboardGrid [C:3] [TYPE Component] [SEMANTICS dashboard, grid, pagination, selection, repository] -->
|
|
<!-- @BRIEF Component component: components/RepositoryDashboardGrid.svelte -->
|
|
<!-- @LAYER UI -->
|
|
<!--
|
|
@SEMANTICS: dashboard, grid, selection, pagination
|
|
@PURPOSE: Displays a grid of dashboards with selection and pagination.
|
|
@LAYER Component
|
|
@RELATION USED_BY -> [frontend/src/routes/migration/+page.svelte]
|
|
|
|
@INVARIANT: Selected IDs must be a subset of available dashboards.
|
|
-->
|
|
|
|
<script lang="ts">
|
|
// [SECTION: IMPORTS]
|
|
import { untrack } from "svelte";
|
|
import type { DashboardMetadata } from "../types/dashboard";
|
|
import { t } from '$lib/i18n/index.svelte.js';
|
|
import { Button, Input } from "../lib/ui";
|
|
import GitManager from "./git/GitManager.svelte";
|
|
import { gitService } from "../services/gitService";
|
|
import { addToast } from "$lib/toasts.svelte.js";
|
|
// [/SECTION]
|
|
|
|
// [SECTION: PROPS]
|
|
let {
|
|
dashboards = [],
|
|
selectedIds = [],
|
|
statusMode = "dashboard",
|
|
envId = null,
|
|
repositoriesOnly = false,
|
|
} = $props();
|
|
|
|
// [/SECTION]
|
|
|
|
// [SECTION: STATE]
|
|
let filterText = $state("");
|
|
let currentPage = $state(0);
|
|
let pageSize = $state(20);
|
|
let sortColumn: keyof DashboardMetadata = $state("title");
|
|
let sortDirection: "asc" | "desc" = $state("asc");
|
|
// [/SECTION]
|
|
|
|
// [SECTION: UI STATE]
|
|
let showGitManager = $state(false);
|
|
let gitDashboardId: string | null = $state(null);
|
|
let gitDashboardTitle = $state("");
|
|
let repositoryStatusByDashboardId = $state<Record<number, string>>({});
|
|
let repositoryStatusRequestId = $state(0);
|
|
let bulkActionRunning = $state(false);
|
|
// [/SECTION]
|
|
|
|
// [SECTION: DERIVED]
|
|
let filteredDashboards = $derived(
|
|
dashboards.filter((d) => {
|
|
const matchesTitle = d.title.toLowerCase().includes(filterText.toLowerCase());
|
|
if (!matchesTitle) return false;
|
|
if (!repositoriesOnly || statusMode !== "repository") return true;
|
|
return getRepositoryStatusToken(d.id) !== "no_repo";
|
|
}),
|
|
);
|
|
|
|
let sortedDashboards = $derived(
|
|
[...filteredDashboards].sort((a, b) => {
|
|
let aVal =
|
|
sortColumn === "status"
|
|
? getSortStatusValue(a)
|
|
: a[sortColumn];
|
|
let bVal =
|
|
sortColumn === "status"
|
|
? getSortStatusValue(b)
|
|
: b[sortColumn];
|
|
if (sortColumn === "id") {
|
|
aVal = Number(aVal);
|
|
bVal = Number(bVal);
|
|
}
|
|
if (aVal < bVal) return sortDirection === "asc" ? -1 : 1;
|
|
if (aVal > bVal) return sortDirection === "asc" ? 1 : -1;
|
|
return 0;
|
|
}),
|
|
);
|
|
|
|
let paginatedDashboards = $derived(
|
|
sortedDashboards.slice(
|
|
currentPage * pageSize,
|
|
(currentPage + 1) * pageSize,
|
|
),
|
|
);
|
|
|
|
let totalPages = $derived(Math.ceil(sortedDashboards.length / pageSize));
|
|
|
|
let allSelected = $derived(
|
|
paginatedDashboards.length > 0 &&
|
|
paginatedDashboards.every((d) => selectedIds.includes(d.id)),
|
|
);
|
|
let someSelected = $derived(
|
|
paginatedDashboards.some((d) => selectedIds.includes(d.id)),
|
|
);
|
|
// [/SECTION]
|
|
|
|
// #region handleSort:Function [TYPE Function]
|
|
// @PURPOSE: Toggles sort direction or changes sort column.
|
|
// @PRE: column name is provided.
|
|
// @POST: sortColumn and sortDirection state updated.
|
|
function handleSort(column: keyof DashboardMetadata) {
|
|
if (sortColumn === column) {
|
|
sortDirection = sortDirection === "asc" ? "desc" : "asc";
|
|
} else {
|
|
sortColumn = column;
|
|
sortDirection = "asc";
|
|
}
|
|
}
|
|
// #endregion handleSort:Function
|
|
|
|
// #region handleSelectionChange:Function [TYPE Function]
|
|
// @PURPOSE: Handles individual checkbox changes.
|
|
// @PRE: dashboard ID and checked status provided.
|
|
// @POST: selectedIds array updated.
|
|
function handleSelectionChange(id: number, checked: boolean) {
|
|
let newSelected = [...selectedIds];
|
|
if (checked) {
|
|
if (!newSelected.includes(id)) newSelected.push(id);
|
|
} else {
|
|
newSelected = newSelected.filter((sid) => sid !== id);
|
|
}
|
|
selectedIds = newSelected;
|
|
}
|
|
// #endregion handleSelectionChange:Function
|
|
|
|
// #region handleSelectAll:Function [TYPE Function]
|
|
// @PURPOSE: Handles select all checkbox.
|
|
// @PRE: checked status provided.
|
|
// @POST: selectedIds array updated for all paginated items.
|
|
function handleSelectAll(checked: boolean) {
|
|
let newSelected = [...selectedIds];
|
|
if (checked) {
|
|
paginatedDashboards.forEach((d) => {
|
|
if (!newSelected.includes(d.id)) newSelected.push(d.id);
|
|
});
|
|
} else {
|
|
paginatedDashboards.forEach((d) => {
|
|
newSelected = newSelected.filter((sid) => sid !== d.id);
|
|
});
|
|
}
|
|
selectedIds = newSelected;
|
|
}
|
|
// #endregion handleSelectAll:Function
|
|
|
|
// #region goToPage:Function [TYPE Function]
|
|
// @PURPOSE: Changes current page.
|
|
// @PRE: page index is provided.
|
|
// @POST: currentPage state updated if within valid range.
|
|
function goToPage(page: number) {
|
|
if (page >= 0 && page < totalPages) {
|
|
currentPage = page;
|
|
}
|
|
}
|
|
// #endregion goToPage:Function
|
|
|
|
// #region getRepositoryStatusToken:Function [TYPE Function]
|
|
/**
|
|
* @purpose Returns normalized repository status token for a dashboard.
|
|
* @pre Dashboard exists.
|
|
* @post Returns one of loading|no_repo|synced|changes|behind_remote|ahead_remote|diverged|error.
|
|
*/
|
|
function getRepositoryStatusToken(dashboardId: number): string {
|
|
return repositoryStatusByDashboardId[dashboardId] || "loading";
|
|
}
|
|
// #endregion getRepositoryStatusToken:Function
|
|
|
|
// #region isRepositoryReady:Function [TYPE Function]
|
|
// @PURPOSE: Determines whether git actions can run for a dashboard.
|
|
function isRepositoryReady(dashboardId: number): boolean {
|
|
const token = getRepositoryStatusToken(dashboardId);
|
|
return token !== "loading" && token !== "no_repo" && token !== "error";
|
|
}
|
|
// #endregion isRepositoryReady:Function
|
|
|
|
// #region invalidateRepositoryStatuses:Function [TYPE Function]
|
|
// @PURPOSE: Marks dashboard statuses as loading so they are refetched.
|
|
function invalidateRepositoryStatuses(dashboardIds: number[]): void {
|
|
if (dashboardIds.length === 0) return;
|
|
const nextStatuses = { ...repositoryStatusByDashboardId };
|
|
dashboardIds.forEach((dashboardId) => {
|
|
nextStatuses[dashboardId] = "loading";
|
|
});
|
|
repositoryStatusByDashboardId = nextStatuses;
|
|
}
|
|
// #endregion invalidateRepositoryStatuses:Function
|
|
|
|
// #region resolveRepositoryStatusToken:Function [TYPE Function]
|
|
/**
|
|
* @purpose Converts git status payload into a stable UI status token.
|
|
*/
|
|
function resolveRepositoryStatusToken(status: any): string {
|
|
const syncState = String(status?.sync_state || "").toUpperCase();
|
|
if (syncState === "DIVERGED") return "diverged";
|
|
if (syncState === "BEHIND_REMOTE") return "behind_remote";
|
|
if (syncState === "AHEAD_REMOTE") return "ahead_remote";
|
|
if (syncState === "CHANGES") return "changes";
|
|
if (syncState === "SYNCED") return "synced";
|
|
|
|
const syncStatus = String(status?.sync_status || "").toUpperCase();
|
|
if (syncStatus === "NO_REPO") return "no_repo";
|
|
if (syncStatus === "ERROR") return "error";
|
|
if (syncStatus === "DIFF") return "changes";
|
|
if (syncStatus === "OK") return "synced";
|
|
|
|
const aheadCount = Number(status?.ahead_count || 0);
|
|
const behindCount = Number(status?.behind_count || 0);
|
|
if (aheadCount > 0 && behindCount > 0) return "diverged";
|
|
if (behindCount > 0) return "behind_remote";
|
|
if (aheadCount > 0) return "ahead_remote";
|
|
|
|
const hasChanges =
|
|
Boolean(status?.is_dirty) ||
|
|
(status?.untracked_files?.length || 0) > 0 ||
|
|
(status?.modified_files?.length || 0) > 0 ||
|
|
(status?.staged_files?.length || 0) > 0;
|
|
|
|
return hasChanges ? "changes" : "synced";
|
|
}
|
|
// #endregion resolveRepositoryStatusToken:Function
|
|
|
|
// #region loadRepositoryStatuses:Function [TYPE Function]
|
|
/**
|
|
* @purpose Hydrates repository status map for dashboards in repository mode.
|
|
*/
|
|
async function loadRepositoryStatuses() {
|
|
if (statusMode !== "repository") {
|
|
repositoryStatusByDashboardId = {};
|
|
return;
|
|
}
|
|
|
|
const requestId = repositoryStatusRequestId + 1;
|
|
repositoryStatusRequestId = requestId;
|
|
|
|
const visibleDashboards = paginatedDashboards;
|
|
const visibleIds = visibleDashboards.map((dashboard) => dashboard.id);
|
|
|
|
if (visibleDashboards.length === 0) {
|
|
repositoryStatusByDashboardId = {};
|
|
return;
|
|
}
|
|
|
|
const missingIds = visibleIds.filter((dashboardId) => {
|
|
const token = repositoryStatusByDashboardId[dashboardId];
|
|
return token === undefined || token === "loading";
|
|
});
|
|
|
|
if (missingIds.length === 0) {
|
|
return;
|
|
}
|
|
|
|
const loadingStatuses = { ...repositoryStatusByDashboardId };
|
|
missingIds.forEach((dashboardId) => {
|
|
loadingStatuses[dashboardId] = "loading";
|
|
});
|
|
repositoryStatusByDashboardId = loadingStatuses;
|
|
|
|
let entries: Array<readonly [number, string]> = [];
|
|
try {
|
|
const batchResult = await gitService.getStatusesBatch(missingIds);
|
|
const statusesByDashboardId = batchResult?.statuses || {};
|
|
entries = missingIds.map((dashboardId) => {
|
|
const status =
|
|
statusesByDashboardId[dashboardId] ??
|
|
statusesByDashboardId[String(dashboardId)];
|
|
if (!status) return [dashboardId, "error"] as const;
|
|
return [dashboardId, resolveRepositoryStatusToken(status)] as const;
|
|
});
|
|
} catch (error) {
|
|
entries = missingIds.map((dashboardId) => [dashboardId, "error"] as const);
|
|
}
|
|
|
|
if (requestId !== repositoryStatusRequestId) return;
|
|
repositoryStatusByDashboardId = {
|
|
...repositoryStatusByDashboardId,
|
|
...Object.fromEntries(entries),
|
|
};
|
|
}
|
|
// #endregion loadRepositoryStatuses:Function
|
|
|
|
// #region runBulkGitAction:Function [TYPE Function]
|
|
// @PURPOSE: Executes git action for selected dashboards with limited parallelism.
|
|
async function runBulkGitAction(
|
|
actionToken: string,
|
|
action: (dashboardId: number) => Promise<void>,
|
|
): Promise<void> {
|
|
if (bulkActionRunning) return;
|
|
const selectedDashboardIds = selectedIds.filter((dashboardId) =>
|
|
isRepositoryReady(dashboardId),
|
|
);
|
|
|
|
if (selectedDashboardIds.length === 0) {
|
|
addToast($t.git?.no_repositories_selected, "error");
|
|
return;
|
|
}
|
|
|
|
bulkActionRunning = true;
|
|
const concurrency = 3;
|
|
const idsQueue = [...selectedDashboardIds];
|
|
let successCount = 0;
|
|
let failedCount = 0;
|
|
|
|
const worker = async () => {
|
|
while (idsQueue.length > 0) {
|
|
const dashboardId = idsQueue.shift();
|
|
if (dashboardId === undefined) break;
|
|
try {
|
|
await action(dashboardId);
|
|
successCount += 1;
|
|
} catch (_error) {
|
|
failedCount += 1;
|
|
}
|
|
}
|
|
};
|
|
|
|
try {
|
|
await Promise.all(
|
|
Array.from({ length: Math.min(concurrency, selectedDashboardIds.length) }, () => worker()),
|
|
);
|
|
invalidateRepositoryStatuses(selectedDashboardIds);
|
|
const actionLabel = $t.git?.[`bulk_action_${actionToken}`] || actionToken;
|
|
addToast(
|
|
$t.git?.bulk_result
|
|
.replace("{action}", actionLabel)
|
|
.replace("{success}", String(successCount))
|
|
.replace("{failed}", String(failedCount)),
|
|
failedCount > 0 ? "warning" : "success",
|
|
);
|
|
} finally {
|
|
bulkActionRunning = false;
|
|
}
|
|
}
|
|
// #endregion runBulkGitAction:Function
|
|
|
|
// #region handleBulkSync:Function [TYPE Function]
|
|
async function handleBulkSync(): Promise<void> {
|
|
await runBulkGitAction("sync", (dashboardId) => gitService.sync(dashboardId, null, envId));
|
|
}
|
|
// #endregion handleBulkSync:Function
|
|
|
|
// #region handleBulkCommit:Function [TYPE Function]
|
|
async function handleBulkCommit(): Promise<void> {
|
|
const message = prompt($t.git?.commit_message);
|
|
if (!message?.trim()) return;
|
|
await runBulkGitAction("commit", (dashboardId) =>
|
|
gitService.commit(dashboardId, message.trim(), [], envId),
|
|
);
|
|
}
|
|
// #endregion handleBulkCommit:Function
|
|
|
|
// #region handleBulkPull:Function [TYPE Function]
|
|
async function handleBulkPull(): Promise<void> {
|
|
await runBulkGitAction("pull", (dashboardId) => gitService.pull(dashboardId, envId));
|
|
}
|
|
// #endregion handleBulkPull:Function
|
|
|
|
// #region handleBulkPush:Function [TYPE Function]
|
|
async function handleBulkPush(): Promise<void> {
|
|
await runBulkGitAction("push", (dashboardId) => gitService.push(dashboardId, envId));
|
|
}
|
|
// #endregion handleBulkPush:Function
|
|
|
|
// #region handleBulkDelete:Function [TYPE Function]
|
|
// @PURPOSE: Removes selected repositories from storage and binding table.
|
|
async function handleBulkDelete(): Promise<void> {
|
|
if (!confirm($t.git?.confirm_delete_repo || "Удалить выбранные репозитории?")) return;
|
|
const idsToDelete = [...selectedIds];
|
|
await runBulkGitAction("delete", (dashboardId) =>
|
|
gitService.deleteRepository(dashboardId, envId),
|
|
);
|
|
dashboards = dashboards.filter((dashboard) => !idsToDelete.includes(dashboard.id));
|
|
selectedIds = [];
|
|
}
|
|
// #endregion handleBulkDelete:Function
|
|
|
|
// #region handleManageSelected:Function [TYPE Function]
|
|
// @PURPOSE: Opens Git manager for exactly one selected dashboard.
|
|
async function handleManageSelected(): Promise<void> {
|
|
if (selectedIds.length !== 1) {
|
|
addToast($t.git?.select_single_for_manage, "warning");
|
|
return;
|
|
}
|
|
|
|
const selectedDashboardId = selectedIds[0];
|
|
const selectedDashboard = dashboards.find(
|
|
(dashboard) => dashboard.id === selectedDashboardId,
|
|
);
|
|
openGitManagerForDashboard(selectedDashboard || null);
|
|
}
|
|
// #endregion handleManageSelected:Function
|
|
|
|
// #region resolveDashboardRef:Function [TYPE Function]
|
|
// @PURPOSE: Resolves dashboard slug from payload fields.
|
|
// @PRE: Dashboard metadata is provided.
|
|
// @POST: Returns slug string or null if unavailable.
|
|
function resolveDashboardRef(dashboard: DashboardMetadata): string | null {
|
|
const directSlug = String(
|
|
dashboard.slug ||
|
|
dashboard.dashboard_slug ||
|
|
dashboard.url_slug ||
|
|
"",
|
|
).trim();
|
|
if (directSlug) return directSlug;
|
|
|
|
const dashboardUrl = String(dashboard.url || "").trim();
|
|
if (!dashboardUrl) return null;
|
|
const slugMatch = dashboardUrl.match(/\/dashboard\/([^/?#]+)/i);
|
|
if (!slugMatch?.[1]) return null;
|
|
return decodeURIComponent(slugMatch[1]);
|
|
}
|
|
// #endregion resolveDashboardRef:Function
|
|
|
|
// #region openGitManagerForDashboard:Function [TYPE Function]
|
|
// @PURPOSE: Opens Git manager for provided dashboard metadata.
|
|
function openGitManagerForDashboard(dashboard: DashboardMetadata | null): void {
|
|
if (!dashboard) return;
|
|
const dashboardRef = resolveDashboardRef(dashboard);
|
|
if (!dashboardRef) {
|
|
addToast($t.git?.select_dashboard_with_slug || "Dashboard slug is required to open GitManager", "error");
|
|
return;
|
|
}
|
|
gitDashboardId = dashboardRef;
|
|
gitDashboardTitle = dashboard.title || "";
|
|
showGitManager = true;
|
|
}
|
|
// #endregion openGitManagerForDashboard:Function
|
|
|
|
// #region handleInitializeRepositories:Function [TYPE Function]
|
|
// @PURPOSE: Opens Git manager from bulk actions to initialize selected repository.
|
|
async function handleInitializeRepositories(): Promise<void> {
|
|
if (selectedIds.length !== 1) {
|
|
addToast($t.git?.select_single_for_manage, "warning");
|
|
return;
|
|
}
|
|
const selectedDashboardId = selectedIds[0];
|
|
const selectedDashboard = dashboards.find((dashboard) => dashboard.id === selectedDashboardId) || null;
|
|
openGitManagerForDashboard(selectedDashboard);
|
|
}
|
|
// #endregion handleInitializeRepositories:Function
|
|
|
|
// #region getSortStatusValue:Function [TYPE Function]
|
|
/**
|
|
* @purpose Returns sort value for status column based on mode.
|
|
*/
|
|
function getSortStatusValue(dashboard: DashboardMetadata): string {
|
|
if (statusMode === "repository") {
|
|
return getRepositoryStatusToken(dashboard.id);
|
|
}
|
|
return String(dashboard.status || "").toLowerCase();
|
|
}
|
|
// #endregion getSortStatusValue:Function
|
|
|
|
// #region getStatusLabel:Function [TYPE Function]
|
|
/**
|
|
* @purpose Returns localized label for status column.
|
|
*/
|
|
function getStatusLabel(dashboard: DashboardMetadata): string {
|
|
if (statusMode !== "repository") {
|
|
return String(dashboard.status || "");
|
|
}
|
|
const token = getRepositoryStatusToken(dashboard.id);
|
|
return $t.git?.repo_status?.[token] || token;
|
|
}
|
|
// #endregion getStatusLabel:Function
|
|
|
|
// #region getStatusBadgeClass:Function [TYPE Function]
|
|
/**
|
|
* @purpose Returns badge style for status column.
|
|
*/
|
|
function getStatusBadgeClass(dashboard: DashboardMetadata): string {
|
|
if (statusMode !== "repository") {
|
|
return dashboard.status === "published"
|
|
? "bg-green-100 text-green-800"
|
|
: "bg-gray-100 text-gray-800";
|
|
}
|
|
|
|
const token = getRepositoryStatusToken(dashboard.id);
|
|
if (token === "loading") return "bg-slate-100 text-slate-500";
|
|
if (token === "no_repo") return "bg-gray-100 text-gray-800";
|
|
if (token === "synced") return "bg-green-100 text-green-800";
|
|
if (token === "changes") return "bg-amber-100 text-amber-800";
|
|
if (token === "behind_remote") return "bg-blue-100 text-blue-800";
|
|
if (token === "ahead_remote") return "bg-indigo-100 text-indigo-800";
|
|
if (token === "diverged") return "bg-purple-100 text-purple-800";
|
|
return "bg-rose-100 text-rose-800";
|
|
}
|
|
// #endregion getStatusBadgeClass:Function
|
|
|
|
$effect(() => {
|
|
dashboards;
|
|
statusMode;
|
|
currentPage;
|
|
pageSize;
|
|
filterText;
|
|
sortColumn;
|
|
sortDirection;
|
|
untrack(() => {
|
|
void loadRepositoryStatuses();
|
|
});
|
|
});
|
|
</script>
|
|
|
|
<!-- [SECTION: TEMPLATE] -->
|
|
<div class="dashboard-grid">
|
|
<!-- Filter Input -->
|
|
<div class="mb-6">
|
|
<Input bind:value={filterText} placeholder={$t.dashboard.search} />
|
|
</div>
|
|
|
|
{#if selectedIds.length > 0}
|
|
<div class="mb-4 flex flex-wrap items-center gap-2 rounded-lg border border-blue-100 bg-primary-light/60 px-3 py-2">
|
|
{#if !repositoriesOnly}
|
|
<Button
|
|
size="sm"
|
|
variant="secondary"
|
|
onclick={handleManageSelected}
|
|
disabled={bulkActionRunning || selectedIds.length !== 1}
|
|
class="border-blue-200 bg-white text-blue-700 hover:bg-blue-50 disabled:opacity-40"
|
|
>
|
|
{$t.git?.manage_selected}
|
|
</Button>
|
|
<Button
|
|
size="sm"
|
|
variant="secondary"
|
|
onclick={handleInitializeRepositories}
|
|
disabled={bulkActionRunning || selectedIds.length !== 1}
|
|
class="border-slate-200 bg-white text-slate-700 hover:bg-slate-50 disabled:opacity-40"
|
|
>
|
|
{$t.git?.init_repo || "Инициализировать Git-репозиторий"}
|
|
</Button>
|
|
{/if}
|
|
<Button size="sm" variant="secondary" onclick={handleBulkSync} disabled={bulkActionRunning} class="border-blue-200 bg-white text-blue-700 hover:bg-blue-50">
|
|
{$t.git?.bulk_sync}
|
|
</Button>
|
|
{#if !repositoriesOnly}
|
|
<Button size="sm" variant="secondary" onclick={handleBulkCommit} disabled={bulkActionRunning} class="border-amber-200 bg-white text-amber-700 hover:bg-amber-50">
|
|
{$t.git?.bulk_commit}
|
|
</Button>
|
|
{/if}
|
|
<Button size="sm" variant="secondary" onclick={handleBulkPull} disabled={bulkActionRunning} class="border-cyan-200 bg-white text-cyan-700 hover:bg-cyan-50">
|
|
{$t.git?.bulk_pull}
|
|
</Button>
|
|
<Button size="sm" variant="secondary" onclick={handleBulkPush} disabled={bulkActionRunning} class="border-indigo-200 bg-white text-indigo-700 hover:bg-indigo-50">
|
|
{$t.git?.bulk_push}
|
|
</Button>
|
|
{#if repositoriesOnly}
|
|
<Button size="sm" variant="secondary" onclick={handleBulkDelete} disabled={bulkActionRunning} class="border-rose-200 bg-white text-rose-700 hover:bg-rose-50">
|
|
{$t.git?.delete_repo || "Удалить репозиторий"}
|
|
</Button>
|
|
{/if}
|
|
<span class="ml-1 text-xs font-medium text-slate-600">
|
|
{$t.git?.selected_count.replace(
|
|
"{count}",
|
|
String(selectedIds.length),
|
|
)}
|
|
</span>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Grid/Table -->
|
|
<div class="overflow-x-auto rounded-lg border border-gray-200">
|
|
<table class="min-w-full divide-y divide-gray-200">
|
|
<thead class="bg-gray-50">
|
|
<tr>
|
|
<th class="px-6 py-3 text-left">
|
|
<input
|
|
type="checkbox"
|
|
checked={allSelected}
|
|
indeterminate={someSelected && !allSelected}
|
|
onchange={(e) =>
|
|
handleSelectAll((e.target as HTMLInputElement).checked)}
|
|
class="h-4 w-4 text-primary border-gray-300 rounded focus-visible:ring-primary-ring"
|
|
/>
|
|
</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"
|
|
onclick={() => handleSort("title")}
|
|
>
|
|
{$t.dashboard.title}
|
|
{sortColumn === "title"
|
|
? sortDirection === "asc"
|
|
? "↑"
|
|
: "↓"
|
|
: ""}
|
|
</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"
|
|
onclick={() => handleSort("last_modified")}
|
|
>
|
|
{$t.dashboard.last_modified}
|
|
{sortColumn === "last_modified"
|
|
? sortDirection === "asc"
|
|
? "↑"
|
|
: "↓"
|
|
: ""}
|
|
</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"
|
|
onclick={() => handleSort("status")}
|
|
>
|
|
{$t.dashboard.status}
|
|
{sortColumn === "status"
|
|
? sortDirection === "asc"
|
|
? "↑"
|
|
: "↓"
|
|
: ""}
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="bg-white divide-y divide-gray-200">
|
|
{#each paginatedDashboards as dashboard (dashboard.id)}
|
|
<tr class="hover:bg-gray-50 transition-colors">
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
<input
|
|
type="checkbox"
|
|
checked={selectedIds.includes(dashboard.id)}
|
|
onchange={(e) =>
|
|
handleSelectionChange(
|
|
dashboard.id,
|
|
(e.target as HTMLInputElement).checked,
|
|
)}
|
|
class="h-4 w-4 text-primary border-gray-300 rounded focus-visible:ring-primary-ring"
|
|
/>
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
|
<button
|
|
type="button"
|
|
class="text-left text-blue-700 hover:text-blue-900 hover:underline"
|
|
onclick={() => openGitManagerForDashboard(dashboard)}
|
|
>
|
|
{dashboard.title}
|
|
</button>
|
|
</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">
|
|
<span
|
|
class="px-2 py-1 text-xs font-medium rounded-full {getStatusBadgeClass(dashboard)}"
|
|
>
|
|
{getStatusLabel(dashboard)}
|
|
</span>
|
|
</td>
|
|
</tr>
|
|
{/each}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Pagination Controls -->
|
|
<div class="flex items-center justify-between mt-6">
|
|
<div class="text-sm text-gray-500">
|
|
{($t.dashboard?.showing )
|
|
.replace("{start}", (currentPage * pageSize + 1).toString())
|
|
.replace(
|
|
"{end}",
|
|
Math.min(
|
|
(currentPage + 1) * pageSize,
|
|
sortedDashboards.length,
|
|
).toString(),
|
|
)
|
|
.replace("{total}", sortedDashboards.length.toString())}
|
|
</div>
|
|
<div class="flex gap-2">
|
|
<Button
|
|
variant="secondary"
|
|
size="sm"
|
|
disabled={currentPage === 0}
|
|
onclick={() => goToPage(currentPage - 1)}
|
|
>
|
|
{$t.dashboard.previous}
|
|
</Button>
|
|
<Button
|
|
variant="secondary"
|
|
size="sm"
|
|
disabled={currentPage >= totalPages - 1}
|
|
onclick={() => goToPage(currentPage + 1)}
|
|
>
|
|
{$t.dashboard.next}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{#if showGitManager && gitDashboardId}
|
|
<GitManager
|
|
dashboardId={gitDashboardId}
|
|
envId={envId}
|
|
dashboardTitle={gitDashboardTitle}
|
|
bind:show={showGitManager}
|
|
/>
|
|
{/if}
|
|
|
|
<!-- [/SECTION] -->
|
|
|
|
<!-- #endregion RepositoryDashboardGrid -->
|