Track client operations and show inline progress
All checks were successful
Build and Deploy Gateway / build-and-push (push) Successful in 16s
Build and Deploy Gateway / deploy (push) Successful in 7s

This commit is contained in:
2026-07-11 20:55:20 +03:00
parent 9da4fef1f0
commit 74660d915f
6 changed files with 299 additions and 66 deletions

View File

@@ -9,6 +9,7 @@ import {
harborReducer,
initialHarborState,
} from './state/harborReducer.js';
import { createOperationRegistry } from './state/operations.js';
function App() {
const previewReady = new URLSearchParams(window.location.search).has('preview-ready');
@@ -17,9 +18,13 @@ function App() {
initialHarborState,
);
const [subscriptionUrl, setSubscriptionUrl] = useState('');
const [busy, setBusy] = useState(false);
const [operations, setOperations] = useState({});
const [error, setError] = useState(null);
const pollGeneration = useRef(0);
const operationRegistry = useRef(null);
if (!operationRegistry.current) {
operationRegistry.current = createOperationRegistry(setOperations);
}
function setPendingTag(serverId) {
dispatch({ type: 'select-server', serverId });
@@ -60,24 +65,25 @@ function App() {
: '/harbor-connect.svg?v=2';
}, [state?.mode]);
async function run(action, context) {
setBusy(true);
function run(key, action, context) {
setError(null);
try {
return await applyMutation(action);
} catch (err) {
const safeError = err instanceof HarborApiError ? err : new HarborApiError();
setError({
context,
message: safeError.message,
code: safeError.code,
correlationId: safeError.correlationId,
retry: safeError.retryable ? () => run(action, context) : null,
});
return false;
} finally {
setBusy(false);
}
return operationRegistry.current.run(key, async () => {
try {
return await applyMutation(action);
} catch (err) {
const safeError = err instanceof HarborApiError
? err
: new HarborApiError({ code: err?.code }, err?.status);
setError({
context,
message: safeError.message,
code: safeError.code,
correlationId: safeError.correlationId,
retry: safeError.retryable ? () => run(key, action, context) : null,
});
return false;
}
});
}
async function applyMutation(action) {
@@ -94,7 +100,7 @@ function App() {
}
async function fetchSubscription() {
return run(async () => {
return run('subscriptionImport', async () => {
const data = await api.subscription.fetch(subscriptionUrl);
dispatch({ type: 'clear-pending-server' });
return data;
@@ -102,11 +108,11 @@ function App() {
}
async function refreshSubscription() {
return run(api.subscription.refresh, 'subscription');
return run('subscriptionRefresh', api.subscription.refresh, 'subscription');
}
async function forgetSubscription() {
return run(async () => {
return run('subscriptionDelete', async () => {
const data = await api.subscription.forget();
setSubscriptionUrl('');
dispatch({ type: 'clear-pending-server' });
@@ -123,7 +129,7 @@ function App() {
<main className="app-main">
<ClientOverviewPage
state={previewReady ? { ...state, mode: 'client', hasSubscription: true, subscriptionHost: 'harbor.example', selectedTag: 'Amsterdam', proxyPort: 8082 } : state}
busy={busy}
operations={operations}
error={error}
subscriptionUrl={subscriptionUrl}
setSubscriptionUrl={setSubscriptionUrl}
@@ -133,10 +139,10 @@ function App() {
onFetchSubscription={fetchSubscription}
onRefreshSubscription={refreshSubscription}
onForgetSubscription={forgetSubscription}
onApply={(tag) => run(() => api.apply(tag), 'connection')}
onRestart={() => run(api.singbox.restart, 'connection')}
onStop={() => run(api.singbox.stop, 'connection')}
onSetGatewayAuto={(enabled) => run(() => api.gatewayAuto.setEnabled(enabled), 'connection')}
onApply={(tag) => run('serverApply', () => api.apply(tag), 'connection')}
onRestart={() => run('connection', api.singbox.restart, 'connection')}
onStop={() => run('connection', api.singbox.stop, 'connection')}
onSetGatewayAuto={(enabled) => run('gatewayAuto', () => api.gatewayAuto.setEnabled(enabled), 'connection')}
/>
</main>
</div>