112 lines
3.5 KiB
TypeScript
112 lines
3.5 KiB
TypeScript
import { formatNumber, t } from '@/i18n'
|
|
import type { ActivitySummary, RangeKey } from '@/lib/activity'
|
|
|
|
export type StripKey = 'total' | 'active' | 'failed' | 'security'
|
|
export type StripTone = 'neutral' | 'error' | 'warning' | 'calm'
|
|
|
|
export interface StripCard {
|
|
key: StripKey
|
|
label: string
|
|
value: string
|
|
subline: string
|
|
icon: string | null
|
|
tone: StripTone
|
|
series: number[]
|
|
}
|
|
|
|
export const STRIP_KEYS: StripKey[] = ['total', 'active', 'failed', 'security']
|
|
export const MIN_BAR_HEIGHT = 2
|
|
|
|
function percent(part: number, whole: number) {
|
|
return Math.round((Math.abs(part - whole) / whole) * 100)
|
|
}
|
|
|
|
function totalSubline(summary: ActivitySummary, range: RangeKey) {
|
|
if (summary.total === 0) return { text: t('No data yet'), icon: null }
|
|
if (summary.previous_total === 0) return { text: t('No earlier data'), icon: null }
|
|
if (summary.total === summary.previous_total) return { text: t('No change'), icon: null }
|
|
const value = formatNumber(percent(summary.total, summary.previous_total))
|
|
const text =
|
|
range === 'today'
|
|
? t('{0}% vs previous day', [value])
|
|
: range === '30d'
|
|
? t('{0}% vs previous 30 days', [value])
|
|
: range === 'custom'
|
|
? t('{0}% vs previous period', [value])
|
|
: t('{0}% vs previous 7 days', [value])
|
|
const icon =
|
|
summary.total >= summary.previous_total ? 'i-lucide-trending-up' : 'i-lucide-trending-down'
|
|
return { text, icon }
|
|
}
|
|
|
|
function peopleSubline(summary: ActivitySummary) {
|
|
if (summary.total === 0) return t('No data yet')
|
|
const { users, api_keys: keys } = summary.active_people
|
|
const people = users === 1 ? t('1 person') : t('{0} people', [formatNumber(users)])
|
|
const apiKeys = keys === 1 ? t('1 API key') : t('{0} API keys', [formatNumber(keys)])
|
|
return `${people}, ${apiKeys}`
|
|
}
|
|
|
|
function failedSubline(summary: ActivitySummary) {
|
|
const ratio = (summary.failed / summary.total) * 100
|
|
return t('{0}% of all actions', [
|
|
formatNumber(ratio, {
|
|
minimumFractionDigits: 1,
|
|
maximumFractionDigits: 1,
|
|
}),
|
|
])
|
|
}
|
|
|
|
function flaggedSubline(count: number) {
|
|
return count === 1 ? t('1 flagged') : t('{0} flagged', [formatNumber(count)])
|
|
}
|
|
|
|
export function stripCards(summary: ActivitySummary, range: RangeKey): StripCard[] {
|
|
const total = totalSubline(summary, range)
|
|
const failedCalm = summary.failed === 0
|
|
const securityCalm = summary.security === 0
|
|
return [
|
|
{
|
|
key: 'total',
|
|
label: t('Total actions'),
|
|
value: formatNumber(summary.total),
|
|
subline: total.text,
|
|
icon: total.icon,
|
|
tone: 'neutral',
|
|
series: summary.series.total,
|
|
},
|
|
{
|
|
key: 'active',
|
|
label: t('Active people'),
|
|
value: formatNumber(summary.active_people.users + summary.active_people.api_keys),
|
|
subline: peopleSubline(summary),
|
|
icon: null,
|
|
tone: 'neutral',
|
|
series: summary.series.active,
|
|
},
|
|
{
|
|
key: 'failed',
|
|
label: t('Failed actions'),
|
|
value: formatNumber(summary.failed),
|
|
subline: failedCalm ? t('Nothing failed') : failedSubline(summary),
|
|
icon: failedCalm ? 'i-lucide-circle-check' : 'i-lucide-circle-x',
|
|
tone: failedCalm ? 'calm' : 'error',
|
|
series: summary.series.failed,
|
|
},
|
|
{
|
|
key: 'security',
|
|
label: t('Security events'),
|
|
value: formatNumber(summary.security),
|
|
subline: securityCalm ? t('Nothing to flag') : flaggedSubline(summary.security),
|
|
icon: securityCalm ? 'i-lucide-circle-check' : 'i-lucide-shield-alert',
|
|
tone: securityCalm ? 'calm' : 'warning',
|
|
series: summary.series.security,
|
|
},
|
|
]
|
|
}
|
|
|
|
export function barHeights(series: number[], height: number) {
|
|
const peak = Math.max(...series, 1)
|
|
return series.map((value) => Math.max(MIN_BAR_HEIGHT, Math.round((value / peak) * height)))
|
|
}
|