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 +2 -2
- package/src/state-machines/RecentJobsWidget.tsx +90 -0
- package/src/state-machines/StateMachinesWidget.tsx +7 -59
- package/src/state-machines/index.tsx +5 -1
- package/src/state-machines/presentation.ts +50 -0
- package/src/state-machines/recentJobs.ts +12 -0
- package/src/state-machines/types.ts +17 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "anbaric-plugins",
|
|
3
|
-
"version": "1.
|
|
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.
|
|
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
|
|
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
|
-
|
|
8
|
-
|
|
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
|
-
<
|
|
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
|
-
|
|
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: [
|
|
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 }
|