Add admin restart flow and loading indicators

This commit is contained in:
2026-07-08 13:34:43 +03:00
parent ae13070eba
commit 838dea0e03
11 changed files with 639 additions and 127 deletions

View File

@@ -1,11 +1,12 @@
import { useEffect, useMemo, useRef, useState, type CSSProperties } from 'react';
import { open } from '@tauri-apps/plugin-dialog';
import { Cpu, FileCode2, FolderOpen, Gauge, Link2, Power, Trash2, Wand2 } from 'lucide-react';
import { Cpu, FileCode2, FolderOpen, Gauge, Link2, Power, ShieldAlert, Trash2, Wand2 } from 'lucide-react';
import {
applyProfiles,
fetchSingBoxSubscription,
forgetSingBoxSubscription,
generateSingBoxConfig,
getAdminStatus,
getComponents,
getProxiFyreSetupStatus,
getSavedState,
@@ -16,6 +17,7 @@ import {
pingAllSingBoxServers,
pingProxyTarget,
pingSingBoxServer,
restartAsAdmin,
saveProfile,
saveSingBoxSubscription,
saveTarget,
@@ -26,6 +28,7 @@ import {
stopSingBoxService,
uninstallProxiFyre,
uninstallSingBox,
type AdminStatusResponse,
type ApplyProfilesResponse,
type LocalSingBoxStatusResponse,
type PingServerResponse,
@@ -35,7 +38,7 @@ import {
type SingBoxSetupStatus,
} from '../api/tauriCommands';
import type { ComponentStatus, Profile, ProfileItemInput, ProfileItemType, SubscriptionServer, Target } from '../domain/types';
import { Button, DetailsPopover, IconButton, LogDock, ServiceControlRow, Tabs } from '../ui';
import { BusyRing, Button, DetailsPopover, IconButton, LogDock, ServiceControlRow, Tabs } from '../ui';
import { getApplyReadiness } from './readiness';
import { serviceControlState } from './viewModel';
@@ -180,6 +183,7 @@ export function App() {
const [subscriptionInput, setSubscriptionInput] = useState('');
const [serverPings, setServerPings] = useState<Record<string, PingServerResponse>>({});
const [proxyCheck, setProxyCheck] = useState<ProxyTargetCheckResponse | null>(null);
const [adminStatus, setAdminStatus] = useState<AdminStatusResponse | null>(null);
const [generatedConfigPath, setGeneratedConfigPath] = useState('');
const [logEntries, setLogEntries] = useState<LogEntry[]>([]);
const [activeLogId, setActiveLogId] = useState<string | null>(null);
@@ -187,6 +191,7 @@ export function App() {
const [isLoading, setIsLoading] = useState(true);
const [isDetectingComponents, setIsDetectingComponents] = useState(true);
const [isApplying, setIsApplying] = useState(false);
const [isRestartingAsAdmin, setIsRestartingAsAdmin] = useState(false);
const [isProxyChecking, setIsProxyChecking] = useState(false);
const [serverPingTag, setServerPingTag] = useState<string | null>(null);
const [serviceAction, setServiceAction] = useState<ProxiFyreAction | null>(null);
@@ -259,6 +264,7 @@ export function App() {
}, [activeLogId]);
async function refresh() {
void refreshAdminStatus();
setIsLoading(true);
try {
const saved = await getSavedState();
@@ -276,6 +282,30 @@ export function App() {
void refreshComponents();
}
async function refreshAdminStatus() {
try {
const status = await getAdminStatus();
setAdminStatus(status);
} catch {
setAdminStatus(null);
}
}
async function restartApplicationAsAdmin() {
setIsRestartingAsAdmin(true);
try {
await restartAsAdmin();
setIsRestartingAsAdmin(false);
} catch (error) {
setIsRestartingAsAdmin(false);
showNotice({
kind: 'error',
title: 'Перезапуск отменен',
text: errorMessage(error),
});
}
}
async function refreshComponents() {
setIsDetectingComponents(true);
try {
@@ -933,6 +963,33 @@ export function App() {
setActiveLogId(entry.id);
}
function renderAdminPrompt() {
if (!adminStatus?.canRestartElevated) return null;
return (
<aside className="admin-prompt" aria-label="Права администратора">
<span className="admin-prompt-icon" aria-hidden="true">
<ShieldAlert size={18} strokeWidth={1.9} />
</span>
<div className="admin-prompt-copy">
<strong>Один запрос прав вместо серии UAC</strong>
<span>{adminStatus.message}</span>
</div>
<Button
type="button"
variant="primary"
size="sm"
className="admin-prompt-action"
onClick={() => void restartApplicationAsAdmin()}
loading={isRestartingAsAdmin}
loadingLabel="Открываю UAC"
>
Перезапустить с правами
</Button>
</aside>
);
}
function renderTabs() {
const tabs: Array<{ id: PanelId; label: string }> = [
{ id: 'summary', label: 'Сводка' },
@@ -1176,6 +1233,7 @@ export function App() {
return (
<div className={`summary-status-control ${systemSummary.tone} ${running ? 'on' : 'off'}`}>
{systemSummary.tone === 'checking' ? <BusyRing /> : null}
<span className={`summary-status-dot ${systemSummary.tone}`} aria-hidden="true" />
<div className="summary-status-copy">
<span>Состояние маршрута</span>
@@ -1504,6 +1562,7 @@ export function App() {
return (
<section className={`proxy-overview ${check.tone}`} aria-label="Состояние прокси">
{check.tone === 'checking' ? <BusyRing /> : null}
{renderConnectionCheck(check)}
</section>
);
@@ -1607,6 +1666,7 @@ export function App() {
</header>
{renderTabs()}
{renderAdminPrompt()}
<div className={`tab-panel-frame swipe-${tabTransitionDirection}`} key={activePanel}>
{renderActivePanel()}
</div>