@zthun/romulator-web 1.18.4 → 1.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.html CHANGED
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>Romulator: Organize your Games</title>
7
- <script type="module" crossorigin src="/assets/index-CZMAQ_HC.js"></script>
7
+ <script type="module" crossorigin src="/assets/index-BImCVu5p.js"></script>
8
8
  </head>
9
9
  <body>
10
10
  <div id="zthunworks-romulator"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zthun/romulator-web",
3
- "version": "1.18.4",
3
+ "version": "1.19.0",
4
4
  "description": "Romulator frontend",
5
5
  "author": "Anthony Bonta",
6
6
  "license": "MIT",
@@ -18,18 +18,18 @@
18
18
  "access": "public"
19
19
  },
20
20
  "devDependencies": {
21
- "@types/node": "^25.1.0",
21
+ "@types/node": "^25.2.3",
22
22
  "@zthun/cirque": "^7.2.5",
23
23
  "@zthun/cirque-du-react": "^7.2.5",
24
24
  "@zthun/fashion-boutique": "^13.0.4",
25
25
  "@zthun/fashion-tailor": "^13.0.4",
26
26
  "@zthun/fashion-theme": "^13.0.4",
27
- "@zthun/helpful-fn": "^9.11.4",
28
- "@zthun/helpful-query": "^9.11.4",
29
- "@zthun/helpful-react": "^9.11.4",
27
+ "@zthun/helpful-fn": "^9.11.10",
28
+ "@zthun/helpful-query": "^9.11.10",
29
+ "@zthun/helpful-react": "^9.11.10",
30
30
  "@zthun/janitor-build-config": "^19.5.6",
31
31
  "@zthun/janitor-ts-config": "^19.5.3",
32
- "@zthun/romulator-client": "^1.18.4",
32
+ "@zthun/romulator-client": "^1.19.0",
33
33
  "@zthun/webigail-http": "^5.0.5",
34
34
  "@zthun/webigail-rest": "^5.0.5",
35
35
  "@zthun/webigail-url": "^5.0.5",
@@ -44,5 +44,5 @@
44
44
  "vitest": "^4.0.18",
45
45
  "vitest-mock-extended": "^3.1.0"
46
46
  },
47
- "gitHead": "e3f4de76344181d22641550c8d562f50f0498d94"
47
+ "gitHead": "8e97e1bc38a9b1f38dd055093cdb5521fd621d84"
48
48
  }
package/src/app/app.tsx CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  import { createDarkTheme } from "@zthun/fashion-theme";
10
10
  import { ZRomulatorGamePage } from "../games/game-page.js";
11
11
  import { ZRomulatorGamesPage } from "../games/games-page.js";
12
+ import { ZRomulatorJobsPage } from "../jobs/jobs-page.js";
12
13
  import { ZRomulatorMenu } from "../menu/menu.js";
13
14
  import { ZRomulatorSettingPage } from "../settings/setting-page.js";
14
15
  import { ZRomulatorSettingsPage } from "../settings/settings-page.js";
