@hanzo/dashboard 0.2.1 → 0.2.3
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/README.md +4 -4
- package/package.json +1 -1
- package/src/charts/Charts.tsx +2 -2
- package/src/landing/Hero.tsx +3 -3
- package/src/overview/Overview.tsx +3 -3
- package/src/overview/config.ts +7 -6
- package/src/overview/superadmin.test.ts +36 -0
- package/src/pipeline/Pipeline.tsx +2 -2
- package/src/pipeline/stages.ts +1 -1
package/README.md
CHANGED
|
@@ -84,7 +84,7 @@ const config: OverviewConfig = {
|
|
|
84
84
|
load: async ({ range }) => fromMyApi(await MyApi.overview(range)), // → OverviewData
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
<Overview config={config}
|
|
87
|
+
<Overview config={config} superAdmin={superAdmin(claims)} /> // @hanzo/iam's one reading
|
|
88
88
|
```
|
|
89
89
|
|
|
90
90
|
The loader returns the normalized **`OverviewData`** (`kpi` / `series` / `distribution`
|
|
@@ -178,8 +178,8 @@ Extracted (and generalized: no `~/lib/*` / `~/config` imports) from
|
|
|
178
178
|
|
|
179
179
|
- **Icons** are consumer-supplied (`IconComponent` = a `size`/`color` icon), replacing
|
|
180
180
|
the console's `ProductIcon` from its registry.
|
|
181
|
-
- **`Overview`** drops `~/lib/api` `ApiError`,
|
|
182
|
-
`FadeIn`, and `States` — `
|
|
181
|
+
- **`Overview`** drops `~/lib/api` `ApiError`, the console's SuperAdmin hook, `PageHeader`,
|
|
182
|
+
`FadeIn`, and `States` — `superAdmin` is now a prop (the host's verdict) and the header/fade/error
|
|
183
183
|
chrome is inlined (self-contained).
|
|
184
184
|
- **Landing** takes brand/docs/support/discord/accent from `LandingConfig` (was
|
|
185
185
|
`~/config`). `LandingHero` inlines the hero graphic + accent button (was
|
|
@@ -205,7 +205,7 @@ per component:
|
|
|
205
205
|
`@hanzo/dashboard`; `RailwayDeploy` + `LandingMetrics` import these hooks.
|
|
206
206
|
3. **Overview** — replace `overview/living/{config,logic,tiles,LivingOverview}` with
|
|
207
207
|
thin wrappers over the package. The console's `LivingOverviewModule` keeps its
|
|
208
|
-
registry-router wrapper and passes `
|
|
208
|
+
registry-router wrapper and passes `superAdmin={useIsSuperAdmin()}` +
|
|
209
209
|
maps `ApiError` into the driver's caught error. `PageHeader`/`FadeIn`/`States`
|
|
210
210
|
stay in console (they're used elsewhere) — the driver now inlines its own.
|
|
211
211
|
4. **Landing** — replace `products/landing/*` with the package; the console builds a
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hanzo/dashboard",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Hanzo Dashboard — the reusable dashboard layer on @hanzo/gui: monochrome SVG charts, a videogame-like LivingOverview (count-up KPIs, live sparklines, streaming feed, health), the ProductLanding kit, and a Pipeline deploy animation. Pure (data via props/loaders), typed, reduced-motion-guarded. Extracted from the Hanzo Cloud Console so every Hanzo app composes ONE dashboard system.",
|
|
6
6
|
"exports": {
|
package/src/charts/Charts.tsx
CHANGED
|
@@ -24,13 +24,13 @@ import { Text, XStack, YStack, useTheme } from '@hanzo/gui'
|
|
|
24
24
|
|
|
25
25
|
/** Distinct hues for categorical series (e.g. spend-by-model donut), dark-tuned. */
|
|
26
26
|
export const CHART_PALETTE = [
|
|
27
|
-
'#
|
|
27
|
+
'#e4e4e7',
|
|
28
28
|
'#23c562',
|
|
29
29
|
'#ff9f45',
|
|
30
30
|
'#3aa0ff',
|
|
31
31
|
'#ff5d8f',
|
|
32
32
|
'#16c0c8',
|
|
33
|
-
'#
|
|
33
|
+
'#a1a1aa',
|
|
34
34
|
'#f4c245',
|
|
35
35
|
]
|
|
36
36
|
export const CHART_OTHER = '#64748b'
|
package/src/landing/Hero.tsx
CHANGED
|
@@ -96,7 +96,7 @@ function HeroGraphic({ size = 170, accent = CHART_PALETTE[0] }: { size?: number;
|
|
|
96
96
|
<stop offset="100%" stopColor={accent} stopOpacity="0" />
|
|
97
97
|
</radialGradient>
|
|
98
98
|
<linearGradient id="hz-hero-cube" x1="0" y1="0" x2="1" y2="1">
|
|
99
|
-
<stop offset="0%" stopColor="#
|
|
99
|
+
<stop offset="0%" stopColor="#e4e4e7" />
|
|
100
100
|
<stop offset="100%" stopColor={accent} />
|
|
101
101
|
</linearGradient>
|
|
102
102
|
</defs>
|
|
@@ -106,9 +106,9 @@ function HeroGraphic({ size = 170, accent = CHART_PALETTE[0] }: { size?: number;
|
|
|
106
106
|
))}
|
|
107
107
|
<g transform="translate(100 96)">
|
|
108
108
|
<polygon points="0,-34 30,-17 30,17 0,34 -30,17 -30,-17" fill="url(#hz-hero-cube)" fillOpacity="0.9" />
|
|
109
|
-
<polygon points="0,-34 30,-17 0,0 -30,-17" fill="#
|
|
109
|
+
<polygon points="0,-34 30,-17 0,0 -30,-17" fill="#f4f4f5" fillOpacity="0.85" />
|
|
110
110
|
<polygon points="0,0 30,-17 30,17 0,34" fill={accent} fillOpacity="0.7" />
|
|
111
|
-
<polygon points="0,0 -30,-17 -30,17 0,34" fill="#
|
|
111
|
+
<polygon points="0,0 -30,-17 -30,17 0,34" fill="#52525b" fillOpacity="0.7" />
|
|
112
112
|
</g>
|
|
113
113
|
</svg>
|
|
114
114
|
)
|
|
@@ -35,7 +35,7 @@ const RANGES: { key: OverviewRange; label: string }[] = [
|
|
|
35
35
|
{ key: '30d', label: '30D' },
|
|
36
36
|
]
|
|
37
37
|
|
|
38
|
-
export function Overview({ config, allOrgs,
|
|
38
|
+
export function Overview({ config, allOrgs, superAdmin = false }: { config: OverviewConfig; allOrgs?: boolean; superAdmin?: boolean }) {
|
|
39
39
|
const [range, setRange] = useState<OverviewRange>('24h')
|
|
40
40
|
const [state, setState] = useState<Loadable<OverviewData>>({ s: 'loading' })
|
|
41
41
|
const [refreshing, setRefreshing] = useState(false)
|
|
@@ -57,7 +57,7 @@ export function Overview({ config, allOrgs, isGlobalAdmin = false }: { config: O
|
|
|
57
57
|
const first = state.s === 'loading'
|
|
58
58
|
if (!first) setRefreshing(true)
|
|
59
59
|
config
|
|
60
|
-
.load({ range, allOrgs,
|
|
60
|
+
.load({ range, allOrgs, superAdmin })
|
|
61
61
|
.then((v) => {
|
|
62
62
|
if (id === reqRef.current) setState({ s: 'ready', v })
|
|
63
63
|
})
|
|
@@ -74,7 +74,7 @@ export function Overview({ config, allOrgs, isGlobalAdmin = false }: { config: O
|
|
|
74
74
|
if (id === reqRef.current) setRefreshing(false)
|
|
75
75
|
})
|
|
76
76
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
77
|
-
}, [range, allOrgs,
|
|
77
|
+
}, [range, allOrgs, superAdmin, tick, config])
|
|
78
78
|
|
|
79
79
|
const onRange = (k: OverviewRange) => {
|
|
80
80
|
setRange(k)
|
package/src/overview/config.ts
CHANGED
|
@@ -142,15 +142,16 @@ export type LiveConfig = {
|
|
|
142
142
|
/** The context passed to a loader (range + org scope). */
|
|
143
143
|
export type OverviewContext = {
|
|
144
144
|
range: OverviewRange
|
|
145
|
-
/**
|
|
145
|
+
/** SuperAdmin all-orgs god view. */
|
|
146
146
|
allOrgs?: boolean
|
|
147
147
|
/**
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
148
|
+
* The host's SuperAdmin verdict (a person whose home org is the reserved `admin`
|
|
149
|
+
* org, as the server or @hanzo/iam `superAdmin` states it — never derived here).
|
|
150
|
+
* Loaders use this to SKIP admin-only aggregates that a tenant user's browser
|
|
151
|
+
* would only ever get a 403 from, going straight to the org-scoped source
|
|
152
|
+
* instead — the board renders the same, minus the console noise.
|
|
152
153
|
*/
|
|
153
|
-
|
|
154
|
+
superAdmin?: boolean
|
|
154
155
|
}
|
|
155
156
|
|
|
156
157
|
export type OverviewRange = '24h' | '7d' | '30d'
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// The dashboard decides no authority. SuperAdmin is the HOST's verdict (the server's
|
|
2
|
+
// `isSuperAdmin`, or @hanzo/iam `superAdmin(claims)` — a person whose home org is the
|
|
3
|
+
// reserved `admin` org), and the overview only carries it to the product's loader
|
|
4
|
+
// under that one name, so a loader can skip a fleet aggregate the viewer would be
|
|
5
|
+
// refused. These hold the package to that: no predicate of its own, one name.
|
|
6
|
+
import { describe, expect, test } from 'bun:test'
|
|
7
|
+
import { readFileSync, readdirSync, statSync } from 'node:fs'
|
|
8
|
+
import { join } from 'node:path'
|
|
9
|
+
|
|
10
|
+
const SRC = join(import.meta.dir, '..')
|
|
11
|
+
|
|
12
|
+
function sources(dir: string): string[] {
|
|
13
|
+
return readdirSync(dir).flatMap((name) => {
|
|
14
|
+
const p = join(dir, name)
|
|
15
|
+
if (statSync(p).isDirectory()) return sources(p)
|
|
16
|
+
return /\.(ts|tsx)$/.test(name) && !/\.test\.tsx?$/.test(name) ? [p] : []
|
|
17
|
+
})
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe('SuperAdmin in @hanzo/dashboard', () => {
|
|
21
|
+
test('the overview hands the loader the host verdict as superAdmin', () => {
|
|
22
|
+
const overview = readFileSync(join(SRC, 'overview', 'Overview.tsx'), 'utf8')
|
|
23
|
+
expect(overview).toMatch(/superAdmin = false \}: \{ config: OverviewConfig; allOrgs\?: boolean; superAdmin\?: boolean \}/)
|
|
24
|
+
expect(overview).toContain('.load({ range, allOrgs, superAdmin })')
|
|
25
|
+
const config = readFileSync(join(SRC, 'overview', 'config.ts'), 'utf8')
|
|
26
|
+
expect(config).toMatch(/\n superAdmin\?: boolean\n/)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test('no source decides SuperAdmin itself or names it another way', () => {
|
|
30
|
+
for (const file of sources(SRC)) {
|
|
31
|
+
const text = readFileSync(file, 'utf8')
|
|
32
|
+
expect({ file, hit: /isGlobalAdmin|global[ -]admin/i.test(text) }).toEqual({ file, hit: false })
|
|
33
|
+
expect({ file, hit: /owner\s*===?\s*['"]admin['"]|orgs\?*\.?\[0\]/.test(text) }).toEqual({ file, hit: false })
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
})
|
|
@@ -16,7 +16,7 @@ import { useReducedMotion } from '../motion/hooks'
|
|
|
16
16
|
import type { PipelineStage, StageState } from './stages'
|
|
17
17
|
import { Text, XStack, YStack } from '@hanzo/gui'
|
|
18
18
|
|
|
19
|
-
const ACCENT = '#
|
|
19
|
+
const ACCENT = '#e4e4e7'
|
|
20
20
|
const GREEN = '#23c562'
|
|
21
21
|
const RED = '#ff5d8f'
|
|
22
22
|
const MUTED = 'rgba(148,163,184,0.28)'
|
|
@@ -26,7 +26,7 @@ export interface PipelineProps {
|
|
|
26
26
|
stages: PipelineStage[]
|
|
27
27
|
/** A short status label shown above the track (e.g. `pipelineModel(...).label`). */
|
|
28
28
|
label?: string
|
|
29
|
-
/** Accent tone for the filled track + active stage. Defaults by state (green live / red error /
|
|
29
|
+
/** Accent tone for the filled track + active stage. Defaults by state (green live / red error / ink). */
|
|
30
30
|
tone?: string
|
|
31
31
|
/** Animate the active leg + pulse. Default true; reduced motion still forces static. */
|
|
32
32
|
animate?: boolean
|
package/src/pipeline/stages.ts
CHANGED
|
@@ -151,6 +151,6 @@ export function pipelineModel(status?: string | null, furthestReached = -1): Pip
|
|
|
151
151
|
}
|
|
152
152
|
|
|
153
153
|
/** The accent tone for a model: red on failure, green on live, else the accent. */
|
|
154
|
-
export function pipelineTone(model: Pick<PipelineModel, 'live' | 'errored'>, accent = '#
|
|
154
|
+
export function pipelineTone(model: Pick<PipelineModel, 'live' | 'errored'>, accent = '#e4e4e7'): string {
|
|
155
155
|
return model.errored ? '#ff5d8f' : model.live ? '#23c562' : accent
|
|
156
156
|
}
|