anbaric-plugins 1.21.5 → 1.23.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "anbaric-plugins",
3
- "version": "1.21.5",
3
+ "version": "1.23.0",
4
4
  "description": "Open-source Anbaric platform plugins: pages and widgets loaded by the hosting server via ANBARIC_PLUGINS",
5
5
  "license": "MIT",
6
6
  "author": "chris@anbaric.ai",
@@ -14,7 +14,7 @@
14
14
  "devDependencies": {
15
15
  "@anbaric/design-system": "file:../anbaric-design-system",
16
16
  "@types/react": "^19.2.14",
17
- "anbaric-cloud-hosting": "^1.21.5",
17
+ "anbaric-cloud-hosting": "^1.23.0",
18
18
  "react": "^19.2.6",
19
19
  "typescript": "^7.0.2"
20
20
  },
@@ -0,0 +1,90 @@
1
+ import { useEffect, useState } from 'react'
2
+
3
+ import { Alert } from '@anbaric/design-system/components/Alert'
4
+ import { Badge } from '@anbaric/design-system/components/Badge'
5
+ import { Card } from '@anbaric/design-system/components/Card'
6
+
7
+ import { cell, formatDate, headerCell, machineHeading, mono, muted, tableScroller } from './presentation'
8
+ import { mostRecent } from './recentJobs'
9
+ import type { Job } from './types'
10
+
11
+ function RecentJobsWidget() {
12
+ const [jobs, setJobs] = useState<Array<Job> | undefined>(undefined)
13
+ const [failed, setFailed] = useState(false)
14
+
15
+ useEffect(() => {
16
+ void (async () => {
17
+ try {
18
+ const response = await fetch('/jobs')
19
+ if (!response.ok) return setFailed(true)
20
+ setJobs(await response.json())
21
+ } catch {
22
+ setFailed(true)
23
+ }
24
+ })()
25
+ }, [])
26
+
27
+ if (failed) {
28
+ return (
29
+ <Alert variant="danger" title="Could not load recent jobs">
30
+ The platform rejected the request — try reloading the page.
31
+ </Alert>
32
+ )
33
+ }
34
+
35
+ const recent = mostRecent(jobs ?? [])
36
+
37
+ return (
38
+ <Card>
39
+ <div
40
+ style={{
41
+ display: 'flex',
42
+ alignItems: 'center',
43
+ justifyContent: 'space-between',
44
+ gap: 'var(--space-md)',
45
+ marginBottom: 'var(--space-md)',
46
+ }}
47
+ >
48
+ <h2 style={machineHeading}>Recent jobs</h2>
49
+ {jobs ? (
50
+ <Badge tone="neutral">
51
+ {jobs.length} {jobs.length === 1 ? 'job' : 'jobs'} in total
52
+ </Badge>
53
+ ) : null}
54
+ </div>
55
+
56
+ {recent.length === 0 ? (
57
+ <p style={{ margin: 0, ...muted }}>No jobs yet.</p>
58
+ ) : (
59
+ <div style={tableScroller}>
60
+ <table style={{ width: '100%', minWidth: '36rem', borderCollapse: 'collapse' }}>
61
+ <thead>
62
+ <tr>
63
+ <th style={headerCell}>Job</th>
64
+ <th style={headerCell}>State machine</th>
65
+ <th style={headerCell}>State</th>
66
+ <th style={headerCell}>Updated</th>
67
+ </tr>
68
+ </thead>
69
+ <tbody>
70
+ {recent.map((job) => (
71
+ <tr key={job.id}>
72
+ <td style={{ ...cell, ...mono }}>{job.id}</td>
73
+ <td style={{ ...cell, ...mono }}>{job.workflowId ?? <span style={muted}>—</span>}</td>
74
+ <td style={cell}>
75
+ <Badge tone="primary" dot>
76
+ {job.state}
77
+ </Badge>
78
+ </td>
79
+ <td style={{ ...cell, whiteSpace: 'nowrap' }}>{formatDate(job.lastUpdated ?? job.startedAt)}</td>
80
+ </tr>
81
+ ))}
82
+ </tbody>
83
+ </table>
84
+ </div>
85
+ )}
86
+ </Card>
87
+ )
88
+ }
89
+
90
+ export { RecentJobsWidget }
@@ -1,65 +1,11 @@
1
- import { useEffect, useState, type CSSProperties } from 'react'
1
+ import { useEffect, useState } from 'react'
2
2
 