@@ -36,6 +37,7 @@ export function ZRomulatorApp() {
36
37
  <ZRoute path="/systems/:id" element={<ZRomulatorSystemPage />} />
37
38
  <ZRoute path="/games" element={<ZRomulatorGamesPage />} />
38
39
  <ZRoute path="/games/:id" element={<ZRomulatorGamePage />} />
40
+ <ZRoute path="/jobs" element={<ZRomulatorJobsPage />} />
39
41
  <ZRoute path="" element={<ZNavigate to="/systems" />} />
40
42
  <ZRoute path="*" element={<ZNotFound />} />
41
43
  </ZRouteMap>
@@ -0,0 +1,54 @@
1
+ import type { IZComponentValueReadonly } from "@zthun/fashion-boutique";
2
+ import {
3
+ useFashionTheme,
4
+ ZContentTitle,
5
+ ZGrid,
6
+ ZLabel,
7
+ ZParagraph,
8
+ ZStack,
9
+ ZTile,
10
+ } from "@zthun/fashion-boutique";
11
+ import { ZSizeFixed, ZSizeVaried } from "@zthun/fashion-tailor";
12
+ import { formatDateTime } from "@zthun/helpful-fn";
13
+ import { type IZJob } from "@zthun/romulator-client";
14
+ import { useJobStatusMetadata } from "./use-job-status-metadata.js";
15
+ import { useJobTypeMetadata } from "./use-job-type-metadata.js";
16
+
17
+ export interface IZJobTile extends Required<IZComponentValueReadonly<IZJob>> {}
18
+
19
+ export function ZJobTile(props: IZJobTile) {
20
+ const { value } = props;
21
+ const { body } = useFashionTheme();
22
+ const { status, percent, type, createdAt } = value;
23
+ const created = formatDateTime(createdAt);
24
+
25
+ const _type = useJobTypeMetadata(type);
26
+ const { name, description, avatar } = _type;
27
+
28
+ const _status = useJobStatusMetadata(status);
29
+
30
+ return (
31
+ <ZTile className="ZRomulatorJobTile-root" fashion={body} name={value.id}>
32
+ <ZStack
33
+ height={ZSizeVaried.Full}
34
+ width={ZSizeVaried.Full}
35
+ gap={ZSizeFixed.Large}
36
+ >
37
+ <ZContentTitle
38
+ heading={name}
39
+ subHeading={description}
40
+ avatar={avatar}
41
+ suffix={_status.avatar}
42
+ />
43
+ <ZGrid columns="auto 1fr" gap={ZSizeFixed.Small}>
44
+ <ZLabel>Created:</ZLabel>
45
+ <ZParagraph compact>{created}</ZParagraph>
46
+ <ZLabel>Status</ZLabel>
47
+ <ZParagraph compact>{_status.name}</ZParagraph>
48
+ <ZLabel>Progress</ZLabel>
49
+ <ZParagraph>{percent}%</ZParagraph>
50
+ </ZGrid>
51
+ </ZStack>
52
+ </ZTile>
53
+ );
54
+ }
@@ -0,0 +1,31 @@
1
+ import { ZCircusBy, ZCircusComponentModel } from "@zthun/cirque";
2
+ import {
3
+ ZGridViewComponentModel,
4
+ ZTileComponentModel,
5
+ } from "@zthun/fashion-boutique";
6
+
7
+ export class ZRomulatorJobsPageComponentModel extends ZCircusComponentModel {
8
+ public static readonly Selector = ".ZRomulatorJobsPage-root";
9
+
10
+ public grid(): Promise<ZGridViewComponentModel> {
11
+ return ZCircusBy.first(this.driver, ZGridViewComponentModel);
12
+ }
13
+
14
+ public async load() {
15
+ const grid = await this.grid();
16
+ const suspense = await grid.suspense();
17
+ await suspense.load();
18
+
19
+ return grid;
20
+ }
21
+
22
+ public async jobs(): Promise<ZTileComponentModel[]> {
23
+ const grid = await this.load();
24
+
25
+ return ZCircusBy.all(
26
+ grid.driver,
27
+ ZTileComponentModel,
28
+ ".ZRomulatorJobTile-root",
29
+ );
30
+ }
31
+ }
@@ -0,0 +1,81 @@
1
+ import {
2
+ ZCircusBy,
3
+ type IZCircusDriver,
4
+ type IZCircusSetup,
5
+ } from "@zthun/cirque";
6
+ import { ZCircusSetupRenderer } from "@zthun/cirque-du-react";
7
+ import { ZTestRouter } from "@zthun/fashion-boutique";
8
+ import { ZDataSourceStatic } from "@zthun/helpful-query";
9
+ import { ZJobBuilder } from "@zthun/romulator-client";
10
+ import { createMemoryHistory } from "history";
11
+ import type { Mocked } from "vitest";
12
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
13
+ import { mock } from "vitest-mock-extended";
14
+ import { ZRomulatorJobsPageComponentModel } from "./jobs-page.cm.mjs";
15
+ import { ZRomulatorJobsPage } from "./jobs-page.js";
16
+ import {
17
+ ZRomulatorJobsServiceContext,
18
+ type IZRomulatorJobsService,
19
+ } from "./jobs-service.js";
20
+
21
+ describe("ZRomulatorJobsPage", () => {
22
+ let _renderer: IZCircusSetup | undefined;
23
+ let _driver: IZCircusDriver | undefined;
24
+
25
+ const now = new Date().toJSON();
26
+
27
+ const alpha = new ZJobBuilder().guid().idle().createdAt(now).build();
28
+ const bravo = new ZJobBuilder()
29
+ .guid()
30
+ .running()
31
+ .createdAt(now)
32
+ .percent(24)
33
+ .build();
34
+ const charlie = new ZJobBuilder().guid().canceled().build();
35
+ const delta = new ZJobBuilder().guid().failed().build();
36
+ const foxtrot = new ZJobBuilder().guid().success().build();
37
+ const jobs = [alpha, bravo, charlie, delta, foxtrot];
38
+
39
+ let service: Mocked<IZRomulatorJobsService>;
40
+
41
+ beforeEach(() => {
42
+ service = mock<IZRomulatorJobsService>();
43
+ const source = new ZDataSourceStatic(jobs);
44
+
45
+ service.retrieve.mockImplementation((r) => source.retrieve(r));
46
+ service.count.mockImplementation((r) => source.count(r));
47
+ });
48
+
49
+ afterEach(async () => {
50
+ await _driver?.destroy?.();
51
+ await _renderer?.destroy?.();
52
+ });
53
+
54
+ const createTestTarget = async () => {
55
+ const history = createMemoryHistory();
56
+
57
+ const element = (
58
+ <ZTestRouter location={history.location} navigator={history}>
59
+ <ZRomulatorJobsServiceContext.Provider value={service}>
60
+ <ZRomulatorJobsPage />;
61
+ </ZRomulatorJobsServiceContext.Provider>
62
+ </ZTestRouter>
63
+ );
64
+
65
+ _renderer = new ZCircusSetupRenderer(element);
66
+ _driver = await _renderer.setup();
67
+
68
+ return ZCircusBy.first(_driver, ZRomulatorJobsPageComponentModel);
69
+ };
70
+
71
+ it("should list all jobs", async () => {
72
+ // Arrange.
73
+ const target = await createTestTarget();
74
+
75
+ // Act.
76
+ const actual = await target.jobs();
77
+
78
+ // Assert.
79
+ expect(actual.length).toEqual(jobs.length);
80
+ });
81
+ });
@@ -0,0 +1,62 @@
1
+ import {
2
+ ZBreadcrumbsLocation,
3
+ ZCard,
4
+ ZGrid,
5
+ ZGridView,
6
+ ZIconFontAwesome,
7
+ ZSearch,
8
+ ZStack,
9
+ } from "@zthun/fashion-boutique";
10
+ import { ZSizeFixed, ZSizeVaried } from "@zthun/fashion-tailor";
11
+ import { ZDataRequestBuilder } from "@zthun/helpful-query";
12
+ import { useState } from "react";
13
+ import { ZJobTile } from "./job-tile.js";
14
+ import { useJobsService } from "./jobs-service.js";
15
+
16
+ export function ZRomulatorJobsPage() {
17
+ const jobs = useJobsService();
18
+ const [request, setRequest] = useState(new ZDataRequestBuilder().build());
19
+
20
+ return (
21
+ <ZStack
22
+ className="ZRomulatorJobsPage-root"
23
+ gap={ZSizeFixed.Medium}
24
+ width={ZSizeVaried.Full}
25
+ >
26
+ <ZBreadcrumbsLocation />
27
+ <ZCard
28
+ width={ZSizeVaried.Full}
29
+ TitleProps={{
30
+ avatar: (
31
+ <ZIconFontAwesome name="briefcase" width={ZSizeFixed.Medium} />
32
+ ),
33
+ heading: "Jobs",
34
+ subHeading: "What's running in the background",
35
+ }}
36
+ >
37
+ <ZGridView
38
+ heading={
39
+ <ZGrid
40
+ align={{ items: "flex-end" }}
41
+ columns={{ xl: "1fr auto", sm: "1fr" }}
42
+ gap={ZSizeFixed.Medium}
43
+ >
44
+ <ZSearch value={request} onValueChange={setRequest} />
45
+ </ZGrid>
46
+ }
47
+ GridProps={{
48
+ columns: {
49
+ xl: "1fr 1fr 1fr",
50
+ lg: "1fr 1fr",
51
+ md: "1fr",
52
+ },
53
+ gap: ZSizeFixed.Medium,
54
+ }}
55
+ dataSource={jobs}
56
+ renderItem={(job) => <ZJobTile key={job.id} value={job} />}
57
+ value={request}
58
+ />
59
+ </ZCard>
60
+ </ZStack>
61
+ );
62
+ }
@@ -0,0 +1,41 @@
1
+ import type { IZDataSource } from "@zthun/helpful-query";
2
+ import type { IZJob } from "@zthun/romulator-client";
3
+ import { ZHttpService } from "@zthun/webigail-http";
4
+ import { ZRestfulService, type IZRestfulGet } from "@zthun/webigail-rest";
5
+ import { ZUrlBuilder } from "@zthun/webigail-url";
6
+ import { createContext, useContext } from "react";
7
+ import { ZRomulatorEnvironmentBuilder } from "../environment/environment.mjs";
8
+
9
+ /**
10
+ * A service that retrieves jobs.
11
+ */
12
+ export interface IZRomulatorJobsService
13
+ extends IZRestfulGet<IZJob>, IZDataSource<IZJob> {}
14
+
15
+ /**
16
+ * Creates the default implementation of the jobs service.
17
+ *
18
+ * @returns
19
+ * The default implementation of the jobs service
20
+ */
21
+ export function createDefaultJobsService(): IZRomulatorJobsService {
22
+ const { api } = new ZRomulatorEnvironmentBuilder().build();
23
+ const endpoint = new ZUrlBuilder().parse(api).append("jobs").build();
24
+ const http = new ZHttpService();
25
+ return new ZRestfulService<IZJob>(http, endpoint);
26
+ }
27
+
28
+ /**
29
+ * The injection context for the jobs service.
30
+ */
31
+ export const ZRomulatorJobsServiceContext = createContext(
32
+ createDefaultJobsService(),
33
+ );
34
+
35
+ /**
36
+ * Returns the current injectable {@link IZRomulatorJobsService} implementation.
37
+ *
38
+ * @returns
39
+ * The current service implementation to manage jobs
40
+ */
41
+ export const useJobsService = () => useContext(ZRomulatorJobsServiceContext);
@@ -0,0 +1,58 @@
1
+ import { ZIconFontAwesome } from "@zthun/fashion-boutique";
2
+ import { ZSizeFixed } from "@zthun/fashion-tailor";
3
+ import type { IZEnumInfo } from "@zthun/helpful-fn";
4
+ import { firstDefined, ZEnumInfoBuilder } from "@zthun/helpful-fn";
5
+ import { ZJobStatus } from "@zthun/romulator-client";
6
+ import { useMemo } from "react";
7
+
8
+ export function useJobStatusMetadata(status?: ZJobStatus) {
9
+ const lookup = useMemo<Record<ZJobStatus, IZEnumInfo<ZJobStatus>>>(
10
+ () => ({
11
+ [ZJobStatus.Idle]: new ZEnumInfoBuilder(ZJobStatus.Idle)
12
+ .name("Idle")
13
+ .description("Waiting to start")
14
+ .avatar(
15
+ <ZIconFontAwesome name="hourglass-start" width={ZSizeFixed.Small} />,
16
+ )
17
+ .build(),
18
+
19
+ [ZJobStatus.Canceled]: new ZEnumInfoBuilder(ZJobStatus.Canceled)
20
+ .name("Canceled")
21
+ .description("The job was canceled by the user or never finished")
22
+ .avatar(<ZIconFontAwesome name="x-mark" width={ZSizeFixed.Small} />)
23
+ .build(),
24
+
25
+ [ZJobStatus.Failed]: new ZEnumInfoBuilder(ZJobStatus.Failed)
26
+ .name("Failed")
27
+ .description("Something went wrong with job")
28
+ .avatar(
29
+ <ZIconFontAwesome
30
+ name="circle-exclamation"
31
+ width={ZSizeFixed.Small}
32
+ />,
33
+ )
34
+ .build(),
35
+
36
+ [ZJobStatus.Running]: new ZEnumInfoBuilder(ZJobStatus.Running)
37
+ .name("Running")
38
+ .description("Job is currently processing")
39
+ .avatar(
40
+ <ZIconFontAwesome
41
+ name="spinner"
42
+ animation="spin"
43
+ width={ZSizeFixed.Small}
44
+ />,
45
+ )
46
+ .build(),
47
+
48
+ [ZJobStatus.Success]: new ZEnumInfoBuilder(ZJobStatus.Success)
49
+ .name("Success")
50
+ .description("Job completed successfully")
51
+ .avatar(<ZIconFontAwesome name="check" width={ZSizeFixed.Small} />)
52
+ .build(),
53
+ }),
54
+ [],
55
+ );
56
+
57
+ return lookup[firstDefined(ZJobStatus.Idle, status)];
58
+ }
@@ -0,0 +1,39 @@
1
+ import { ZIconFontAwesome } from "@zthun/fashion-boutique";
2
+ import { ZSizeFixed } from "@zthun/fashion-tailor";
3
+ import type { IZEnumInfo } from "@zthun/helpful-fn";
4
+ import { firstDefined, ZEnumInfoBuilder } from "@zthun/helpful-fn";
5
+ import { ZJobType } from "@zthun/romulator-client";
6
+ import { useMemo } from "react";
7
+
8
+ export function useJobTypeMetadata(type?: ZJobType) {
9
+ const _type = firstDefined(ZJobType.Unknown, type);
10
+ const lookup: Record<ZJobType, IZEnumInfo<ZJobType>> = useMemo(
11
+ () => ({
12
+ [ZJobType.Unknown]: new ZEnumInfoBuilder(ZJobType.Unknown)
13
+ .name("Unknown")
14
+ .description("Invalid job")
15
+ .avatar(<ZIconFontAwesome name="question" width={ZSizeFixed.Medium} />)
16
+ .build(),
17
+
18
+ [ZJobType.Ping]: new ZEnumInfoBuilder(ZJobType.Ping)
19
+ .name("Ping")
20
+ .description("Test the job framework")
21
+ .avatar(
22
+ <ZIconFontAwesome
23
+ name="table-tennis-paddle-ball"
24
+ width={ZSizeFixed.Medium}
25
+ />,
26
+ )
27
+ .build(),
28
+
29
+ [ZJobType.Scrape]: new ZEnumInfoBuilder(ZJobType.Scrape)
30
+ .name("Scrape")
31
+ .description("Retrieves all media and metadata for your games")
32
+ .avatar(<ZIconFontAwesome name="image" width={ZSizeFixed.Medium} />)
33
+ .build(),
34
+ }),
35
+ [],
36
+ );
37
+
38
+ return lookup[_type];
39
+ }
@@ -23,6 +23,7 @@ export class ZRomulatorMenuComponentModel extends ZCircusComponentModel {
23
23
 
24
24
  public systems = this.listItem.bind(this, "systems");
25
25
  public games = this.listItem.bind(this, "games");
26
+ public jobs = this.listItem.bind(this, "jobs");
26
27
  public settings = this.listItem.bind(this, "settings");
27
28
 
28
29
  public drawer(): Promise<ZDialogComponentModel> {
@@ -74,7 +74,7 @@ describe("ZRomulatorMenu", () => {
74
74
  });
75
75
 
76
76
  describe("Navigation", () => {
77
- type NavigationName = "systems" | "settings" | "steam" | "audits" | "games";
77
+ type NavigationName = "systems" | "settings" | "jobs" | "games";
78
78
 
79
79
  const shouldNavigateTo = async (expected: string, name: NavigationName) => {
80
80
  // Arrange.
@@ -89,14 +89,18 @@ describe("ZRomulatorMenu", () => {
89
89
  expect(_history.location.pathname).toEqual(expected);
90
90
  };
91
91
 
92
- it("should navigation to the systems page", async () => {
92
+ it("should navigate to the systems page", async () => {
93
93
  await shouldNavigateTo("/systems", "systems");
94
94
  });
95
95
 
96
- it("should navigation to the games page", async () => {
96
+ it("should navigate to the games page", async () => {
97
97
  await shouldNavigateTo("/games", "games");
98
98
  });
99
99
 
100
+ it("should navigate to the jobs page", async () => {
101
+ await shouldNavigateTo("/jobs", "jobs");
102
+ });
103
+
100
104
  it("should navigate to the settings page", async () => {
101
105
  await shouldNavigateTo("/settings", "settings");
102
106
  });
package/src/menu/menu.tsx CHANGED
@@ -85,6 +85,19 @@ export function ZRomulatorMenu() {
85
85
  />
86
86
  </ZListItem>
87
87
 
88
+ <ZListItem
89
+ name="jobs"
90
+ interactive
91
+ cursor="pointer"
92
+ onClick={navigateAndClose.bind(null, "/jobs")}
93
+ >
94
+ <ZContentTitle
95
+ avatar={<ZIconFontAwesome name="briefcase" />}
96
+ heading={<ZH3 compact>Jobs</ZH3>}
97
+ subHeading={<ZCaption>What's running in the background</ZCaption>}
98
+ />
99
+ </ZListItem>
100
+
88
101
  <ZListItem
89
102
  name="settings"
90
103
  interactive