Writes use only <ID>_ADMIN_TOKEN and need the apps.write capability. Store URLs are resolved from each server's own listing. The config is saved to history before an uninstall. Zip uploads are fanned out to the servers. Every target's result is audited. The console gets a row menu on the matrix, a target picker, a results dialog with Retry failed, the store and install from file in All servers mode, and single-server actions for writers. Co-Authored-By: Claude Opus 5.5 <[email protected]>
84 lines
2.1 KiB
TypeScript
84 lines
2.1 KiB
TypeScript
import { useEffect, useRef, useState } from 'react'
|
|
import { updateApp } from '../../api/apps'
|
|
import { Button } from '../../ui/Button'
|
|
import { Dialog } from '../../ui/Dialog'
|
|
import { LabeledInput } from '../../ui/Field'
|
|
import { error, type AlertState } from './Apps'
|
|
import { Notifier } from '../../ui/Notifier'
|
|
|
|
/*
|
|
A replica of `showUpdateAppModal` / `updateApp` (apps.js:211-218 and 381-423).
|
|
|
|
The name comes filled in and DISABLED (index.html:6232): updating is replacing
|
|
the zip of an app that already exists, not renaming it. Only the file is
|
|
validated here, just as in upstream.
|
|
*/
|
|
export function UpdateApp({
|
|
open,
|
|
onOpenChange,
|
|
token,
|
|
name,
|
|
onUpdated,
|
|
node = '',
|
|
}: {
|
|
open: boolean
|
|
onOpenChange: (o: boolean) => void
|
|
token: string | null
|
|
name: string
|
|
onUpdated: (name: string) => void
|
|
/** Behind a hub: the one server it goes to. */
|
|
node?: string
|
|
}) {
|
|
const [alert, setAlert] = useState<AlertState | null>(null)
|
|
const [busy, setBusy] = useState(false)
|
|
const fileRef = useRef<HTMLInputElement>(null)
|
|
|
|
useEffect(() => {
|
|
if (open) {
|
|
setAlert(null)
|
|
setBusy(false)
|
|
}
|
|
}, [open])
|
|
|
|
async function update() {
|
|
const file = fileRef.current?.files?.[0]
|
|
if (!file) {
|
|
setAlert({
|
|
type: 'warning',
|
|
title: 'Missing!',
|
|
text: 'Please select an application zip file to update.',
|
|
})
|
|
return
|
|
}
|
|
|
|
setBusy(true)
|
|
const outcome = await updateApp(token, name, file, node)
|
|
setBusy(false)
|
|
|
|
if (outcome.kind !== 'ok') {
|
|
setAlert(error(outcome))
|
|
return
|
|
}
|
|
onUpdated(name)
|
|
}
|
|
|
|
return (
|
|
<Dialog
|
|
open={open}
|
|
onOpenChange={onOpenChange}
|
|
title="Update App"
|
|
actions={
|
|
<>
|
|
<Button variant="primary" disabled={busy} onClick={() => void update()}>
|
|
Update
|
|
</Button>
|
|
</>
|
|
}
|
|
>
|
|
<Notifier notice={alert} onClose={() => setAlert(null)} />
|
|
<LabeledInput label="App Name" value={name} disabled readOnly />
|
|
<LabeledInput label="App Zip File" type="file" ref={fileRef} />
|
|
</Dialog>
|
|
)
|
|
}
|