3
3
  import { Alert } from '@anbaric/design-system/components/Alert'
4
4
  import { Badge } from '@anbaric/design-system/components/Badge'
5
5
  import { Card } from '@anbaric/design-system/components/Card'
6
6
 
7
- interface StateMachine {
8
- workflowId: string
9
- url: string
10
- }
11
-
12
- interface Job {
13
- id: string
14
- state: string
15
- workflowId?: string
16
- properties: Record<string, unknown>
17
- startedAt?: string
18
- startedBy?: string
19
- lastUpdated?: string
20
- transitions?: Array<{ from: string; to: string; actor: string }>
21
- }
22
-
23
- const formatDate = (iso?: string) =>
24
- iso
25
- ? new Date(iso).toLocaleString(undefined, {
26
- day: 'numeric',
27
- month: 'short',
28
- year: 'numeric',
29
- hour: 'numeric',
30
- minute: '2-digit',
31
- })
32
- : '—'
33
-
34
- const machineHeading: CSSProperties = {
35
- margin: 0,
36
- fontFamily: 'var(--font-title)',
37
- fontSize: '1.15rem',
38
- textTransform: 'var(--title-transform)' as CSSProperties['textTransform'],
39
- }
40
-
41
- const cell: CSSProperties = {
42
- textAlign: 'left',
43
- padding: 'calc(var(--space-sm) / 2) var(--space-sm)',
44
- borderBottom: '1px solid color-mix(in srgb, var(--color-grey) 12%, transparent)',
45
- }
46
-
47
- const headerCell: CSSProperties = {
48
- ...cell,
49
- fontSize: '0.72rem',
50
- textTransform: 'uppercase',
51
- letterSpacing: '0.06em',
52
- color: 'var(--color-foreground-tint-2)',
53
- }
54
-
55
- const mono: CSSProperties = {
56
- fontFamily: 'var(--font-mono)',
57
- fontSize: '0.78rem',
58
- }
59
-
60
- const muted: CSSProperties = {
61
- color: 'var(--color-foreground-tint-2)',
62
- }
7
+ import { cell, formatDate, headerCell, machineHeading, mono, muted, tableScroller } from './presentation'
8
+ import type { Job, StateMachine } from './types'
63
9
 
64
10
  function PropertyList({ job }: { job: Job }) {
65
11
  const entries: Array<[string, unknown]> = [['id', job.id], ...Object.entries(job.properties)]
@@ -96,7 +42,8 @@ function JobsTable({ jobs }: { jobs: Job[] }) {
96
42
  return <p style={{ margin: 0, color: 'var(--color-foreground-tint-2)' }}>No jobs yet.</p>
97
43
  }
98
44
  return (
99
- <table style={{ width: '100%', borderCollapse: 'collapse' }}>
45
+ <div style={tableScroller}>
46
+ <table style={{ width: '100%', minWidth: '44rem', borderCollapse: 'collapse' }}>
100
47
  <thead>
101
48
  <tr>
102
49
  <th style={headerCell}>Properties</th>
@@ -128,7 +75,8 @@ function JobsTable({ jobs }: { jobs: Job[] }) {
128
75
  </tr>
129
76
  ))}
130
77
  </tbody>
131
- </table>
78
+ </table>
79
+ </div>
132
80
  )
133
81
  }
134
82
 
@@ -1,11 +1,15 @@
1
1
  import type { Plugin } from 'anbaric-cloud-hosting'
