mirror of
https://github.com/mudler/LocalAI.git
synced 2026-11-06 04:23:15 -05:00
feat(ui): show locked host grants in the install dialog and the Host access page
When the owner keeps UI grants locked because sign-in is off, the overview and the preview say so. The Host access page and the install and upgrade dialogs show the operations read-only with one sentence that says why, and the dialogs approve no operation. The sentence is added to every locale. Assisted-by: Claude:claude-sonnet-5-5 node
This commit is contained in:
15 files changed
+32
-5
No files matched your search
@@ -858,6 +858,7 @@
|
||||
"loading": "Lese, was dieses Add-on anfordert...",
|
||||
"loadingPage": "Host-Zugriff wird geladen",
|
||||
"locked": "Das Deployment hat Änderungen am Host-Zugriff abgeschaltet. Du kannst diese Seite lesen, aber nicht ändern.",
|
||||
"lockedNoAuth": "Die Anmeldung ist ausgeschaltet, daher könnte jeder, der diesen Server erreicht, dies ändern; der Administrator legt es in der Richtliniendatei fest.",
|
||||
"newOptional": "Die neue Version fordert auch {{ops}} an. Diese sind optional und nicht erlaubt.",
|
||||
"noLongerRequested": "Diese Version fordert es nicht mehr an. Du kannst es ausschalten.",
|
||||
"notOfficial": "Dieses Add-on stammt aus {{library}}, nicht aus der offiziellen Bibliothek.",
|
||||
|
||||
@@ -858,6 +858,7 @@
|
||||
"loading": "Reading what this add-on asks for...",
|
||||
"loadingPage": "Loading host access",
|
||||
"locked": "The deployment turned off changes to host access. You can read this page but not change it.",
|
||||
"lockedNoAuth": "Sign-in is off, so anyone who can reach this server could change these; they are set by the administrator in the policy file.",
|
||||
"newOptional": "The new version also asks for {{ops}}. These are optional and not allowed.",
|
||||
"noLongerRequested": "This version no longer asks for it. You can turn it off.",
|
||||
"notOfficial": "This add-on comes from {{library}}, not the official library.",
|
||||
|
||||
@@ -858,6 +858,7 @@
|
||||
"loading": "Leyendo lo que pide este complemento...",
|
||||
"loadingPage": "Cargando el acceso al host",
|
||||
"locked": "El despliegue desactivó los cambios en el acceso al host. Puedes leer esta página, pero no cambiarla.",
|
||||
"lockedNoAuth": "El inicio de sesión está desactivado, así que cualquiera que llegue a este servidor podría cambiar esto; el administrador lo define en el archivo de políticas.",
|
||||
"newOptional": "La nueva versión también pide {{ops}}. Son opcionales y no están permitidos.",
|
||||
"noLongerRequested": "Esta versión ya no lo pide. Puedes desactivarlo.",
|
||||
"notOfficial": "Este complemento viene de {{library}}, no de la biblioteca oficial.",
|
||||
|
||||
@@ -862,6 +862,7 @@
|
||||
"loading": "Lecture de ce que demande ce module...",
|
||||
"loadingPage": "Chargement de l'accès à l'hôte",
|
||||
"locked": "Le déploiement a désactivé les changements d'accès à l'hôte. Vous pouvez lire cette page, pas la modifier.",
|
||||
"lockedNoAuth": "La connexion est désactivée, donc toute personne qui atteint ce serveur pourrait modifier ceci ; l'administrateur le définit dans le fichier de politique.",
|
||||
"newOptional": "La nouvelle version demande aussi {{ops}}. Elles sont facultatives et non autorisées.",
|
||||
"noLongerRequested": "Cette version ne le demande plus. Vous pouvez le désactiver.",
|
||||
"notOfficial": "Ce module vient de {{library}}, pas de la bibliothèque officielle.",
|
||||
|
||||
@@ -858,6 +858,7 @@
|
||||
"loading": "Membaca apa yang diminta add-on ini...",
|
||||
"loadingPage": "Memuat akses host",
|
||||
"locked": "Deployment mematikan perubahan akses host. Anda dapat membaca halaman ini tetapi tidak mengubahnya.",
|
||||
"lockedNoAuth": "Login dimatikan, sehingga siapa pun yang dapat menjangkau server ini dapat mengubahnya; administrator mengaturnya di berkas kebijakan.",
|
||||
"newOptional": "Versi baru juga meminta {{ops}}. Ini opsional dan tidak diizinkan.",
|
||||
"noLongerRequested": "Versi ini tidak lagi memintanya. Anda dapat mematikannya.",
|
||||
"notOfficial": "Add-on ini berasal dari {{library}}, bukan pustaka resmi.",
|
||||
|
||||
@@ -858,6 +858,7 @@
|
||||
"loading": "Lettura di ciò che questo add-on richiede...",
|
||||
"loadingPage": "Caricamento dell'accesso all'host",
|
||||
"locked": "Il deployment ha disattivato le modifiche all'accesso all'host. Puoi leggere questa pagina ma non modificarla.",
|
||||
"lockedNoAuth": "L'accesso è disattivato, quindi chiunque raggiunga questo server potrebbe modificarlo; l'amministratore lo imposta nel file di policy.",
|
||||
"newOptional": "La nuova versione chiede anche {{ops}}. Sono facoltative e non consentite.",
|
||||
"noLongerRequested": "Questa versione non lo chiede più. Puoi spegnerlo.",
|
||||
"notOfficial": "Questo add-on viene da {{library}}, non dalla libreria ufficiale.",
|
||||
|
||||
@@ -858,6 +858,7 @@
|
||||
"loading": "이 애드온이 요청하는 내용을 읽는 중...",
|
||||
"loadingPage": "호스트 접근을 불러오는 중",
|
||||
"locked": "배포 설정이 호스트 접근 변경을 꺼 두었습니다. 이 페이지는 읽을 수만 있습니다.",
|
||||
"lockedNoAuth": "로그인이 꺼져 있어 이 서버에 접근할 수 있는 누구나 이를 바꿀 수 있습니다. 관리자가 정책 파일에서 설정합니다.",
|
||||
"newOptional": "새 버전은 {{ops}}도 요청합니다. 선택 항목이며 허용되지 않았습니다.",
|
||||
"noLongerRequested": "이 버전은 더 이상 요청하지 않습니다. 끌 수 있습니다.",
|
||||
"notOfficial": "이 애드온은 공식 라이브러리가 아니라 {{library}}에서 왔습니다.",
|
||||
|
||||
@@ -858,6 +858,7 @@
|
||||
"loading": "Lendo o que este add-on pede...",
|
||||
"loadingPage": "Carregando o acesso ao host",
|
||||
"locked": "A implantação desativou mudanças no acesso ao host. Você pode ler esta página, mas não alterá-la.",
|
||||
"lockedNoAuth": "O login está desativado, então qualquer pessoa que alcance este servidor poderia alterar isto; o administrador define no arquivo de política.",
|
||||
"newOptional": "A nova versão também pede {{ops}}. São opcionais e não permitidas.",
|
||||
"noLongerRequested": "Esta versão não pede mais. Você pode desligar.",
|
||||
"notOfficial": "Este add-on vem de {{library}}, não da biblioteca oficial.",
|
||||
|
||||
@@ -858,6 +858,7 @@
|
||||
"loading": "正在读取此插件的请求...",
|
||||
"loadingPage": "正在加载主机访问",
|
||||
"locked": "部署已关闭对主机访问的修改。你可以查看此页面,但不能修改。",
|
||||
"lockedNoAuth": "登录已关闭,任何能访问此服务器的人都可能修改这些设置;它们由管理员在策略文件中设定。",
|
||||
"newOptional": "新版本还请求 {{ops}}。这些是可选的,且未被允许。",
|
||||
"noLongerRequested": "此版本不再请求它。你可以将其关闭。",
|
||||
"notOfficial": "此插件来自 {{library}},不是官方库。",
|
||||
|
||||
@@ -64,7 +64,8 @@ export default function ApproveUpgradeDialog({ addon, sourceId, api, actions, on
|
||||
{describeError(host.error, t)}
|
||||
</Callout>
|
||||
)}
|
||||
{ready && <HostAccessList ops={host.preview.ops} ticked={host.ticked} onTick={host.setTick} added={added} testId="approve" />}
|
||||
{ready && <HostAccessList ops={host.preview.ops} ticked={host.ticked} onTick={host.setTick} added={added} testId="approve" locked={host.preview.locked} />}
|
||||
{ready && host.preview.locked && <Callout tone="info" testId="approve-locked">{t(host.preview.lockedReason === 'no_auth' ? 'hostAccess.lockedNoAuth' : 'hostAccess.locked')}</Callout>}
|
||||
{block && <Callout tone="error" role="alert" testId="approve-blocked">{t(block.kind === 'blocked' ? 'hostAccess.cannotInstallBlocked' : 'hostAccess.cannotInstallUnsupported', { ops: block.ops.join(', ') })}</Callout>}
|
||||
{failure && <Callout tone="error" role="alert" testId="approve-failed">{failure}</Callout>}
|
||||
</HostDialog>
|
||||
|
||||
@@ -16,7 +16,7 @@ export function opNote(op, t) {
|
||||
// are on and cannot be turned off; optional ones start off. A blocked one is
|
||||
// greyed with the reason. `added` highlights what a new version asks for on
|
||||
// top of what the old one held.
|
||||
export default function HostAccessList({ ops, ticked, onTick, added = [], testId = 'hostaccess' }) {
|
||||
export default function HostAccessList({ ops, ticked, onTick, added = [], testId = 'hostaccess', locked = false }) {
|
||||
const { t } = useTranslation('addons')
|
||||
return (
|
||||
<div className="ad-hostlist" data-testid={`${testId}-list`}>
|
||||
@@ -35,7 +35,7 @@ export default function HostAccessList({ ops, ticked, onTick, added = [], testId
|
||||
reason={note.reason}
|
||||
meta={[op.risk ? t(`hostAccess.risk.${op.risk}`) : '', isNew ? t('hostAccess.op.new') : ''].filter(Boolean).join(' · ') || undefined}
|
||||
checked={on}
|
||||
disabled={!tickable(op) || op.required}
|
||||
disabled={locked || !tickable(op) || op.required}
|
||||
onChange={(next) => onTick(op.name, next)}
|
||||
stacked
|
||||
testId={`${testId}-op`}
|
||||
|
||||
@@ -69,7 +69,7 @@ export default function HostAccessView({ grants, api = hostGrantsApi }) {
|
||||
<div className="ad-host" data-testid="host-access">
|
||||
<p className="ad-host__intro">{t('hostAccess.intro')}</p>
|
||||
{grants.stale && <Callout tone="info" role="status" className="ad-callout">{t('apps.stale')}</Callout>}
|
||||
{locked && <Callout tone="info" testId="host-locked" className="ad-callout">{t('hostAccess.locked')}</Callout>}
|
||||
{locked && <Callout tone="info" testId="host-locked" className="ad-callout">{t(data.lockedReason === 'no_auth' ? 'hostAccess.lockedNoAuth' : 'hostAccess.locked')}</Callout>}
|
||||
{notice && <Callout tone={notice.tone} role={notice.tone === 'error' ? 'alert' : 'status'} testId="host-notice" className="ad-callout">{notice.text}</Callout>}
|
||||
{pending.map(a => <WaitingBanner key={a.id} addon={a} onReview={() => setApproving(a)} />)}
|
||||
|
||||
|
||||
@@ -122,7 +122,8 @@ export default function InstallDialog({ entry, flow, actions, onClose, installed
|
||||
{host.preview.official === false && (
|
||||
<p className="ad-quiet" data-testid="hostaccess-not-official">{t('hostAccess.notOfficial', { library: entry.sourceId })}</p>
|
||||
)}
|
||||
<HostAccessList ops={host.preview.ops} ticked={host.ticked} onTick={host.setTick} />
|
||||
<HostAccessList ops={host.preview.ops} ticked={host.ticked} onTick={host.setTick} locked={host.preview.locked} />
|
||||
{host.preview.locked && <Callout tone="info" testId="hostaccess-locked">{t(host.preview.lockedReason === 'no_auth' ? 'hostAccess.lockedNoAuth' : 'hostAccess.locked')}</Callout>}
|
||||
{block && (
|
||||
<Callout tone="error" role="alert" testId="hostaccess-blocked">
|
||||
{t(block.kind === 'blocked' ? 'hostAccess.cannotInstallBlocked' : 'hostAccess.cannotInstallUnsupported', { ops: block.ops.join(', ') })}
|
||||
|
||||
@@ -79,6 +79,9 @@ export function normalizePreview(payload) {
|
||||
revision: Number(p.revision) || 0,
|
||||
nonce: typeof p.nonce === 'string' ? p.nonce : '',
|
||||
official: p.official === true ? true : (p.official === false ? false : null),
|
||||
// The owner keeps UI grants locked: the dialog shows what the add-on asks for and grants nothing.
|
||||
locked: p.locked === true,
|
||||
lockedReason: p.lockedReason === 'no_auth' ? 'no_auth' : 'deployment',
|
||||
// Operations that are new in this version. Empty on an install.
|
||||
added: list(p.added),
|
||||
requested,
|
||||
@@ -103,6 +106,7 @@ export function tickable(op) {
|
||||
// before an uninstall (the owner sends it as a hint).
|
||||
export function initialTicks(preview) {
|
||||
const out = []
|
||||
if (preview.locked) return out
|
||||
for (const op of preview.ops) {
|
||||
if (!tickable(op)) continue
|
||||
if (op.required || op.hint) out.push(op.name)
|
||||
@@ -114,6 +118,7 @@ export function initialTicks(preview) {
|
||||
// can still be granted, so a required operation can never go missing from the
|
||||
// request because of a stale tick list.
|
||||
export function approvedOps(preview, ticked) {
|
||||
if (preview.locked) return []
|
||||
const set = new Set(ticked)
|
||||
for (const op of preview.ops) if (tickable(op) && op.required) set.add(op.name)
|
||||
return preview.ops.filter(op => tickable(op) && set.has(op.name)).map(op => op.name)
|
||||
@@ -201,6 +206,7 @@ export function normalizeOverview(payload) {
|
||||
return {
|
||||
revision: Number(p.revision) || 0,
|
||||
locked: p.locked === true,
|
||||
lockedReason: p.lockedReason === 'no_auth' ? 'no_auth' : 'deployment',
|
||||
blanket: {
|
||||
mode,
|
||||
includeOptional: b.includeOptional === true,
|
||||
|
||||
@@ -199,3 +199,13 @@ test('hostgrants: install with an approval goes through the host-grants route, a
|
||||
assert.ok(calls.includes('plain:install'))
|
||||
assert.ok(!calls.some(c => c.startsWith('grants:')))
|
||||
})
|
||||
|
||||
test('hostgrants: a locked preview ticks nothing and approves no operation', () => {
|
||||
const p = normalizePreview({ ...PREVIEW, locked: true, lockedReason: 'no_auth' })
|
||||
assert.equal(p.locked, true)
|
||||
assert.equal(p.lockedReason, 'no_auth')
|
||||
assert.deepEqual(initialTicks(p), [])
|
||||
assert.deepEqual(approvedOps(p, ['egress.check']), [])
|
||||
assert.equal(normalizeOverview({ locked: true, lockedReason: 'no_auth' }).lockedReason, 'no_auth')
|
||||
assert.equal(normalizePreview(PREVIEW).locked, false)
|
||||
})
|
||||
Reference in new issue
Block a user