From 7ccc3ade9eb1dd17b668f055c9b3f50eaf88556d Mon Sep 17 00:00:00 2001 From: gamer147 Date: Sun, 1 Feb 2026 12:07:57 -0500 Subject: [PATCH] [FA-misc] Reporting service now has a status page on the frontend --- .../GraphQL/JobQueries.cs | 26 --- .../GraphQL/Query.cs | 71 ++++++ .../Models/DTOs/JobDto.cs | 19 ++ .../Program.cs | 3 +- .../Properties/launchSettings.json | 1 + .../subgraph-config.json | 2 +- .../GraphQL/Query.cs | 2 +- .../src/lib/components/JobFilters.svelte | 217 ++++++++++++++++++ .../src/lib/components/JobRow.svelte | 168 ++++++++++++++ .../src/lib/components/JobStatusBadge.svelte | 20 ++ .../src/lib/components/JobsTab.svelte | 179 +++++++++++++++ .../src/lib/components/JobsTable.svelte | 42 ++++ .../src/lib/components/Navbar.svelte | 4 + .../src/lib/components/StatusPage.svelte | 21 ++ .../src/lib/graphql/__generated__/graphql.ts | 123 +++++++++- .../src/lib/graphql/queries/jobs.graphql | 40 ++++ .../src/lib/utils/jobFilterParams.ts | 131 +++++++++++ .../src/pages/status/index.astro | 8 + 18 files changed, 1046 insertions(+), 31 deletions(-) delete mode 100644 FictionArchive.Service.ReportingService/GraphQL/JobQueries.cs create mode 100644 FictionArchive.Service.ReportingService/GraphQL/Query.cs create mode 100644 FictionArchive.Service.ReportingService/Models/DTOs/JobDto.cs create mode 100644 fictionarchive-web-astro/src/lib/components/JobFilters.svelte create mode 100644 fictionarchive-web-astro/src/lib/components/JobRow.svelte create mode 100644 fictionarchive-web-astro/src/lib/components/JobStatusBadge.svelte create mode 100644 fictionarchive-web-astro/src/lib/components/JobsTab.svelte create mode 100644 fictionarchive-web-astro/src/lib/components/JobsTable.svelte create mode 100644 fictionarchive-web-astro/src/lib/components/StatusPage.svelte create mode 100644 fictionarchive-web-astro/src/lib/graphql/queries/jobs.graphql create mode 100644 fictionarchive-web-astro/src/lib/utils/jobFilterParams.ts create mode 100644 fictionarchive-web-astro/src/pages/status/index.astro diff --git a/FictionArchive.Service.ReportingService/GraphQL/JobQueries.cs b/FictionArchive.Service.ReportingService/GraphQL/JobQueries.cs deleted file mode 100644 index 2fbe7b8..0000000 --- a/FictionArchive.Service.ReportingService/GraphQL/JobQueries.cs +++ /dev/null @@ -1,26 +0,0 @@ -using FictionArchive.Service.ReportingService.Models; -using FictionArchive.Service.ReportingService.Services; -using HotChocolate.Authorization; -using HotChocolate.Data; - -namespace FictionArchive.Service.ReportingService.GraphQL; - -[QueryType] -public static class JobQueries -{ - [UseProjection] - [Authorize] - [UseFirstOrDefault] - public static IQueryable GetJobById( - Guid jobId, - ReportingDbContext db) - => db.Jobs.Where(j => j.Id == jobId); - - [UsePaging] - [UseProjection] - [UseFiltering] - [UseSorting] - [Authorize] - public static IQueryable GetJobs(ReportingDbContext db) - => db.Jobs; -} diff --git a/FictionArchive.Service.ReportingService/GraphQL/Query.cs b/FictionArchive.Service.ReportingService/GraphQL/Query.cs new file mode 100644 index 0000000..6edc2ee --- /dev/null +++ b/FictionArchive.Service.ReportingService/GraphQL/Query.cs @@ -0,0 +1,71 @@ +using FictionArchive.Service.ReportingService.Models.DTOs; +using FictionArchive.Service.ReportingService.Services; +using HotChocolate.Authorization; +using HotChocolate.Data; + +namespace FictionArchive.Service.ReportingService.GraphQL; + +public class Query +{ + [Authorize] + //[UseProjection] + [UseFirstOrDefault] + public IQueryable GetJobById( + Guid jobId, + ReportingDbContext db) + => db.Jobs.Where(j => j.Id == jobId).Select(j => new JobDto + { + Id = j.Id, + CreatedTime = j.CreatedTime, + LastUpdatedTime = j.LastUpdatedTime, + ParentJobId = j.ParentJobId, + JobType = j.JobType, + DisplayName = j.DisplayName, + Status = j.Status, + ErrorMessage = j.ErrorMessage, + Metadata = j.Metadata, + ChildJobs = j.ChildJobs.Select(c => new JobDto + { + Id = c.Id, + CreatedTime = c.CreatedTime, + LastUpdatedTime = c.LastUpdatedTime, + ParentJobId = c.ParentJobId, + JobType = c.JobType, + DisplayName = c.DisplayName, + Status = c.Status, + ErrorMessage = c.ErrorMessage, + Metadata = c.Metadata + }) + }); + + [Authorize] + [UsePaging] + //[UseProjection] + [UseFiltering] + [UseSorting] + public IQueryable GetJobs(ReportingDbContext db) + => db.Jobs.Select(j => new JobDto + { + Id = j.Id, + CreatedTime = j.CreatedTime, + LastUpdatedTime = j.LastUpdatedTime, + ParentJobId = j.ParentJobId, + JobType = j.JobType, + DisplayName = j.DisplayName, + Status = j.Status, + ErrorMessage = j.ErrorMessage, + Metadata = j.Metadata, + ChildJobs = j.ChildJobs.Select(c => new JobDto + { + Id = c.Id, + CreatedTime = c.CreatedTime, + LastUpdatedTime = c.LastUpdatedTime, + ParentJobId = c.ParentJobId, + JobType = c.JobType, + DisplayName = c.DisplayName, + Status = c.Status, + ErrorMessage = c.ErrorMessage, + Metadata = c.Metadata + }) + }); +} diff --git a/FictionArchive.Service.ReportingService/Models/DTOs/JobDto.cs b/FictionArchive.Service.ReportingService/Models/DTOs/JobDto.cs new file mode 100644 index 0000000..dce71a0 --- /dev/null +++ b/FictionArchive.Service.ReportingService/Models/DTOs/JobDto.cs @@ -0,0 +1,19 @@ +using FictionArchive.Common.Enums; +using HotChocolate.Data; +using NodaTime; + +namespace FictionArchive.Service.ReportingService.Models.DTOs; + +public class JobDto +{ + public Guid Id { get; init; } + public Instant CreatedTime { get; init; } + public Instant LastUpdatedTime { get; init; } + public Guid? ParentJobId { get; init; } + public required string JobType { get; init; } + public required string DisplayName { get; init; } + public JobStatus Status { get; init; } + public string? ErrorMessage { get; init; } + public Dictionary? Metadata { get; init; } + public IEnumerable? ChildJobs { get; init; } +} diff --git a/FictionArchive.Service.ReportingService/Program.cs b/FictionArchive.Service.ReportingService/Program.cs index 4f43e86..946420c 100644 --- a/FictionArchive.Service.ReportingService/Program.cs +++ b/FictionArchive.Service.ReportingService/Program.cs @@ -36,9 +36,8 @@ public class Program #region GraphQL builder.Services.AddGraphQLServer() - .AddQueryConventions() - .AddTypeExtension(typeof(JobQueries)) .ApplySaneDefaults() + .AddQueryType() .AddAuthorization(); #endregion diff --git a/FictionArchive.Service.ReportingService/Properties/launchSettings.json b/FictionArchive.Service.ReportingService/Properties/launchSettings.json index b6c8106..8ce2e5d 100644 --- a/FictionArchive.Service.ReportingService/Properties/launchSettings.json +++ b/FictionArchive.Service.ReportingService/Properties/launchSettings.json @@ -14,6 +14,7 @@ "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, + "launchUrl": "graphql", "applicationUrl": "https://localhost:7310;http://localhost:5140", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" diff --git a/FictionArchive.Service.ReportingService/subgraph-config.json b/FictionArchive.Service.ReportingService/subgraph-config.json index 529f9a0..42aacbc 100644 --- a/FictionArchive.Service.ReportingService/subgraph-config.json +++ b/FictionArchive.Service.ReportingService/subgraph-config.json @@ -1,6 +1,6 @@ { "subgraph": "Reporting", "http": { - "baseAddress": "http://localhost:5140/graphql" + "baseAddress": "https://localhost:7310/graphql" } } diff --git a/FictionArchive.Service.SchedulerService/GraphQL/Query.cs b/FictionArchive.Service.SchedulerService/GraphQL/Query.cs index 2594569..31b54a4 100644 --- a/FictionArchive.Service.SchedulerService/GraphQL/Query.cs +++ b/FictionArchive.Service.SchedulerService/GraphQL/Query.cs @@ -8,7 +8,7 @@ namespace FictionArchive.Service.SchedulerService.GraphQL; public class Query { - public async Task> GetJobs(JobManagerService jobManager) + public async Task> GetScheduledJobs(JobManagerService jobManager) { return await jobManager.GetScheduledJobs(); } diff --git a/fictionarchive-web-astro/src/lib/components/JobFilters.svelte b/fictionarchive-web-astro/src/lib/components/JobFilters.svelte new file mode 100644 index 0000000..083bde3 --- /dev/null +++ b/fictionarchive-web-astro/src/lib/components/JobFilters.svelte @@ -0,0 +1,217 @@ + + +
+ +
+ + handleSearchInput(e.currentTarget.value)} + class="pl-9" + /> +
+ + + handleStatusChange(v as string[])} + > + + + {filters.statuses.length > 0 ? selectedStatusLabels : 'Status'} + + + + + {#each JOB_STATUS_OPTIONS as option (option.value)} + + {#snippet children({ selected })} +
+ {#if selected} + + {/if} +
+ {option.label} + {/snippet} +
+ {/each} +
+
+ + + {#if availableJobTypes.length > 0} + v && handleJobTypeChange(v)} + > + + + {filters.jobType || 'Job Type'} + + + + + + {#snippet children({ selected })} +
+ {#if selected} + + {/if} +
+ All Types + {/snippet} +
+ {#each availableJobTypes as jobType (jobType)} + + {#snippet children({ selected })} +
+ {#if selected} + + {/if} +
+ {jobType} + {/snippet} +
+ {/each} +
+
+ {/if} + + + {#if hasActiveJobFilters(filters)} + + {/if} +
+ + +{#if hasActiveJobFilters(filters)} +
+ {#if filters.search} + + Search: {filters.search} + + + {/if} + + {#each filters.statuses as status (status)} + + {JOB_STATUS_OPTIONS.find((o) => o.value === status)?.label ?? status} + + + {/each} + + {#if filters.jobType} + + Type: {filters.jobType} + + + {/if} +
+{/if} diff --git a/fictionarchive-web-astro/src/lib/components/JobRow.svelte b/fictionarchive-web-astro/src/lib/components/JobRow.svelte new file mode 100644 index 0000000..8d83181 --- /dev/null +++ b/fictionarchive-web-astro/src/lib/components/JobRow.svelte @@ -0,0 +1,168 @@ + + + + + + {#if hasChildren} + + {/if} + + {job.displayName} + {job.jobType} + + + + + {formatTime(job.createdTime)} + {formatTimeFull(job.createdTime)} + + + + + {#if hasChildren} + {children.length} sub-job{children.length !== 1 ? 's' : ''} + {/if} + + + + +{#if expanded} + + +
+ +
+ Last Updated + {formatTimeFull(job.lastUpdatedTime)} +
+ + {#if job.errorMessage} +
+ Error: + {job.errorMessage} +
+ {/if} + + + {#if metadata.length > 0} +
+

Metadata

+
+ {#each metadata as entry (entry.key)} + {entry.key} + {entry.value} + {/each} +
+
+ {/if} + + + {#if hasChildren} +
+

Sub-jobs

+ + + + + + + + + + + {#each children as child (child.id)} + + + + + + + {#if child.errorMessage} + + + + {/if} + {/each} + +
NameTypeStatusCreated
{child.displayName}{child.jobType} + + + {formatTime(child.createdTime)} + {formatTimeFull(child.createdTime)} + + +
+ {child.errorMessage} +
+
+ {/if} +
+ + +{/if} diff --git a/fictionarchive-web-astro/src/lib/components/JobStatusBadge.svelte b/fictionarchive-web-astro/src/lib/components/JobStatusBadge.svelte new file mode 100644 index 0000000..8dc7f6f --- /dev/null +++ b/fictionarchive-web-astro/src/lib/components/JobStatusBadge.svelte @@ -0,0 +1,20 @@ + + +{config.label} diff --git a/fictionarchive-web-astro/src/lib/components/JobsTab.svelte b/fictionarchive-web-astro/src/lib/components/JobsTab.svelte new file mode 100644 index 0000000..90dc129 --- /dev/null +++ b/fictionarchive-web-astro/src/lib/components/JobsTab.svelte @@ -0,0 +1,179 @@ + + +
+ + + Filters + + + + + + + {#if fetching && initialLoad} + + +
+
+
+
+
+ {/if} + + {#if error} + + +

Could not load jobs: {error}

+
+
+ {/if} + + {#if !initialLoad && !error && jobs.length === 0} + + +

+ {#if hasActiveJobFilters(filters)} + No jobs match your filters. Try adjusting your search criteria. + {:else} + No jobs found. + {/if} +

+
+
+ {/if} + + {#if jobs.length > 0} + + + +
+ + Page {currentPage} + +
+ {/if} +
diff --git a/fictionarchive-web-astro/src/lib/components/JobsTable.svelte b/fictionarchive-web-astro/src/lib/components/JobsTable.svelte new file mode 100644 index 0000000..1167787 --- /dev/null +++ b/fictionarchive-web-astro/src/lib/components/JobsTable.svelte @@ -0,0 +1,42 @@ + + +
+ + + + + + + + + + + + + {#each jobs as job (job.id)} + toggleRow(job.id)} + columnCount={COLUMN_COUNT} + /> + {/each} + +
NameTypeStatusCreatedSub-jobs
+
diff --git a/fictionarchive-web-astro/src/lib/components/Navbar.svelte b/fictionarchive-web-astro/src/lib/components/Navbar.svelte index 826c3c6..b430d50 100644 --- a/fictionarchive-web-astro/src/lib/components/Navbar.svelte +++ b/fictionarchive-web-astro/src/lib/components/Navbar.svelte @@ -29,7 +29,11 @@ Reading Lists + + Status + {/if} +
diff --git a/fictionarchive-web-astro/src/lib/components/StatusPage.svelte b/fictionarchive-web-astro/src/lib/components/StatusPage.svelte new file mode 100644 index 0000000..3af3b5a --- /dev/null +++ b/fictionarchive-web-astro/src/lib/components/StatusPage.svelte @@ -0,0 +1,21 @@ + + +
+
+

Status

+

Monitor jobs and system activity

+
+ + + + Jobs + + + + + + +
diff --git a/fictionarchive-web-astro/src/lib/graphql/__generated__/graphql.ts b/fictionarchive-web-astro/src/lib/graphql/__generated__/graphql.ts index 4ba2090..bf09f18 100644 --- a/fictionarchive-web-astro/src/lib/graphql/__generated__/graphql.ts +++ b/fictionarchive-web-astro/src/lib/graphql/__generated__/graphql.ts @@ -257,6 +257,45 @@ export type InvitedUserDto = { username: Scalars['String']['output']; }; +export type JobDto = { + childJobs: Maybe>; + createdTime: Scalars['Instant']['output']; + displayName: Scalars['String']['output']; + errorMessage: Maybe; + id: Scalars['UUID']['output']; + jobType: Scalars['String']['output']; + lastUpdatedTime: Scalars['Instant']['output']; + metadata: Maybe>; + parentJobId: Maybe; + status: JobStatus; +}; + +export type JobDtoFilterInput = { + and?: InputMaybe>; + childJobs?: InputMaybe; + createdTime?: InputMaybe; + displayName?: InputMaybe; + errorMessage?: InputMaybe; + id?: InputMaybe; + jobType?: InputMaybe; + lastUpdatedTime?: InputMaybe; + metadata?: InputMaybe; + or?: InputMaybe>; + parentJobId?: InputMaybe; + status?: InputMaybe; +}; + +export type JobDtoSortInput = { + createdTime?: InputMaybe; + displayName?: InputMaybe; + errorMessage?: InputMaybe; + id?: InputMaybe; + jobType?: InputMaybe; + lastUpdatedTime?: InputMaybe; + parentJobId?: InputMaybe; + status?: InputMaybe; +}; + export type JobKey = { group: Scalars['String']['output']; name: Scalars['String']['output']; @@ -266,6 +305,39 @@ export type JobPersistenceError = Error & { message: Scalars['String']['output']; }; +export const JobStatus = { + Completed: 'COMPLETED', + Failed: 'FAILED', + InProgress: 'IN_PROGRESS', + Pending: 'PENDING' +} as const; + +export type JobStatus = typeof JobStatus[keyof typeof JobStatus]; +export type JobStatusOperationFilterInput = { + eq?: InputMaybe; + in?: InputMaybe>; + neq?: InputMaybe; + nin?: InputMaybe>; +}; + +/** A connection to a list of items. */ +export type JobsConnection = { + /** A list of edges. */ + edges: Maybe>; + /** A flattened list of the nodes. */ + nodes: Maybe>; + /** Information to aid in pagination. */ + pageInfo: PageInfo; +}; + +/** An edge in a connection. */ +export type JobsEdge = { + /** A cursor for use in pagination. */ + cursor: Scalars['String']['output']; + /** The item at the end of the edge. */ + node: JobDto; +}; + export type KeyNotFoundError = Error & { message: Scalars['String']['output']; }; @@ -275,6 +347,13 @@ export type KeyValuePairOfStringAndString = { value: Scalars['String']['output']; }; +export type KeyValuePairOfStringAndStringFilterInput = { + and?: InputMaybe>; + key?: InputMaybe; + or?: InputMaybe>; + value?: InputMaybe; +}; + export const Language = { Ch: 'CH', En: 'EN', @@ -304,6 +383,20 @@ export type ListFilterInputTypeOfImageDtoFilterInput = { some?: InputMaybe; }; +export type ListFilterInputTypeOfJobDtoFilterInput = { + all?: InputMaybe; + any?: InputMaybe; + none?: InputMaybe; + some?: InputMaybe; +}; + +export type ListFilterInputTypeOfKeyValuePairOfStringAndStringFilterInput = { + all?: InputMaybe; + any?: InputMaybe; + none?: InputMaybe; + some?: InputMaybe; +}; + export type ListFilterInputTypeOfNovelTagDtoFilterInput = { all?: InputMaybe; any?: InputMaybe; @@ -576,10 +669,12 @@ export type Query = { bookmarks: Array; chapter: Maybe; currentUser: Maybe; - jobs: Array; + jobById: Maybe; + jobs: Maybe; novels: Maybe; readingList: Maybe; readingLists: Array; + scheduledJobs: Array; translationEngines: Array; translationRequests: Maybe; }; @@ -598,6 +693,21 @@ export type QueryChapterArgs = { }; +export type QueryJobByIdArgs = { + jobId: Scalars['UUID']['input']; +}; + + +export type QueryJobsArgs = { + after?: InputMaybe; + before?: InputMaybe; + first?: InputMaybe; + last?: InputMaybe; + order?: InputMaybe>; + where?: InputMaybe; +}; + + export type QueryNovelsArgs = { after?: InputMaybe; before?: InputMaybe; @@ -1076,6 +1186,16 @@ export type GetChapterQueryVariables = Exact<{ export type GetChapterQuery = { chapter: { id: any, order: any, name: string, body: string, url: string | null, revision: any, createdTime: any, lastUpdatedTime: any, novelId: any, novelName: string, volumeId: any, volumeName: string, volumeOrder: number, totalChaptersInVolume: number, prevChapterVolumeOrder: number | null, prevChapterOrder: any | null, nextChapterVolumeOrder: number | null, nextChapterOrder: any | null, images: Array<{ id: any, newPath: string | null }> } | null }; +export type JobsQueryVariables = Exact<{ + first?: InputMaybe; + after?: InputMaybe; + where?: InputMaybe; + order?: InputMaybe | JobDtoSortInput>; +}>; + + +export type JobsQuery = { jobs: { edges: Array<{ cursor: string, node: { id: any, parentJobId: any | null, jobType: string, displayName: string, status: JobStatus, errorMessage: string | null, createdTime: any, lastUpdatedTime: any, metadata: Array<{ key: string, value: string }> | null, childJobs: Array<{ id: any, jobType: string, displayName: string, status: JobStatus, errorMessage: string | null, createdTime: any, lastUpdatedTime: any, metadata: Array<{ key: string, value: string }> | null }> | null } }> | null, pageInfo: { hasNextPage: boolean, hasPreviousPage: boolean, startCursor: string | null, endCursor: string | null } } | null }; + export type NovelQueryVariables = Exact<{ id: Scalars['UnsignedInt']['input']; }>; @@ -1129,6 +1249,7 @@ export const UpdateReadingListDocument = {"kind":"Document","definitions":[{"kin export const UpsertBookmarkDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpsertBookmark"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpsertBookmarkInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"upsertBookmark"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"bookmarkPayload"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"success"}},{"kind":"Field","name":{"kind":"Name","value":"bookmark"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"chapterId"}},{"kind":"Field","name":{"kind":"Name","value":"novelId"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"createdTime"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"errors"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Error"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"message"}}]}}]}}]}}]}}]} as unknown as DocumentNode; export const GetBookmarksDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetBookmarks"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"novelId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UnsignedInt"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"bookmarks"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"novelId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"novelId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"chapterId"}},{"kind":"Field","name":{"kind":"Name","value":"novelId"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"createdTime"}}]}}]}}]} as unknown as DocumentNode; export const GetChapterDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetChapter"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"novelId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UnsignedInt"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"volumeOrder"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UnsignedInt"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"chapterOrder"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UnsignedInt"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"chapter"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"novelId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"novelId"}}},{"kind":"Argument","name":{"kind":"Name","value":"volumeOrder"},"value":{"kind":"Variable","name":{"kind":"Name","value":"volumeOrder"}}},{"kind":"Argument","name":{"kind":"Name","value":"chapterOrder"},"value":{"kind":"Variable","name":{"kind":"Name","value":"chapterOrder"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"order"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"body"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"revision"}},{"kind":"Field","name":{"kind":"Name","value":"createdTime"}},{"kind":"Field","name":{"kind":"Name","value":"lastUpdatedTime"}},{"kind":"Field","name":{"kind":"Name","value":"images"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"newPath"}}]}},{"kind":"Field","name":{"kind":"Name","value":"novelId"}},{"kind":"Field","name":{"kind":"Name","value":"novelName"}},{"kind":"Field","name":{"kind":"Name","value":"volumeId"}},{"kind":"Field","name":{"kind":"Name","value":"volumeName"}},{"kind":"Field","name":{"kind":"Name","value":"volumeOrder"}},{"kind":"Field","name":{"kind":"Name","value":"totalChaptersInVolume"}},{"kind":"Field","name":{"kind":"Name","value":"prevChapterVolumeOrder"}},{"kind":"Field","name":{"kind":"Name","value":"prevChapterOrder"}},{"kind":"Field","name":{"kind":"Name","value":"nextChapterVolumeOrder"}},{"kind":"Field","name":{"kind":"Name","value":"nextChapterOrder"}}]}}]}}]} as unknown as DocumentNode; +export const JobsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"Jobs"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"first"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"after"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"where"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"JobDtoFilterInput"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"order"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"JobDtoSortInput"}}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"jobs"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"Variable","name":{"kind":"Name","value":"first"}}},{"kind":"Argument","name":{"kind":"Name","value":"after"},"value":{"kind":"Variable","name":{"kind":"Name","value":"after"}}},{"kind":"Argument","name":{"kind":"Name","value":"where"},"value":{"kind":"Variable","name":{"kind":"Name","value":"where"}}},{"kind":"Argument","name":{"kind":"Name","value":"order"},"value":{"kind":"Variable","name":{"kind":"Name","value":"order"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"parentJobId"}},{"kind":"Field","name":{"kind":"Name","value":"jobType"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"errorMessage"}},{"kind":"Field","name":{"kind":"Name","value":"metadata"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"value"}}]}},{"kind":"Field","name":{"kind":"Name","value":"createdTime"}},{"kind":"Field","name":{"kind":"Name","value":"lastUpdatedTime"}},{"kind":"Field","name":{"kind":"Name","value":"childJobs"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"jobType"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"errorMessage"}},{"kind":"Field","name":{"kind":"Name","value":"metadata"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"value"}}]}},{"kind":"Field","name":{"kind":"Name","value":"createdTime"}},{"kind":"Field","name":{"kind":"Name","value":"lastUpdatedTime"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}},{"kind":"Field","name":{"kind":"Name","value":"hasPreviousPage"}},{"kind":"Field","name":{"kind":"Name","value":"startCursor"}},{"kind":"Field","name":{"kind":"Name","value":"endCursor"}}]}}]}}]}}]} as unknown as DocumentNode; export const NovelDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"Novel"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UnsignedInt"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"novels"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"where"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"id"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"eq"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}]}}]}},{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"IntValue","value":"1"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"nodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"rawLanguage"}},{"kind":"Field","name":{"kind":"Name","value":"rawStatus"}},{"kind":"Field","name":{"kind":"Name","value":"statusOverride"}},{"kind":"Field","name":{"kind":"Name","value":"externalId"}},{"kind":"Field","name":{"kind":"Name","value":"createdTime"}},{"kind":"Field","name":{"kind":"Name","value":"lastUpdatedTime"}},{"kind":"Field","name":{"kind":"Name","value":"author"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"externalUrl"}}]}},{"kind":"Field","name":{"kind":"Name","value":"source"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"url"}}]}},{"kind":"Field","name":{"kind":"Name","value":"coverImage"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"newPath"}}]}},{"kind":"Field","name":{"kind":"Name","value":"tags"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"tagType"}}]}},{"kind":"Field","name":{"kind":"Name","value":"volumes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"order"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"createdTime"}},{"kind":"Field","name":{"kind":"Name","value":"lastUpdatedTime"}},{"kind":"Field","name":{"kind":"Name","value":"chapters"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"order"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"lastUpdatedTime"}},{"kind":"Field","name":{"kind":"Name","value":"images"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"newPath"}}]}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode; export const NovelsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"Novels"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"first"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"after"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"where"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"NovelDtoFilterInput"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"order"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"NovelDtoSortInput"}}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"novels"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"Variable","name":{"kind":"Name","value":"first"}}},{"kind":"Argument","name":{"kind":"Name","value":"after"},"value":{"kind":"Variable","name":{"kind":"Name","value":"after"}}},{"kind":"Argument","name":{"kind":"Name","value":"where"},"value":{"kind":"Variable","name":{"kind":"Name","value":"where"}}},{"kind":"Argument","name":{"kind":"Name","value":"order"},"value":{"kind":"Variable","name":{"kind":"Name","value":"order"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"coverImage"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"newPath"}}]}},{"kind":"Field","name":{"kind":"Name","value":"rawStatus"}},{"kind":"Field","name":{"kind":"Name","value":"lastUpdatedTime"}},{"kind":"Field","name":{"kind":"Name","value":"volumes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"order"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"chapters"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"order"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"tags"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"tagType"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}},{"kind":"Field","name":{"kind":"Name","value":"endCursor"}}]}}]}}]}}]} as unknown as DocumentNode; export const GetReadingListDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetReadingList"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"readingList"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"itemCount"}},{"kind":"Field","name":{"kind":"Name","value":"createdTime"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"novelId"}},{"kind":"Field","name":{"kind":"Name","value":"order"}},{"kind":"Field","name":{"kind":"Name","value":"addedTime"}}]}}]}}]}}]} as unknown as DocumentNode; diff --git a/fictionarchive-web-astro/src/lib/graphql/queries/jobs.graphql b/fictionarchive-web-astro/src/lib/graphql/queries/jobs.graphql new file mode 100644 index 0000000..ab45458 --- /dev/null +++ b/fictionarchive-web-astro/src/lib/graphql/queries/jobs.graphql @@ -0,0 +1,40 @@ +query Jobs($first: Int, $after: String, $where: JobDtoFilterInput, $order: [JobDtoSortInput!]) { + jobs(first: $first, after: $after, where: $where, order: $order) { + edges { + cursor + node { + id + parentJobId + jobType + displayName + status + errorMessage + metadata { + key + value + } + createdTime + lastUpdatedTime + childJobs { + id + jobType + displayName + status + errorMessage + metadata { + key + value + } + createdTime + lastUpdatedTime + } + } + } + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + } +} diff --git a/fictionarchive-web-astro/src/lib/utils/jobFilterParams.ts b/fictionarchive-web-astro/src/lib/utils/jobFilterParams.ts new file mode 100644 index 0000000..81db1a5 --- /dev/null +++ b/fictionarchive-web-astro/src/lib/utils/jobFilterParams.ts @@ -0,0 +1,131 @@ +import type { JobDtoFilterInput, JobDtoSortInput, JobStatus, SortEnumType } from '$lib/graphql/__generated__/graphql'; + +export type JobSortField = 'createdTime' | 'lastUpdatedTime' | 'status'; +export type JobSortDirection = SortEnumType; + +export interface JobSort { + field: JobSortField; + direction: JobSortDirection; +} + +export interface JobFilters { + search: string; + statuses: JobStatus[]; + jobType: string; + sort: JobSort; +} + +export const JOB_STATUS_OPTIONS: { value: JobStatus; label: string }[] = [ + { value: 'PENDING', label: 'Pending' }, + { value: 'IN_PROGRESS', label: 'In Progress' }, + { value: 'COMPLETED', label: 'Completed' }, + { value: 'FAILED', label: 'Failed' }, +]; + +export const DEFAULT_JOB_SORT: JobSort = { + field: 'createdTime', + direction: 'DESC', +}; + +export const EMPTY_JOB_FILTERS: JobFilters = { + search: '', + statuses: [], + jobType: '', + sort: DEFAULT_JOB_SORT, +}; + +const VALID_STATUSES: string[] = JOB_STATUS_OPTIONS.map((o) => o.value); +const VALID_SORT_FIELDS: JobSortField[] = ['createdTime', 'lastUpdatedTime', 'status']; +const VALID_SORT_DIRECTIONS: JobSortDirection[] = ['ASC', 'DESC']; + +export function parseJobFiltersFromURL(searchParams?: URLSearchParams): JobFilters { + const params = searchParams ?? new URLSearchParams(window.location.search); + + const search = params.get('search') ?? ''; + const jobType = params.get('jobType') ?? ''; + + const statusParam = params.get('status') ?? ''; + const statuses = statusParam + .split(',') + .filter((s) => s && VALID_STATUSES.includes(s)) as JobStatus[]; + + const sortField = params.get('sortBy') as JobSortField | null; + const sortDir = params.get('sortDir') as JobSortDirection | null; + const sort: JobSort = { + field: sortField && VALID_SORT_FIELDS.includes(sortField) ? sortField : DEFAULT_JOB_SORT.field, + direction: sortDir && VALID_SORT_DIRECTIONS.includes(sortDir) ? sortDir : DEFAULT_JOB_SORT.direction, + }; + + return { search, statuses, jobType, sort }; +} + +export function jobFiltersToURLParams(filters: JobFilters): string { + const params = new URLSearchParams(); + + if (filters.search.trim()) { + params.set('search', filters.search.trim()); + } + if (filters.statuses.length > 0) { + params.set('status', filters.statuses.join(',')); + } + if (filters.jobType.trim()) { + params.set('jobType', filters.jobType.trim()); + } + if (filters.sort.field !== DEFAULT_JOB_SORT.field || filters.sort.direction !== DEFAULT_JOB_SORT.direction) { + params.set('sortBy', filters.sort.field); + params.set('sortDir', filters.sort.direction); + } + + return params.toString(); +} + +export function syncJobFiltersToURL(filters: JobFilters): void { + const params = jobFiltersToURLParams(filters); + const newUrl = params ? `${window.location.pathname}?${params}` : window.location.pathname; + window.history.replaceState({}, '', newUrl); +} + +export function jobFiltersToGraphQLWhere(filters: JobFilters): JobDtoFilterInput | null { + const conditions: JobDtoFilterInput[] = []; + + if (filters.search.trim()) { + conditions.push({ + displayName: { contains: filters.search.trim() }, + }); + } + + if (filters.statuses.length > 0) { + conditions.push({ + status: { in: filters.statuses }, + }); + } + + if (filters.jobType.trim()) { + conditions.push({ + jobType: { eq: filters.jobType.trim() }, + }); + } + + // Always filter to top-level jobs only (no parent) + conditions.push({ + parentJobId: { eq: null }, + }); + + if (conditions.length === 1) { + return conditions[0]; + } + + return { and: conditions }; +} + +export function jobSortToGraphQLOrder(sort: JobSort): JobDtoSortInput[] { + return [{ [sort.field]: sort.direction }]; +} + +export function hasActiveJobFilters(filters: JobFilters): boolean { + return ( + filters.search.trim().length > 0 || + filters.statuses.length > 0 || + filters.jobType.trim().length > 0 + ); +} diff --git a/fictionarchive-web-astro/src/pages/status/index.astro b/fictionarchive-web-astro/src/pages/status/index.astro new file mode 100644 index 0000000..592a8b7 --- /dev/null +++ b/fictionarchive-web-astro/src/pages/status/index.astro @@ -0,0 +1,8 @@ +--- +import AppLayout from '../../layouts/AppLayout.astro'; +import StatusPage from '../../lib/components/StatusPage.svelte'; +--- + + + +