2
2
 
3
+ import { RecentJobsWidget } from './RecentJobsWidget'
3
4
  import { StateMachinesWidget } from './StateMachinesWidget'
4
5
 
5
6
  const plugin: Plugin = {
6
7
  name: 'state-machines',
7
8
  pages: [{ path: '/', title: 'Dashboard', icon: 'dashboard', navOrder: 0 }],
8
- widgets: [{ page: '/', id: 'state-machines', component: StateMachinesWidget }],
9
+ widgets: [
10
+ { page: '/', id: 'recent-jobs', component: RecentJobsWidget },
11
+ { page: '/', id: 'state-machines', component: StateMachinesWidget },
12
+ ],
9
13
  }
10
14
 
11
15
  export { plugin }
@@ -0,0 +1,50 @@
1
+ import type { CSSProperties } from 'react'
2
+
3
+ const formatDate = (iso?: string) =>
4
+ iso
5
+ ? new Date(iso).toLocaleString(undefined, {
6
+ day: 'numeric',
7
+ month: 'short',
8
+ year: 'numeric',
9
+ hour: 'numeric',
10
+ minute: '2-digit',
11
+ })
12
+ : '—'
13
+
14
+ const machineHeading: CSSProperties = {
15
+ margin: 0,
16
+ fontFamily: 'var(--font-title)',
17
+ fontSize: '1.15rem',
18
+ textTransform: 'var(--title-transform)' as CSSProperties['textTransform'],
19
+ }
20
+
21
+ const cell: CSSProperties = {
22
+ textAlign: 'left',
23
+ padding: 'calc(var(--space-sm) / 2) var(--space-sm)',
24
+ borderBottom: '1px solid color-mix(in srgb, var(--color-grey) 12%, transparent)',
25
+ }
26
+
27
+ const headerCell: CSSProperties = {
28
+ ...cell,
29
+ fontSize: '0.72rem',
30
+ textTransform: 'uppercase',
31
+ letterSpacing: '0.06em',
32
+ color: 'var(--color-foreground-tint-2)',
33
+ }
34
+
35
+ const mono: CSSProperties = {
36
+ fontFamily: 'var(--font-mono)',
37
+ fontSize: '0.78rem',
38
+ }
39
+
40
+ const muted: CSSProperties = {
41
+ color: 'var(--color-foreground-tint-2)',
42
+ }
43
+
44
+ // Tables of job properties get wide; scroll them rather than break the page.
45
+ const tableScroller: CSSProperties = {
46
+ overflowX: 'auto',
47
+ maxWidth: '100%',
48
+ }
49
+
50
+ export { cell, formatDate, headerCell, machineHeading, mono, muted, tableScroller }
@@ -0,0 +1,12 @@
1
+ import type { Job } from './types'
2
+
3
+ const RECENT_LIMIT = 10
4
+
5
+ const updatedAt = (job: Job) => new Date(job.lastUpdated ?? job.startedAt ?? 0).getTime()
6
+
7
+ // Most recently touched first, falling back to when the job started for one
8
+ // that has never been updated.
9
+ const mostRecent = (jobs: Array<Job>, limit: number = RECENT_LIMIT) =>
10
+ [...jobs].sort((left, right) => updatedAt(right) - updatedAt(left)).slice(0, limit)
11
+
12
+ export { RECENT_LIMIT, mostRecent }
@@ -0,0 +1,17 @@
1
+ interface StateMachine {
2
+ workflowId: string
3
+ url: string
4
+ }
5
+
6
+ interface Job {
7
+ id: string
8
+ state: string
9
+ workflowId?: string
10
+ properties: Record<string, unknown>
11
+ startedAt?: string
12
+ startedBy?: string
13
+ lastUpdated?: string
14
+ transitions?: Array<{ from: string; to: string; actor: string }>
15
+ }
16
+
17
+ export type { Job, StateMachine }