[FA-misc] Reporting service now has a status page on the frontend
This commit is contained in:
179
fictionarchive-web-astro/src/lib/components/JobsTab.svelte
Normal file
179
fictionarchive-web-astro/src/lib/components/JobsTab.svelte
Normal file
@@ -0,0 +1,179 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { client } from '$lib/graphql/client';
|
||||
import { JobsDocument, type JobsQuery } from '$lib/graphql/__generated__/graphql';
|
||||
import JobsTable from './JobsTable.svelte';
|
||||
import JobFilters from './JobFilters.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '$lib/components/ui/card';
|
||||
import {
|
||||
type JobFilters as JobFiltersType,
|
||||
parseJobFiltersFromURL,
|
||||
syncJobFiltersToURL,
|
||||
jobFiltersToGraphQLWhere,
|
||||
jobSortToGraphQLOrder,
|
||||
hasActiveJobFilters,
|
||||
EMPTY_JOB_FILTERS,
|
||||
} from '$lib/utils/jobFilterParams';
|
||||
|
||||
const PAGE_SIZE = 20;
|
||||
|
||||
type JobsConnection = NonNullable<JobsQuery['jobs']>;
|
||||
type JobEdge = NonNullable<JobsConnection['edges']>[number];
|
||||
type PageInfo = JobsConnection['pageInfo'];
|
||||
|
||||
let edges: JobEdge[] = $state([]);
|
||||
let pageInfo: PageInfo | null = $state(null);
|
||||
let fetching = $state(false);
|
||||
let error: string | null = $state(null);
|
||||
let initialLoad = $state(true);
|
||||
let filters: JobFiltersType = $state({ ...EMPTY_JOB_FILTERS });
|
||||
|
||||
// Pagination: stack of "after" cursors used to reach each page
|
||||
// cursorStack[0] = the "after" cursor used to fetch page 2, etc.
|
||||
let cursorStack: (string | null)[] = $state([]);
|
||||
let currentPage = $derived(cursorStack.length + 1);
|
||||
|
||||
const jobs = $derived((edges ?? []).map((edge) => edge.node).filter(Boolean));
|
||||
|
||||
// Extract unique job types from loaded data for the filter dropdown
|
||||
const availableJobTypes = $derived(() => {
|
||||
const types = new Set<string>();
|
||||
for (const job of jobs) {
|
||||
types.add(job.jobType);
|
||||
}
|
||||
return Array.from(types).sort();
|
||||
});
|
||||
|
||||
async function fetchJobs(after: string | null = null) {
|
||||
fetching = true;
|
||||
error = null;
|
||||
|
||||
try {
|
||||
const where = jobFiltersToGraphQLWhere(filters);
|
||||
const order = jobSortToGraphQLOrder(filters.sort);
|
||||
const result = await client
|
||||
.query(JobsDocument, { first: PAGE_SIZE, after, where, order }, { requestPolicy: 'network-only' })
|
||||
.toPromise();
|
||||
|
||||
if (result.error) {
|
||||
error = result.error.message;
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.data?.jobs) {
|
||||
edges = result.data.jobs.edges ?? [];
|
||||
pageInfo = result.data.jobs.pageInfo;
|
||||
}
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Unknown error';
|
||||
} finally {
|
||||
fetching = false;
|
||||
initialLoad = false;
|
||||
}
|
||||
}
|
||||
|
||||
function nextPage() {
|
||||
if (pageInfo?.endCursor && pageInfo.hasNextPage) {
|
||||
cursorStack = [...cursorStack, pageInfo.endCursor];
|
||||
fetchJobs(pageInfo.endCursor);
|
||||
}
|
||||
}
|
||||
|
||||
function prevPage() {
|
||||
if (cursorStack.length > 0) {
|
||||
const newStack = [...cursorStack];
|
||||
newStack.pop(); // remove current page's cursor
|
||||
cursorStack = newStack;
|
||||
// Fetch with the previous page's cursor (or null for page 1)
|
||||
const after = newStack.length > 0 ? newStack[newStack.length - 1] : null;
|
||||
fetchJobs(after);
|
||||
}
|
||||
}
|
||||
|
||||
function handleFilterChange(newFilters: JobFiltersType) {
|
||||
filters = newFilters;
|
||||
edges = [];
|
||||
pageInfo = null;
|
||||
cursorStack = [];
|
||||
syncJobFiltersToURL(filters);
|
||||
fetchJobs();
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
filters = parseJobFiltersFromURL();
|
||||
fetchJobs();
|
||||
|
||||
const handlePopState = () => {
|
||||
filters = parseJobFiltersFromURL();
|
||||
edges = [];
|
||||
pageInfo = null;
|
||||
cursorStack = [];
|
||||
fetchJobs();
|
||||
};
|
||||
|
||||
window.addEventListener('popstate', handlePopState);
|
||||
return () => window.removeEventListener('popstate', handlePopState);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="space-y-4">
|
||||
<Card class="shadow-md shadow-primary/10">
|
||||
<CardHeader>
|
||||
<CardTitle>Filters</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<JobFilters {filters} onFilterChange={handleFilterChange} availableJobTypes={availableJobTypes()} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{#if fetching && initialLoad}
|
||||
<Card>
|
||||
<CardContent>
|
||||
<div class="flex items-center justify-center py-8">
|
||||
<div
|
||||
class="border-primary h-10 w-10 animate-spin rounded-full border-2 border-t-transparent"
|
||||
aria-label="Loading jobs"
|
||||
></div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{/if}
|
||||
|
||||
{#if error}
|
||||
<Card class="border-destructive/40 bg-destructive/5">
|
||||
<CardContent>
|
||||
<p class="text-destructive py-4 text-sm">Could not load jobs: {error}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{/if}
|
||||
|
||||
{#if !initialLoad && !error && jobs.length === 0}
|
||||
<Card>
|
||||
<CardContent>
|
||||
<p class="text-muted-foreground py-4 text-sm">
|
||||
{#if hasActiveJobFilters(filters)}
|
||||
No jobs match your filters. Try adjusting your search criteria.
|
||||
{:else}
|
||||
No jobs found.
|
||||
{/if}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{/if}
|
||||
|
||||
{#if jobs.length > 0}
|
||||
<JobsTable {jobs} />
|
||||
|
||||
<!-- Pagination -->
|
||||
<div class="flex items-center justify-center gap-4">
|
||||
<Button variant="outline" size="sm" disabled={currentPage === 1 || fetching} onclick={prevPage}>
|
||||
Previous
|
||||
</Button>
|
||||
<span class="text-sm text-muted-foreground">Page {currentPage}</span>
|
||||
<Button variant="outline" size="sm" disabled={!pageInfo?.hasNextPage || fetching} onclick={nextPage}>
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
Reference in New Issue
Block a user