fix(ui): ограничить каталог бэкапов и вернуть фокус действий
This commit is contained in:
1 parent
5d9a6f3757
commit
611e542ea7
11 files changed
+123
-35
No files matched your search
@@ -74,6 +74,24 @@ function BackupsContent() {
|
||||
const canWrite = isLive && can("mm:backups:write")
|
||||
const canReadServers = can("mm:servers:read")
|
||||
const [resources, setResources] = useState<Record<Resource, ResourceState>>(() => ({ backups: unknownResource(), servers: unknownResource(), schedule: unknownResource(), storage: unknownResource() }))
|
||||
const dialogTrigger = useRef<{ element: HTMLElement; rowId: string | null; label: string | null } | null>(null)
|
||||
const refreshTrigger = useRef<HTMLButtonElement | null>(null)
|
||||
const captureDialogFocus = () => {
|
||||
const element = document.activeElement
|
||||
dialogTrigger.current = element instanceof HTMLElement
|
||||
? { element, rowId: element.closest("[data-row-id]")?.getAttribute("data-row-id") ?? null, label: element.getAttribute("aria-label") }
|
||||
: null
|
||||
}
|
||||
const returnDialogFocus = () => {
|
||||
const trigger = dialogTrigger.current
|
||||
if (trigger?.element.isConnected) return trigger.element
|
||||
// Cell renderers can remount when operation state changes; resolve the same row/action.
|
||||
if (trigger?.rowId && trigger.label) {
|
||||
const replacement = document.querySelector<HTMLElement>(`[data-row-id="${CSS.escape(trigger.rowId)}"] button[aria-label="${CSS.escape(trigger.label)}"]`)
|
||||
if (replacement) return replacement
|
||||
}
|
||||
return refreshTrigger.current
|
||||
}
|
||||
const reader = useRef<AbortController | null>(null)
|
||||
const revision = useRef(0)
|
||||
const alive = useRef(true)
|
||||
@@ -403,7 +421,7 @@ function BackupsContent() {
|
||||
{!canRead && <p role="status">Нет права mm:backups:read</p>}
|
||||
{isLive && !canReadServers && <p role="status">Каталог устройств недоступен без mm:servers:read.</p>}
|
||||
{isLive && canRead && <div className="flex flex-wrap items-center gap-3 text-sm text-muted-foreground">
|
||||
<Button type="button" variant="outline" size="sm" onClick={() => void loadLive()} disabled={loading || scheduleSaveBusy || opBusy}>Обновить</Button>
|
||||
<Button type="button" variant="outline" size="sm" ref={refreshTrigger} onClick={() => void loadLive()} disabled={loading || scheduleSaveBusy || opBusy}>Обновить</Button>
|
||||
<span role="status">{loading ? "Обновление данных…" : resources.backups.receivedAt ? "Получено: " + new Date(resources.backups.receivedAt).toLocaleString("ru-RU") : "Резервные копии пока неизвестны"}</span>
|
||||
</div>}
|
||||
{isLive && (Object.keys(resources) as Resource[]).filter(resource => resources[resource].error).map(resource => <Alert key={resource} variant="warning" aria-label={"Данные резервных копий: " + resource}>
|
||||
@@ -493,8 +511,8 @@ function BackupsContent() {
|
||||
filters={advancedFilters}
|
||||
onFiltersChange={setAdvancedFilters}
|
||||
onDownload={handleDownload}
|
||||
onRestore={backup => { setOpError(null); setRestoreTarget(backup) }}
|
||||
onDelete={backup => { setOpError(null); setDeleteTarget(backup) }}
|
||||
onRestore={backup => { captureDialogFocus(); setOpError(null); setRestoreTarget(backup) }}
|
||||
onDelete={backup => { captureDialogFocus(); setOpError(null); setDeleteTarget(backup) }}
|
||||
onCreate={() => openCreate(false)}
|
||||
canCreate={canCreate}
|
||||
canWrite={canWrite && !opBusy && !scheduleSaveBusy && !backupJobId}
|
||||
@@ -578,6 +596,7 @@ function BackupsContent() {
|
||||
/>
|
||||
|
||||
<BackupDeleteDialog
|
||||
returnFocus={returnDialogFocus}
|
||||
backup={deleteTarget}
|
||||
busy={opBusy}
|
||||
error={opError}
|
||||
@@ -585,6 +604,7 @@ function BackupsContent() {
|
||||
onCancel={() => setDeleteTarget(null)}
|
||||
/>
|
||||
<BackupRestoreDialog
|
||||
returnFocus={returnDialogFocus}
|
||||
backup={restoreTarget}
|
||||
busy={opBusy}
|
||||
blocked={!restoreGuard.canStart}
|
||||
|
||||
Reference in new issue
Block a user