mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-11-02 10:52:22 +00:00
ui: compact the current ProxMenux version status
This commit is contained in:
@@ -47,6 +47,7 @@ NODE_FILES = (
|
|||||||
"tests/test_reboot_health_refresh.cjs",
|
"tests/test_reboot_health_refresh.cjs",
|
||||||
"tests/test_host_update_controls.cjs",
|
"tests/test_host_update_controls.cjs",
|
||||||
"tests/test_reboot_recovery.cjs",
|
"tests/test_reboot_recovery.cjs",
|
||||||
|
"tests/test_proxmenux_version_row.cjs",
|
||||||
"tests/test_lxc_update_jobs.cjs",
|
"tests/test_lxc_update_jobs.cjs",
|
||||||
"tests/test_api_import_boundaries.cjs",
|
"tests/test_api_import_boundaries.cjs",
|
||||||
"tests/test_api_errors.cjs",
|
"tests/test_api_errors.cjs",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import * as Confirm from "@radix-ui/react-alert-dialog"
|
|||||||
import { Button } from "./ui/button"
|
import { Button } from "./ui/button"
|
||||||
import { getAuthToken } from "@/lib/api-config"
|
import { getAuthToken } from "@/lib/api-config"
|
||||||
import { useT } from "@/lib/i18n/provider"
|
import { useT } from "@/lib/i18n/provider"
|
||||||
|
import { CheckCircle2, AlertTriangle, Info, ChevronRight } from "lucide-react"
|
||||||
|
|
||||||
function headers() {
|
function headers() {
|
||||||
const token = getAuthToken()
|
const token = getAuthToken()
|
||||||
@@ -145,14 +146,32 @@ export function ProxMenuxVersionCard({ getApiUrl }: { getApiUrl: Url }) {
|
|||||||
return () => { disposed = true; active?.abort(); clearInterval(interval); window.removeEventListener("focus", read); document.removeEventListener("visibilitychange", read) }
|
return () => { disposed = true; active?.abort(); clearInterval(interval); window.removeEventListener("focus", read); document.removeEventListener("visibilitychange", read) }
|
||||||
}, [getApiUrl])
|
}, [getApiUrl])
|
||||||
const state = value?.state ?? "unknown"
|
const state = value?.state ?? "unknown"
|
||||||
return <section aria-label={t("healthStatus.versions.title")} className="mt-3 mx-2 sm:mx-3 rounded-lg border bg-muted/10 p-3 text-[10px] sm:text-xs">
|
const versions = <dl className="grid grid-cols-2 gap-4">
|
||||||
<div className="flex flex-wrap items-center justify-between gap-2 mb-3"><h3 className="font-semibold">{t("healthStatus.versions.title")}</h3>
|
|
||||||
<span className={state === "unknown" ? "text-amber-500" : state === "available" ? "text-blue-500" : "text-green-500"}>{t(`healthStatus.versions.${state}`)}</span></div>
|
|
||||||
<dl className="grid grid-cols-2 gap-4">
|
|
||||||
<div><dt className="text-muted-foreground mb-1">{t("healthStatus.versions.installed")}</dt><dd className="font-mono font-semibold">{value?.installed ?? t("healthStatus.versions.unverified")}</dd></div>
|
<div><dt className="text-muted-foreground mb-1">{t("healthStatus.versions.installed")}</dt><dd className="font-mono font-semibold">{value?.installed ?? t("healthStatus.versions.unverified")}</dd></div>
|
||||||
<div><dt className="text-muted-foreground mb-1">{t(state === "unknown" && value?.available ? "healthStatus.versions.lastKnown" : "healthStatus.versions.latest")}</dt><dd className="font-mono font-semibold">{value?.available ?? t("healthStatus.versions.unverified")}</dd></div>
|
<div><dt className="text-muted-foreground mb-1">{t(state === "unknown" && value?.available ? "healthStatus.versions.lastKnown" : "healthStatus.versions.latest")}</dt><dd className="font-mono font-semibold">{value?.available ?? t("healthStatus.versions.unverified")}</dd></div>
|
||||||
</dl>
|
</dl>
|
||||||
|
const metadata = <>
|
||||||
{value && <p className="mt-3 text-muted-foreground">{t(`healthStatus.versions.${value.channel}`)}</p>}
|
{value && <p className="mt-3 text-muted-foreground">{t(`healthStatus.versions.${value.channel}`)}</p>}
|
||||||
{value?.checked_at && <p className="mt-1 text-muted-foreground">{t("healthStatus.versions.checked", { time: new Date(value.checked_at).toLocaleString() })}</p>}
|
{value?.checked_at && <p className="mt-1 text-muted-foreground">{t("healthStatus.versions.checked", { time: new Date(value.checked_at).toLocaleString() })}</p>}
|
||||||
|
</>
|
||||||
|
if (state !== "available") return <details aria-label={t("healthStatus.versions.title")} className="group text-[10px] sm:text-xs">
|
||||||
|
<summary className="flex items-start gap-2 py-1.5 px-2 sm:px-3 rounded-md hover:bg-muted/40 cursor-pointer list-none [&::-webkit-details-marker]:hidden">
|
||||||
|
{state === "current" ? <CheckCircle2 aria-hidden="true" className="h-3 w-3 sm:h-3.5 sm:w-3.5 shrink-0 text-green-500" /> : <AlertTriangle aria-hidden="true" className="h-3 w-3 sm:h-3.5 sm:w-3.5 shrink-0 text-amber-500" />}
|
||||||
|
<span className="font-medium shrink-0">ProxMenux</span>
|
||||||
|
<span className={`flex-1 min-w-0 break-words ${state === "current" ? "text-muted-foreground" : "text-amber-500"}`}>
|
||||||
|
{state === "current" ? t("healthStatus.versions.currentVersion", { version: value!.installed! }) : t("healthStatus.versions.unknown")}
|
||||||
|
</span>
|
||||||
|
<ChevronRight aria-hidden="true" className="h-3 w-3 shrink-0 text-muted-foreground transition-transform group-open:rotate-90" />
|
||||||
|
</summary>
|
||||||
|
<div className="ml-7 mr-2 sm:mr-3 pb-2">
|
||||||
|
{state === "unknown" && versions}
|
||||||
|
{metadata}
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
return <section aria-label={t("healthStatus.versions.title")} className="mt-3 mx-2 sm:mx-3 rounded-lg border bg-muted/10 p-3 text-[10px] sm:text-xs">
|
||||||
|
<div className="flex flex-wrap items-center justify-between gap-2 mb-3"><h3 className="font-semibold">{t("healthStatus.versions.title")}</h3>
|
||||||
|
<span className="flex items-center gap-1.5 text-blue-500"><Info aria-hidden="true" className="h-3.5 w-3.5" />{t("healthStatus.versions.available")}</span></div>
|
||||||
|
{versions}
|
||||||
|
{metadata}
|
||||||
</section>
|
</section>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5326,6 +5326,7 @@
|
|||||||
"lastKnown": "Last known available version",
|
"lastKnown": "Last known available version",
|
||||||
"available": "Update available",
|
"available": "Update available",
|
||||||
"current": "ProxMenux is up to date",
|
"current": "ProxMenux is up to date",
|
||||||
|
"currentVersion": "Version {version} is up to date",
|
||||||
"unknown": "Could not verify the available version",
|
"unknown": "Could not verify the available version",
|
||||||
"unverified": "Unverified",
|
"unverified": "Unverified",
|
||||||
"stable": "Stable channel",
|
"stable": "Stable channel",
|
||||||
|
|||||||
@@ -5198,6 +5198,7 @@
|
|||||||
"lastKnown": "Posledná známa dostupná verzia",
|
"lastKnown": "Posledná známa dostupná verzia",
|
||||||
"available": "Dostupná aktualizácia",
|
"available": "Dostupná aktualizácia",
|
||||||
"current": "ProxMenux je aktuálny",
|
"current": "ProxMenux je aktuálny",
|
||||||
|
"currentVersion": "Verzia {version} je aktuálna",
|
||||||
"unknown": "Dostupnú verziu sa nepodarilo overiť",
|
"unknown": "Dostupnú verziu sa nepodarilo overiť",
|
||||||
"unverified": "Neoverené",
|
"unverified": "Neoverené",
|
||||||
"stable": "Stabilný kanál",
|
"stable": "Stabilný kanál",
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
// Render the shipped component with an inert snapshot; no network or power.
|
||||||
|
const assert=require('node:assert/strict'),fs=require('node:fs'),path=require('node:path'),vm=require('node:vm')
|
||||||
|
const app=path.resolve(__dirname,'../AppImage'),ts=require(path.join(app,'node_modules/typescript'))
|
||||||
|
const react=require(path.join(app,'node_modules/react'))
|
||||||
|
const {renderToStaticMarkup}=require(path.join(app,'node_modules/react-dom/server'))
|
||||||
|
const code=ts.transpileModule(fs.readFileSync(path.join(app,'components/host-update-status.tsx'),'utf8'),{
|
||||||
|
compilerOptions:{target:ts.ScriptTarget.ES2020,module:ts.ModuleKind.CommonJS,jsx:ts.JsxEmit.ReactJSX,esModuleInterop:true}}).outputText
|
||||||
|
for(const locale of ['en','sk']){
|
||||||
|
const catalog=JSON.parse(fs.readFileSync(path.join(app,'messages',locale,'common.json'),'utf8'))
|
||||||
|
const t=(key,params={})=>{const text=key.split('.').reduce((x,k)=>x?.[k],catalog);assert.equal(typeof text,'string',key);return text.replace(/\{(\w+)\}/g,(_,k)=>params[k]??'{'+k+'}')}
|
||||||
|
const versions=catalog.healthStatus.versions
|
||||||
|
let value, effects=0
|
||||||
|
const exports={}
|
||||||
|
vm.runInNewContext(code,{exports,require:name=>{
|
||||||
|
if(name==='react')return {...react,useState:()=>[value,()=>{}],useEffect:()=>{},useRef:()=>({current:null})}
|
||||||
|
if(name==='react/jsx-runtime')return require(path.join(app,'node_modules/react/jsx-runtime'))
|
||||||
|
if(name==='@radix-ui/react-alert-dialog'||name==='./ui/button')return {}
|
||||||
|
if(name==='@/lib/i18n/provider')return {useT:()=>t}
|
||||||
|
if(name==='@/lib/api-config')return {getAuthToken:()=>null}
|
||||||
|
if(name==='lucide-react')return require(path.join(app,'node_modules/lucide-react'))
|
||||||
|
throw Error('Unexpected boundary '+name)
|
||||||
|
},fetch:()=>{effects++;throw Error('Forbidden request')},console})
|
||||||
|
const snapshot={installed:'1.2.7',available:'1.2.7',state:'current',channel:'stable',checked_at:'2026-10-28T08:00:00Z'}
|
||||||
|
const render=data=>{value=data;return renderToStaticMarkup(react.createElement(exports.ProxMenuxVersionCard,{getApiUrl:p=>p}))}
|
||||||
|
const current=render(snapshot)
|
||||||
|
assert.ok(current.includes('<details')&¤t.includes('<summary'),'current version is a compact native disclosure')
|
||||||
|
assert.ok(!current.includes('<section')&&!current.includes('<dl>'),'up-to-date version has no expanded update card')
|
||||||
|
assert.ok(current.includes('1.2.7')&¤t.includes('text-green-500'))
|
||||||
|
assert.ok(current.includes(versions.stable)&¤t.includes(versions.checked.split('{')[0]))
|
||||||
|
const available=render({...snapshot,state:'available',available:'1.2.8'})
|
||||||
|
assert.ok(available.includes('<section')&&available.includes('<dl'))
|
||||||
|
assert.ok(available.includes('1.2.7')&&available.includes('1.2.8')&&available.includes(versions.available))
|
||||||
|
assert.ok(available.includes('text-blue-500'))
|
||||||
|
for(const data of [null,{...snapshot,state:'unknown'},{...snapshot,state:'unknown',available:null,checked_at:null,channel:'beta'}]){
|
||||||
|
const unknown=render(data)
|
||||||
|
assert.ok(unknown.includes('<details')&&unknown.includes('text-amber-500'))
|
||||||
|
assert.ok(unknown.includes(versions.unknown)&&!unknown.includes('text-green-500'))
|
||||||
|
if(data?.available)assert.ok(unknown.includes(versions.lastKnown),'old version remains explicitly last known')
|
||||||
|
else assert.ok(unknown.includes(versions.unverified),'missing data is never a current version')
|
||||||
|
}
|
||||||
|
assert.equal(effects,0,'rendering/disclosure never triggers update, installation or power')
|
||||||
|
}
|
||||||
|
console.log('PASS actual ProxMenux version rendering: compact current, expanded update, unknown/last-known; EN/SK and zero effects')
|
||||||
Reference in New Issue
Block a user