fix reactive table error

This commit is contained in:
d34dscene
2025-02-01 00:17:46 +01:00
parent 614db97e7e
commit f1ff7d29ec
7 changed files with 261 additions and 247 deletions
+21 -7
View File
@@ -8,6 +8,7 @@ import (
"github.com/MizuchiLabs/mantrae/internal/db"
"github.com/MizuchiLabs/mantrae/internal/source"
"github.com/MizuchiLabs/mantrae/internal/traefik"
"github.com/MizuchiLabs/mantrae/internal/util"
)
@@ -45,19 +46,19 @@ func GetProfile(DB *sql.DB) http.HandlerFunc {
func CreateProfile(DB *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
q := db.New(DB)
var profile db.CreateProfileParams
if err := json.NewDecoder(r.Body).Decode(&profile); err != nil {
var params db.CreateProfileParams
if err := json.NewDecoder(r.Body).Decode(&params); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
profileID, err := q.CreateProfile(r.Context(), profile)
profileID, err := q.CreateProfile(r.Context(), params)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Create default local config
if err := q.UpsertTraefikConfig(r.Context(), db.UpsertTraefikConfigParams{
if err = q.UpsertTraefikConfig(r.Context(), db.UpsertTraefikConfigParams{
ProfileID: profileID,
Source: source.Local,
}); err != nil {
@@ -65,6 +66,12 @@ func CreateProfile(DB *sql.DB) http.HandlerFunc {
return
}
profile, err := q.GetProfile(r.Context(), profileID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
go traefik.UpdateTraefikAPI(DB, profile)
util.Broadcast <- util.EventMessage{
Type: util.EventTypeCreate,
Message: "profile",
@@ -76,15 +83,22 @@ func CreateProfile(DB *sql.DB) http.HandlerFunc {
func UpdateProfile(DB *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
q := db.New(DB)
var profile db.UpdateProfileParams
if err := json.NewDecoder(r.Body).Decode(&profile); err != nil {
var params db.UpdateProfileParams
if err := json.NewDecoder(r.Body).Decode(&params); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err := q.UpdateProfile(r.Context(), profile); err != nil {
if err := q.UpdateProfile(r.Context(), params); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
profile, err := q.GetProfile(r.Context(), params.ID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
go traefik.UpdateTraefikAPI(DB, profile)
util.Broadcast <- util.EventMessage{
Type: util.EventTypeUpdate,
Message: "profile",
+82 -60
View File
@@ -23,6 +23,86 @@ const (
VersionAPI = "/api/version"
)
func UpdateTraefikAPI(DB *sql.DB, profile db.Profile) error {
rawResponse, err := fetch(profile, RawAPI)
if err != nil {
slog.Error("Failed to fetch raw data", "error", err)
// Clear api data
if err = ClearTraefikAPI(DB, profile.ID); err != nil {
slog.Error("Failed to update api data", "error", err)
}
return err
}
defer rawResponse.Close()
var config db.TraefikConfiguration
if err = json.NewDecoder(rawResponse).Decode(&config); err != nil {
return fmt.Errorf("failed to decode raw data: %w", err)
}
epResponse, err := fetch(profile, EntrypointsAPI)
if err != nil {
return fmt.Errorf("failed to fetch %s: %w", profile.Url+EntrypointsAPI, err)
}
defer epResponse.Close()
var entrypoints db.TraefikEntryPoints
if err = json.NewDecoder(epResponse).Decode(&entrypoints); err != nil {
return fmt.Errorf("failed to decode entrypoints: %w", err)
}
oResponse, err := fetch(profile, OverviewAPI)
if err != nil {
return fmt.Errorf("failed to fetch %s: %w", profile.Url+OverviewAPI, err)
}
defer epResponse.Close()
var overview db.TraefikOverview
if err = json.NewDecoder(oResponse).Decode(&overview); err != nil {
return fmt.Errorf("failed to decode overview: %w", err)
}
vResponse, err := fetch(profile, VersionAPI)
if err != nil {
return fmt.Errorf("failed to fetch %s: %w", profile.Url+VersionAPI, err)
}
defer vResponse.Close()
var version db.TraefikVersion
if err := json.NewDecoder(vResponse).Decode(&version); err != nil {
return fmt.Errorf("failed to decode version: %w", err)
}
q := db.New(DB)
if err := q.UpsertTraefikConfig(context.Background(), db.UpsertTraefikConfigParams{
ProfileID: profile.ID,
Entrypoints: &entrypoints,
Overview: &overview,
Version: &version.Version,
Config: &config,
Source: source.API,
}); err != nil {
return fmt.Errorf("failed to update api data: %w", err)
}
return nil
}
func ClearTraefikAPI(DB *sql.DB, profileID int64) error {
q := db.New(DB)
if err := q.UpsertTraefikConfig(context.Background(), db.UpsertTraefikConfigParams{
ProfileID: profileID,
Source: source.API,
Entrypoints: nil,
Overview: nil,
Version: nil,
Config: nil,
}); err != nil {
return fmt.Errorf("failed to update api data: %w", err)
}
return nil
}
func GetTraefikConfig(DB *sql.DB) {
q := db.New(DB)
profiles, err := q.ListProfiles(context.Background())
@@ -36,66 +116,8 @@ func GetTraefikConfig(DB *sql.DB) {
continue
}
rawResponse, err := fetch(profile, RawAPI)
if err != nil {
slog.Error("Failed to fetch raw data", "error", err)
continue
}
defer rawResponse.Close()
var config db.TraefikConfiguration
if err := json.NewDecoder(rawResponse).Decode(&config); err != nil {
slog.Error("Failed to decode raw data", "error", err)
continue
}
epResponse, err := fetch(profile, EntrypointsAPI)
if err != nil {
slog.Error("Failed to fetch raw data", "error", err)
continue
}
defer epResponse.Close()
var entrypoints db.TraefikEntryPoints
if err := json.NewDecoder(epResponse).Decode(&entrypoints); err != nil {
slog.Error("Failed to decode raw data", "error", err)
continue
}
oResponse, err := fetch(profile, OverviewAPI)
if err != nil {
slog.Error("Failed to fetch raw data", "error", err)
continue
}
defer epResponse.Close()
var overview db.TraefikOverview
if err := json.NewDecoder(oResponse).Decode(&overview); err != nil {
slog.Error("Failed to decode raw data", "error", err)
continue
}
vResponse, err := fetch(profile, VersionAPI)
if err != nil {
slog.Error("Failed to fetch raw data", "error", err)
continue
}
defer vResponse.Close()
var version db.TraefikVersion
if err := json.NewDecoder(vResponse).Decode(&version); err != nil {
slog.Error("Failed to decode raw data", "error", err)
continue
}
if err := q.UpsertTraefikConfig(context.Background(), db.UpsertTraefikConfigParams{
ProfileID: profile.ID,
Entrypoints: &entrypoints,
Overview: &overview,
Version: &version.Version,
Config: &config,
Source: source.API,
}); err != nil {
slog.Error("Failed to update Traefik config", "error", err)
if err := UpdateTraefikAPI(DB, profile); err != nil {
slog.Error("Failed to update api data", "error", err)
continue
}
}
+10 -5
View File
@@ -214,8 +214,11 @@ export const api = {
// Profiles ------------------------------------------------------------------
async listProfiles() {
const data = await send('/profile');
const data: Profile[] = await send('/profile');
profiles.set(data);
if (profile.isValid()) {
profile.value = data.find((item) => item.id === profile.value?.id) ?? data[0];
}
},
async getProfile(id: number) {
@@ -230,12 +233,15 @@ export const api = {
await api.listProfiles(); // Refresh the list
},
async updateProfile(profile: Profile) {
async updateProfile(p: Profile) {
await send('/profile', {
method: 'PUT',
body: profile
body: p
});
await api.listProfiles(); // Refresh the list
if (p.id === profile.id) {
await api.getTraefikConfig(TraefikSource.API);
}
},
async deleteProfile(id: number) {
@@ -607,9 +613,8 @@ async function fetchTraefikConfig(src: TraefikSource) {
toast.error('No valid profile selected');
return;
}
source.value = src;
const res = await send(`/traefik/${profile.id}/${source.value}`);
const res = await send(`/traefik/${profile.id}/${src}`);
if (!res) {
// Reset stores
traefik.set([]);
+11 -11
View File
@@ -93,13 +93,13 @@
<div class="grid grid-cols-4 items-center gap-2 text-sm">
<span class="col-span-1">Features</span>
<div class="col-span-3 space-x-2">
{#if $overview?.features.tracing}
{#if $overview?.features?.tracing}
<Badge variant="secondary">Tracing</Badge>
{/if}
{#if $overview?.features.metrics}
{#if $overview?.features?.metrics}
<Badge variant="secondary">Metrics</Badge>
{/if}
{#if $overview?.features.accessLog}
{#if $overview?.features?.accessLog}
<Badge variant="secondary">Access Log</Badge>
{/if}
</div>
@@ -126,13 +126,13 @@
<span class="col-span-1 font-mono">HTTP</span>
<div class="col-span-3 space-x-2">
<Badge variant="secondary">
Routers: {$overview?.http.routers.total ?? 0}
Routers: {$overview?.http?.routers.total ?? 0}
</Badge>
<Badge variant="secondary">
Services: {$overview?.http.services.total ?? 0}
Services: {$overview?.http?.services.total ?? 0}
</Badge>
<Badge variant="secondary">
Middlewares: {$overview?.http.middlewares.total ?? 0}
Middlewares: {$overview?.http?.middlewares.total ?? 0}
</Badge>
</div>
</div>
@@ -142,13 +142,13 @@
<span class="col-span-1 font-mono">TCP</span>
<div class="col-span-3 space-x-2">
<Badge variant="secondary">
Routers: {$overview?.tcp.routers.total ?? 0}
Routers: {$overview?.tcp?.routers.total ?? 0}
</Badge>
<Badge variant="secondary">
Services: {$overview?.tcp.services.total ?? 0}
Services: {$overview?.tcp?.services.total ?? 0}
</Badge>
<Badge variant="secondary">
Middlewares: {$overview?.tcp.middlewares.total ?? 0}
Middlewares: {$overview?.tcp?.middlewares.total ?? 0}
</Badge>
</div>
</div>
@@ -158,10 +158,10 @@
<span class="col-span-1 font-mono">UDP</span>
<div class="col-span-3 space-x-2">
<Badge variant="secondary">
Routers: {$overview?.udp.routers.total ?? 0}
Routers: {$overview?.udp?.routers.total ?? 0}
</Badge>
<Badge variant="secondary">
Services: {$overview?.udp.services.total ?? 0}
Services: {$overview?.udp?.services.total ?? 0}
</Badge>
</div>
</div>
+76 -67
View File
@@ -98,6 +98,7 @@
},
...columns
],
autoResetAll: true,
filterFns: {
fuzzy: (row, columnId, value, addMeta) => {
const itemRank = rankItem(row.getValue(columnId), value);
@@ -182,6 +183,12 @@
async function handleTabChange(value: string) {
if (!source.isValid(value)) return;
source.value = value;
// Reset table state
table.resetRowSelection();
table.resetColumnFilters();
table.resetGlobalFilter();
table.resetColumnOrder();
table.resetPagination();
await api.getTraefikConfig(source.value);
}
function handleLimitChange(value: string) {
@@ -248,8 +255,75 @@
{/if}
</div>
<!-- Table -->
<div class="rounded-md border">
{#key source.value + JSON.stringify(data)}
<Table.Root>
<Table.Header>
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
<Table.Row>
{#each headerGroup.headers as header (header.id)}
<Table.Head>
{#if !header.isPlaceholder}
<div class="flex items-center">
<Button
variant="ghost"
size="sm"
class="-ml-3 h-8 data-[sortable=false]:cursor-default"
data-sortable={header.column.getCanSort()}
onclick={() => header.column.toggleSorting()}
>
<FlexRender
content={header.column.columnDef.header}
context={header.getContext()}
/>
{#if header.column.getCanSort()}
{#if header.column.getIsSorted() === 'asc'}
<ArrowDown />
{:else if header.column.getIsSorted() === 'desc'}
<ArrowUp />
{/if}
{/if}
</Button>
</div>
{/if}
</Table.Head>
{/each}
</Table.Row>
{/each}
</Table.Header>
<Table.Body>
{#each table.getRowModel().rows as row (row.id)}
<Table.Row
data-state={row.getIsSelected() && 'selected'}
class={getRowClassName ? getRowClassName(row.original) : ''}
>
{#each row.getVisibleCells() as cell (cell.id)}
<Table.Cell>
<FlexRender content={cell.column.columnDef.cell} context={cell.getContext()} />
</Table.Cell>
{/each}
</Table.Row>
{:else}
<Table.Row>
<Table.Cell colspan={columns.length} class="h-24 text-center">No results.</Table.Cell>
</Table.Row>
{/each}
</Table.Body>
<Table.Footer>
<Table.Row class="border-t">
<Table.Cell colspan={columns.length}>Total</Table.Cell>
<Table.Cell class="mr-4 text-right"
>{table.getPrePaginationRowModel().rows.length}</Table.Cell
>
</Table.Row>
</Table.Footer>
</Table.Root>
{/key}
</div>
{#if table.getSelectedRowModel().rows.length > 0}
<div class="my-2 flex items-center gap-2 rounded-md bg-muted/50 p-2">
<div class="my-2 flex items-center gap-2 rounded-lg border bg-muted/50 p-2">
<span class="text-sm text-muted-foreground">
{table.getFilteredSelectedRowModel().rows.length} of{' '}
{table.getFilteredRowModel().rows.length} item(s) selected.
@@ -258,78 +332,13 @@
</div>
{/if}
<!-- Table -->
<div class="rounded-md border">
<Table.Root>
<Table.Header>
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
<Table.Row>
{#each headerGroup.headers as header (header.id)}
<Table.Head>
{#if !header.isPlaceholder}
<div class="flex items-center">
<Button
variant="ghost"
size="sm"
class="-ml-3 h-8 data-[sortable=false]:cursor-default"
data-sortable={header.column.getCanSort()}
onclick={() => header.column.toggleSorting()}
>
<FlexRender
content={header.column.columnDef.header}
context={header.getContext()}
/>
{#if header.column.getCanSort()}
{#if header.column.getIsSorted() === 'asc'}
<ArrowDown />
{:else if header.column.getIsSorted() === 'desc'}
<ArrowUp />
{/if}
{/if}
</Button>
</div>
{/if}
</Table.Head>
{/each}
</Table.Row>
{/each}
</Table.Header>
<Table.Body>
{#each table.getRowModel().rows as row (row.id)}
<Table.Row
data-state={row.getIsSelected() && 'selected'}
class={getRowClassName ? getRowClassName(row.original) : ''}
>
{#each row.getVisibleCells() as cell (cell.id)}
<Table.Cell>
<FlexRender content={cell.column.columnDef.cell} context={cell.getContext()} />
</Table.Cell>
{/each}
</Table.Row>
{:else}
<Table.Row>
<Table.Cell colspan={columns.length} class="h-24 text-center">No results.</Table.Cell>
</Table.Row>
{/each}
</Table.Body>
<Table.Footer>
<Table.Row class="border-t">
<Table.Cell colspan={columns.length}>Total</Table.Cell>
<Table.Cell class="mr-4 text-right"
>{table.getPrePaginationRowModel().rows.length}</Table.Cell
>
</Table.Row>
</Table.Footer>
</Table.Root>
</div>
<!-- Pagination -->
<div class="flex items-center justify-between py-4">
<div>
<Select.Root
type="single"
allowDeselect={false}
bind:value={limit.value}
value={limit.value ?? pagination.pageSize.toString()}
onValueChange={handleLimitChange}
>
<Select.Trigger class="w-[180px]">
+19 -40
View File
@@ -1,5 +1,4 @@
<script lang="ts">
import * as Tabs from '$lib/components/ui/tabs/index.js';
import ColumnBadge from '$lib/components/tables/ColumnBadge.svelte';
import DataTable from '$lib/components/tables/DataTable.svelte';
import MiddlewareModal from '$lib/components/modals/middleware.svelte';
@@ -158,45 +157,25 @@
<title>Middlewares</title>
</svelte:head>
<Tabs.Root value={source.value}>
<Tabs.Content value={TraefikSource.LOCAL}>
<div class="flex flex-col gap-4">
<div class="flex items-center justify-start gap-2">
<Layers />
<h1 class="text-2xl font-bold">Middleware Management</h1>
</div>
<DataTable
{columns}
data={$middlewares || []}
showSourceTabs={true}
createButton={{
label: 'Add Middleware',
onClick: openCreateModal
}}
/>
</div>
</Tabs.Content>
<Tabs.Content value={TraefikSource.API}>
<div class="flex flex-col gap-4">
<div class="flex items-center justify-start gap-2">
<Layers />
<h1 class="text-2xl font-bold">Middleware Management</h1>
</div>
<DataTable {columns} data={$middlewares || []} showSourceTabs={true} />
</div>
</Tabs.Content>
<Tabs.Content value={TraefikSource.AGENT}>
<div class="flex flex-col gap-4">
<div class="flex items-center justify-start gap-2">
<Layers />
<h1 class="text-2xl font-bold">Middleware Management</h1>
</div>
<DataTable {columns} data={$middlewares || []} showSourceTabs={true} />
</div>
</Tabs.Content>
</Tabs.Root>
<div class="flex flex-col gap-4">
<div class="flex items-center justify-start gap-2">
<Layers />
<h1 class="text-2xl font-bold">Middleware Management</h1>
</div>
{#if source.value === TraefikSource.LOCAL}
<DataTable
{columns}
data={$middlewares || []}
showSourceTabs={true}
createButton={{
label: 'Add Middleware',
onClick: openCreateModal
}}
/>
{:else}
<DataTable {columns} data={$middlewares || []} showSourceTabs={true} />
{/if}
</div>
<MiddlewareModal
bind:open={modalState.isOpen}
+42 -57
View File
@@ -1,5 +1,4 @@
<script lang="ts">
import * as Tabs from '$lib/components/ui/tabs/index.js';
import ColumnBadge from '$lib/components/tables/ColumnBadge.svelte';
import DataTable from '$lib/components/tables/DataTable.svelte';
import RouterModal from '$lib/components/modals/router.svelte';
@@ -83,32 +82,33 @@
const columns: ColumnDef<RouterWithService>[] = [
{
header: 'Name',
accessorFn: (row) => row.router.name,
accessorKey: 'router.name',
id: 'name',
enableSorting: true,
cell: ({ row }) => {
const name = row.getValue('name') as string;
return name.split('@')[0];
return name?.split('@')[0];
}
},
{
header: 'Protocol',
accessorFn: (row) => row.router.protocol,
accessorKey: 'router.protocol',
id: 'protocol',
enableSorting: true,
cell: ({ row }) => {
const protocol = row.getValue('protocol') as string;
return renderComponent(ColumnBadge, { label: protocol });
return renderComponent(ColumnBadge, {
label: row.getValue('protocol') as string
});
}
},
{
header: 'Provider',
accessorFn: (row) => row.router.name,
accessorKey: 'router.name',
id: 'provider',
enableSorting: true,
cell: ({ row }) => {
const name = row.getValue('provider') as string;
const provider = name.split('@')[1];
const provider = name?.split('@')[1];
if (!provider && source.value === TraefikSource.AGENT) {
return renderComponent(ColumnBadge, {
label: 'agent',
@@ -129,38 +129,36 @@
},
{
header: 'Entrypoints',
accessorFn: (row) => row.router.entryPoints,
accessorKey: 'router.entryPoints',
id: 'entryPoints',
enableSorting: true,
cell: ({ row }) => {
const ep = row.getValue('entryPoints') as string[];
return renderComponent(ColumnBadge, {
label: ep,
label: row.getValue('entryPoints') as string[],
variant: 'secondary'
});
}
},
{
header: 'Middlewares',
accessorFn: (row) => row.router.middlewares,
accessorKey: 'router.middlewares',
id: 'middlewares',
enableSorting: true,
cell: ({ row }) => {
const middlewares = row.getValue('middlewares') as string[];
return renderComponent(ColumnBadge, {
label: middlewares,
label: row.getValue('middlewares') as string[],
variant: 'secondary'
});
}
},
{
header: 'Cert Resolver',
accessorFn: (row) => row.router.tls,
id: 'resolver',
accessorKey: 'router.tls',
id: 'tls',
enableSorting: true,
cell: ({ row }) => {
const resolver = row.getValue('resolver') as TLS;
if (!resolver?.certResolver) {
const tls = row.getValue('tls') as TLS;
if (!tls?.certResolver) {
return renderComponent(ColumnBadge, {
label: 'None',
variant: 'secondary',
@@ -168,7 +166,7 @@
});
}
return renderComponent(ColumnBadge, {
label: resolver.certResolver as string,
label: tls.certResolver as string,
variant: 'secondary',
class: 'bg-slate-300 dark:bg-slate-700'
});
@@ -181,7 +179,12 @@
enableSorting: true,
cell: ({ row }) => {
const status = row.getValue('serverStatus') as Record<string, string>;
if (!status) return renderComponent(ColumnBadge, { label: 'N/A', variant: 'secondary' });
if (status === undefined) {
return renderComponent(ColumnBadge, {
label: 'N/A',
variant: 'secondary'
});
}
const upCount = Object.values(status).filter((status) => status === 'UP').length;
const totalCount = Object.values(status).length;
const greenBadge = 'bg-green-300 dark:bg-green-600';
@@ -243,43 +246,25 @@
<title>Routers</title>
</svelte:head>
<Tabs.Root value={source.value}>
<Tabs.Content value={TraefikSource.LOCAL}>
<div class="flex flex-col gap-4">
<div class="flex items-center justify-start gap-2">
<Route />
<h1 class="text-2xl font-bold">Router Management</h1>
</div>
<DataTable
{columns}
data={$routerServiceMerge || []}
showSourceTabs={true}
createButton={{
label: 'Add Router',
onClick: openCreateModal
}}
/>
</div>
</Tabs.Content>
<Tabs.Content value={TraefikSource.API}>
<div class="flex flex-col gap-4">
<div class="flex items-center justify-start gap-2">
<Route />
<h1 class="text-2xl font-bold">Router Management</h1>
</div>
<DataTable {columns} data={$routerServiceMerge || []} showSourceTabs={true} />
</div>
</Tabs.Content>
<Tabs.Content value={TraefikSource.AGENT}>
<div class="flex flex-col gap-4">
<div class="flex items-center justify-start gap-2">
<Route />
<h1 class="text-2xl font-bold">Router Management</h1>
</div>
<DataTable {columns} data={$routerServiceMerge || []} showSourceTabs={true} />
</div>
</Tabs.Content>
</Tabs.Root>
<div class="flex flex-col gap-4">
<div class="flex items-center justify-start gap-2">
<Route />
<h1 class="text-2xl font-bold">Router Management</h1>
</div>
{#if source.value === TraefikSource.LOCAL}
<DataTable
{columns}
data={$routerServiceMerge || []}
showSourceTabs={true}
createButton={{
label: 'Add Router',
onClick: openCreateModal
}}
/>
{:else}
<DataTable {columns} data={$routerServiceMerge || []} showSourceTabs={true} />
{/if}
</div>
<RouterModal
bind:open={modalState.isOpen}