Unify Harbor error handling across server and client
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { flushSync } from 'react-dom';
|
||||
import { api } from '../api.js';
|
||||
import { api, validationStatusForError } from '../api.js';
|
||||
import {
|
||||
connectionAction,
|
||||
connectionDurationParts,
|
||||
@@ -20,6 +20,19 @@ function CloudTooltip({ children }) {
|
||||
return <span className="client-tooltip" role="tooltip">{children}</span>;
|
||||
}
|
||||
|
||||
function InlineError({ error, context }) {
|
||||
if (!error || error.context !== context) return null;
|
||||
return (
|
||||
<div className={`client-inline-error is-${context}`} role="alert">
|
||||
<span>{error.message}</span>
|
||||
{error.correlationId && (
|
||||
<small title={error.correlationId}>Код: {error.correlationId.slice(0, 8)}</small>
|
||||
)}
|
||||
{error.retry && <button type="button" onClick={error.retry}>Повторить</button>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function InstructionStep({ step }) {
|
||||
if (typeof step === 'string') return step;
|
||||
return (
|
||||
@@ -196,6 +209,7 @@ export function ClientOverviewPage({
|
||||
});
|
||||
const [editingSubscription, setEditingSubscription] = useState(!state?.hasSubscription);
|
||||
const [subscriptionValidation, setSubscriptionValidation] = useState({ url: '', status: 'idle' });
|
||||
const [validationAttempt, setValidationAttempt] = useState(0);
|
||||
const [showIntro, setShowIntro] = useState(!hasSubscription);
|
||||
const [subscriptionContentReady, setSubscriptionContentReady] = useState(hasSubscription);
|
||||
const [pings, setPings] = useState({});
|
||||
@@ -240,6 +254,9 @@ export function ClientOverviewPage({
|
||||
const subscriptionValidationStatus = subscriptionValidation.url === normalizedSubscriptionUrl
|
||||
? subscriptionValidation.status
|
||||
: normalizedSubscriptionUrl ? 'checking' : 'idle';
|
||||
const subscriptionError = subscriptionValidation.url === normalizedSubscriptionUrl
|
||||
? subscriptionValidation.error
|
||||
: null;
|
||||
const subscriptionWaiting = hasSubscription && !subscriptionContentReady;
|
||||
|
||||
useEffect(() => {
|
||||
@@ -316,10 +333,32 @@ export function ClientOverviewPage({
|
||||
setSubscriptionValidation({ url: normalizedSubscriptionUrl, status: 'checking' });
|
||||
const timer = setTimeout(() => {
|
||||
api.subscription.validate(normalizedSubscriptionUrl, controller.signal)
|
||||
.then(() => setSubscriptionValidation({ url: normalizedSubscriptionUrl, status: 'valid' }))
|
||||
.catch(() => {
|
||||
.then(() => setSubscriptionValidation({
|
||||
url: normalizedSubscriptionUrl,
|
||||
status: 'valid',
|
||||
error: null,
|
||||
}))
|
||||
.catch((validationError) => {
|
||||
if (!controller.signal.aborted) {
|
||||
setSubscriptionValidation({ url: normalizedSubscriptionUrl, status: 'invalid' });
|
||||
setSubscriptionValidation({
|
||||
url: normalizedSubscriptionUrl,
|
||||
status: validationStatusForError(validationError),
|
||||
error: {
|
||||
context: 'subscription',
|
||||
message: validationError.message,
|
||||
correlationId: validationError.correlationId,
|
||||
retry: validationError.retryable
|
||||
? () => {
|
||||
setSubscriptionValidation({
|
||||
url: normalizedSubscriptionUrl,
|
||||
status: 'checking',
|
||||
error: null,
|
||||
});
|
||||
setValidationAttempt((attempt) => attempt + 1);
|
||||
}
|
||||
: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
}, 350);
|
||||
@@ -328,7 +367,7 @@ export function ClientOverviewPage({
|
||||
clearTimeout(timer);
|
||||
controller.abort();
|
||||
};
|
||||
}, [normalizedSubscriptionUrl]);
|
||||
}, [normalizedSubscriptionUrl, validationAttempt]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasSubscription) {
|
||||
@@ -356,7 +395,7 @@ export function ClientOverviewPage({
|
||||
|
||||
useEffect(() => {
|
||||
if (!state?.hasSubscription) return undefined;
|
||||
onRefreshSubscription().catch(() => {});
|
||||
onRefreshSubscription();
|
||||
return undefined;
|
||||
}, [state?.hasSubscription]);
|
||||
|
||||
@@ -413,7 +452,7 @@ export function ClientOverviewPage({
|
||||
async function submitSubscription(event) {
|
||||
event.preventDefault();
|
||||
if (subscriptionValidationStatus !== 'valid') return;
|
||||
await onFetchSubscription();
|
||||
if (!await onFetchSubscription()) return;
|
||||
setSubscriptionUrl('');
|
||||
setEditingSubscription(false);
|
||||
}
|
||||
@@ -434,7 +473,7 @@ export function ClientOverviewPage({
|
||||
const startedAt = performance.now();
|
||||
setRefreshingInfo(true);
|
||||
try {
|
||||
await onRefreshSubscription();
|
||||
if (!await onRefreshSubscription()) return;
|
||||
setUsageUpdated(false);
|
||||
requestAnimationFrame(() => setUsageUpdated(true));
|
||||
setTimeout(() => setUsageUpdated(false), 900);
|
||||
@@ -451,7 +490,7 @@ export function ClientOverviewPage({
|
||||
}
|
||||
|
||||
async function forgetSubscription() {
|
||||
await onForgetSubscription();
|
||||
if (!await onForgetSubscription()) return;
|
||||
setConfirmingDelete(false);
|
||||
}
|
||||
|
||||
@@ -641,11 +680,10 @@ export function ClientOverviewPage({
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
<InlineError error={error} context="connection" />
|
||||
</section>
|
||||
)}
|
||||
|
||||
{error && <p className="client-error" role="alert">{error}</p>}
|
||||
|
||||
<div
|
||||
className={`client-form${subscriptionWaiting ? ' is-waiting' : ''}${confirmingDelete ? ' is-confirming-delete' : ''}`}
|
||||
aria-hidden={subscriptionWaiting}
|
||||
@@ -759,15 +797,20 @@ export function ClientOverviewPage({
|
||||
? 'Сохранить подписку'
|
||||
: subscriptionValidationStatus === 'invalid'
|
||||
? 'Ссылка подписки не распознана'
|
||||
: 'Проверяем ссылку подписки'}
|
||||
: subscriptionValidationStatus === 'unavailable'
|
||||
? 'Проверка подписки временно недоступна'
|
||||
: 'Проверяем ссылку подписки'}
|
||||
disabled={busy || subscriptionValidationStatus !== 'valid'}
|
||||
>
|
||||
{subscriptionValidationStatus === 'valid'
|
||||
? '✓'
|
||||
: subscriptionValidationStatus === 'invalid' ? '×' : '…'}
|
||||
: subscriptionValidationStatus === 'invalid'
|
||||
? '×'
|
||||
: subscriptionValidationStatus === 'unavailable' ? '!' : '…'}
|
||||
</button>
|
||||
)}
|
||||
</form>
|
||||
<InlineError error={subscriptionError || error} context="subscription" />
|
||||
</div>
|
||||
|
||||
{hasSubscription && subscriptionContentReady && hasUsage && (
|
||||
|
||||
Reference in New Issue
Block a user