Files
TREK/client/src/components/Admin/useRangeBypass.ts
T
Maurice 0067b0ff19 fix(plugins): let an admin run a plugin outside its TREK range, and tidy the plugins panel
TREK 5.0 left most registry plugins blocked because their authors still declare
"<5.0.0", and the only way past that was TREK_PLUGINS_IGNORE_TREK_RANGE for every
plugin at once. An admin can now install or switch on one such plugin after a
warning; the consent is kept on its row for that plugin version and TREK major,
so an update or a new major asks again. The environment switch works as before.

Discover marks a plugin not confirmed for this TREK on its card, and Install opens
the warning first. Admin > Plugins reads cleaner: one row of alike facts per plugin
with each state that needs attention on its own line, type, status and order behind
one Filter button, and the containment notes in a dialog behind the shield in the
header. The row, the filter menu and the dialog moved into their own files.
2026-10-26 09:49:22 +01:00

130 lines
5.0 KiB
TypeScript

import { useState } from 'react'
import { AlertTriangle } from 'lucide-react'
import { useTranslation } from '../../i18n'
type T = (k: string, p?: Record<string, unknown>) => string
/**
* The server's "this only went through because the version check was set aside" marker:
* by an admin for this one plugin (`acceptedByAdmin`), or for every plugin through
* TREK_PLUGINS_IGNORE_TREK_RANGE.
*/
export interface TrekRangeBypass { trekRange: string | null; hostVersion: string; acceptedByAdmin?: boolean }
/**
* What the range-bypass warning needs to say, and what to do if the admin accepts (null = a
* notice). `action` names what the confirm button does: install (the default) or activate.
*/
export interface RangeWarning extends TrekRangeBypass {
name: string
onConfirm: (() => void) | null
action?: 'install' | 'activate'
}
/**
* The chip a row carries while it runs outside its declared range on the operator's
* say-so. Not a blocker — the toggle works — but a warning that must outlive the install
* dialog for as long as the plugin runs here. Null when nothing was bypassed.
*/
export function bypassChip(b: TrekRangeBypass | null | undefined, t: T) {
if (!b) return null
const label = b.acceptedByAdmin
? b.trekRange
? t('admin.plugins.dep.trekAccepted', { range: b.trekRange, host: b.hostVersion })
: t('admin.plugins.dep.trekAcceptedUnknown')
: b.trekRange
? t('admin.plugins.dep.trekBypassed', { range: b.trekRange, host: b.hostVersion })
: t('admin.plugins.dep.trekBypassedUnknown')
return {
icon: AlertTriangle,
label,
blocked: false,
warn: true,
}
}
/**
* The Install button for a registry entry the server flagged incompatible: the server will
* take the newest version regardless, so the button says exactly that — and carries what
* the warning has to say first. `byAdmin` is the admin's own "install anyway" for this one
* plugin; without it the operator set TREK_PLUGINS_IGNORE_TREK_RANGE for every plugin.
* `title` is the shell's existing "why" line (needs TREK X — this server runs Y).
*/
export function bypassOffer(
item: { name: string; trek?: string | null; hostVersion?: string },
t: T,
title: string,
byAdmin = false
) {
const warn: RangeWarning = {
name: item.name,
trekRange: item.trek ?? null,
hostVersion: item.hostVersion ?? '?',
acceptedByAdmin: byAdmin,
onConfirm: null,
}
return { blocked: false, label: t('admin.plugins.install'), title, warn }
}
/**
* Title + body of the warning, for either mode: a CONFIRM step before a registry install
* the server would otherwise refuse (`onConfirm` set), or a plain NOTICE after a path that
* could not ask first — sideload, dev-link, update, dependency download. The copy is the
* whole point: the admin is accepting that the author never vouched for this TREK and
* that, in rare cases, that can cost them data.
*/
export function rangeWarningCopy(
w: RangeWarning,
t: T
): { title: string; body: string; confirm: boolean; confirmLabel: string } {
const confirm = w.onConfirm !== null
const vars = { name: w.name, range: w.trekRange, host: w.hostVersion }
const body = w.acceptedByAdmin
? w.trekRange
? t('admin.plugins.rangeBypass.bodyAdmin', vars)
: t('admin.plugins.rangeBypass.bodyAdminUnknown', vars)
: w.trekRange
? t('admin.plugins.rangeBypass.body', vars)
: t('admin.plugins.rangeBypass.bodyUnknown', vars)
const title = w.acceptedByAdmin
? t('admin.plugins.rangeBypass.titleAdmin')
: confirm
? t('admin.plugins.rangeBypass.title')
: t('admin.plugins.rangeBypass.noticeTitle')
return {
confirm,
title,
body,
confirmLabel: w.action === 'activate' ? t('admin.plugins.activateAnyway') : t('admin.plugins.installAnyway'),
}
}
/**
* The TREK_PLUGINS_IGNORE_TREK_RANGE warning state behind BOTH admin shells (the desktop
* panel and the phone panel render their own dialog/sheet over it). `guard` parks a
* registry install behind the confirm step when the entry was already flagged
* incompatible; `notice` shows the after-the-fact warning for a response that carries the
* server's marker. Whether the switch is ON stays a plain `useState` in each shell: its
* setter is read inside the mount-time refresh, and only a useState setter is something
* the exhaustive-deps rule knows to be stable.
*/
export function useRangeBypass() {
const { t } = useTranslation()
const [warning, setWarning] = useState<RangeWarning | null>(null)
const notice = (name: string, b: TrekRangeBypass | null | undefined) => {
if (b) setWarning({ name, trekRange: b.trekRange, hostVersion: b.hostVersion, onConfirm: null })
}
const guard = (warn: RangeWarning | undefined, run: () => void) => {
if (warn) setWarning({ ...warn, onConfirm: run })
else run()
}
const confirm = () => { const w = warning; setWarning(null); w?.onConfirm?.() }
const dismiss = () => setWarning(null)
return {
warning,
copy: warning ? rangeWarningCopy(warning, t) : null,
notice, guard, confirm, dismiss,
}
}