garm/webapp/src/routes/scalesets/+page.svelte
Gabriel Adrian Samfira 6fee10c737 Fix double creation of pools
This change fixes the creation of pools though the UI. Both the modal and
the page were sending a request to create the pool, leading to double pool.

Signed-off-by: Gabriel Adrian Samfira <gsamfira@cloudbasesolutions.com>
2025-08-25 16:12:01 +00:00

318 lines
No EOL
8.4 KiB
Svelte

<script lang="ts">
import { onMount } from 'svelte';
import { garmApi } from '$lib/api/client.js';
import type { ScaleSet, CreateScaleSetParams } from '$lib/api/generated/api.js';
import PageHeader from '$lib/components/PageHeader.svelte';
import CreateScaleSetModal from '$lib/components/CreateScaleSetModal.svelte';
import UpdateScaleSetModal from '$lib/components/UpdateScaleSetModal.svelte';
import DeleteModal from '$lib/components/DeleteModal.svelte';
import { eagerCache, eagerCacheManager } from '$lib/stores/eager-cache.js';
import { toastStore } from '$lib/stores/toast.js';
import { getEntityName, filterEntities } from '$lib/utils/common.js';
import { extractAPIError } from '$lib/utils/apiError';
import DataTable from '$lib/components/DataTable.svelte';
import { EntityCell, EndpointCell, StatusCell, ActionsCell, GenericCell, PoolEntityCell } from '$lib/components/cells';
let scaleSets: ScaleSet[] = [];
let loading = true;
let error = '';
// Subscribe to eager cache for scale sets (when WebSocket is connected)
$: {
// Only use cache data if we're not in direct API mode
if (!scaleSets.length || $eagerCache.loaded.scalesets) {
scaleSets = $eagerCache.scalesets;
}
}
$: loading = $eagerCache.loading.scalesets;
$: cacheError = $eagerCache.errorMessages.scalesets;
let searchTerm = '';
let currentPage = 1;
let perPage = 25;
let showCreateModal = false;
let showUpdateModal = false;
let showDeleteModal = false;
let selectedScaleSet: ScaleSet | null = null;
// Filtered and paginated data
$: filteredScaleSets = filterEntities(scaleSets, searchTerm, (scaleSet) => getEntityName(scaleSet));
$: totalPages = Math.ceil(filteredScaleSets.length / perPage);
$: {
if (currentPage > totalPages && totalPages > 0) {
currentPage = totalPages;
}
}
$: paginatedScaleSets = filteredScaleSets.slice(
(currentPage - 1) * perPage,
currentPage * perPage
);
async function handleCreateScaleSet(params: CreateScaleSetParams) {
try {
error = '';
// The actual creation will be handled by the modal based on entity type
// No need to reload - eager cache websocket will handle the update
showCreateModal = false;
// Note: We don't have access to the created scale set data here
toastStore.success(
'Scale Set Created',
'Scale set has been created successfully.'
);
} catch (err) {
error = extractAPIError(err);
throw err; // Let the modal handle the error
}
}
async function handleUpdateScaleSet(params: Partial<CreateScaleSetParams>) {
if (!selectedScaleSet) return;
try {
await garmApi.updateScaleSet(selectedScaleSet.id!, params);
// No need to reload - eager cache websocket will handle the update
toastStore.success(
'Scale Set Updated',
`Scale set ${selectedScaleSet.name} has been updated successfully.`
);
showUpdateModal = false;
selectedScaleSet = null;
} catch (err) {
throw err; // Let the modal handle the error
}
}
async function handleDeleteScaleSet() {
if (!selectedScaleSet) return;
try {
await garmApi.deleteScaleSet(selectedScaleSet.id!);
// No need to reload - websocket will handle the update
toastStore.success(
'Scale Set Deleted',
`Scale set ${selectedScaleSet.name} has been deleted successfully.`
);
} catch (err) {
const errorMessage = extractAPIError(err);
toastStore.error('Delete Failed', errorMessage);
} finally {
showDeleteModal = false;
selectedScaleSet = null;
}
}
function openCreateModal() {
showCreateModal = true;
}
function openUpdateModal(scaleSet: ScaleSet) {
selectedScaleSet = scaleSet;
showUpdateModal = true;
}
function openDeleteModal(scaleSet: ScaleSet) {
selectedScaleSet = scaleSet;
showDeleteModal = true;
}
onMount(async () => {
// Load scale sets through eager cache (priority load + background load others)
try {
loading = true;
const scaleSetData = await eagerCacheManager.getScaleSets();
// If WebSocket is disconnected, getScaleSets returns direct API data
// Update our local scaleSets array with this data
if (scaleSetData && Array.isArray(scaleSetData)) {
scaleSets = scaleSetData;
}
} catch (err) {
// Cache error is already handled by the eager cache system
// We don't need to set error here anymore since it's in the cache state
if (!import.meta.env?.VITEST) {
console.error('Failed to load scale sets:', err);
}
error = extractAPIError(err);
} finally {
loading = false;
}
});
async function retryLoadScaleSets() {
try {
await eagerCacheManager.retryResource('scalesets');
} catch (err) {
console.error('Retry failed:', err);
}
}
// DataTable configuration
const columns = [
{
key: 'name',
title: 'Name',
cellComponent: EntityCell,
cellProps: { entityType: 'scaleset' }
},
{
key: 'image',
title: 'Image',
cellComponent: GenericCell,
cellProps: {
field: 'image',
type: 'code',
showTitle: true
}
},
{
key: 'provider',
title: 'Provider',
cellComponent: GenericCell,
cellProps: { field: 'provider_name' }
},
{
key: 'flavor',
title: 'Flavor',
cellComponent: GenericCell,
cellProps: { field: 'flavor' }
},
{
key: 'entity',
title: 'Entity',
cellComponent: PoolEntityCell
},
{
key: 'endpoint',
title: 'Endpoint',
cellComponent: EndpointCell
},
{
key: 'status',
title: 'Status',
cellComponent: StatusCell,
cellProps: { statusType: 'enabled' }
},
{
key: 'actions',
title: 'Actions',
align: 'right' as const,
cellComponent: ActionsCell
}
];
// Mobile card configuration
const mobileCardConfig = {
entityType: 'scaleset' as const,
primaryText: {
field: 'name',
isClickable: true,
href: '/scalesets/{id}'
},
secondaryText: {
field: 'entity_name',
computedValue: (item: any) => getEntityName(item)
},
badges: [
{
type: 'custom' as const,
value: (item: any) => ({
variant: item.enabled ? 'success' : 'error',
text: item.enabled ? 'Enabled' : 'Disabled'
})
}
],
actions: [
{
type: 'edit' as const,
handler: (item: any) => openUpdateModal(item)
},
{
type: 'delete' as const,
handler: (item: any) => openDeleteModal(item)
}
]
};
function handleTableSearch(event: CustomEvent<{ term: string }>) {
searchTerm = event.detail.term;
currentPage = 1;
}
function handleTablePageChange(event: CustomEvent<{ page: number }>) {
currentPage = event.detail.page;
}
function handleTablePerPageChange(event: CustomEvent<{ perPage: number }>) {
perPage = event.detail.perPage;
currentPage = 1;
}
function handleEdit(event: CustomEvent<{ item: any }>) {
openUpdateModal(event.detail.item);
}
function handleDelete(event: CustomEvent<{ item: any }>) {
openDeleteModal(event.detail.item);
}
// Scale sets are now handled by eager cache with websocket subscriptions
</script>
<svelte:head>
<title>Scale Sets - GARM</title>
</svelte:head>
<div class="space-y-6">
<!-- Header -->
<PageHeader
title="Scale Sets"
description="Manage GitHub runner scale sets"
actionLabel="Add Scale Set"
on:action={openCreateModal}
/>
<DataTable
{columns}
data={paginatedScaleSets}
{loading}
error={cacheError || error}
{searchTerm}
searchPlaceholder="Search by entity name..."
{currentPage}
{perPage}
{totalPages}
totalItems={filteredScaleSets.length}
itemName="scale sets"
emptyIconType="cog"
showRetry={!!cacheError}
{mobileCardConfig}
on:search={handleTableSearch}
on:pageChange={handleTablePageChange}
on:perPageChange={handleTablePerPageChange}
on:retry={retryLoadScaleSets}
on:edit={handleEdit}
on:delete={handleDelete}
/>
</div>
<!-- Modals -->
{#if showCreateModal}
<CreateScaleSetModal
on:close={() => showCreateModal = false}
on:submit={(e) => handleCreateScaleSet(e.detail)}
/>
{/if}
{#if showUpdateModal && selectedScaleSet}
<UpdateScaleSetModal
scaleSet={selectedScaleSet}
on:close={() => { showUpdateModal = false; selectedScaleSet = null; }}
on:submit={(e) => handleUpdateScaleSet(e.detail)}
/>
{/if}
{#if showDeleteModal && selectedScaleSet}
<DeleteModal
title="Delete Scale Set"
message="Are you sure you want to delete this scale set? This action cannot be undone and will remove all associated runners."
itemName={`Scale Set ${selectedScaleSet.name} (${getEntityName(selectedScaleSet)})`}
on:close={() => { showDeleteModal = false; selectedScaleSet = null; }}
on:confirm={handleDeleteScaleSet}
/>
{/if}