Add per-row connectivity diagnostic refresh controls
Build and Deploy Gateway / build-and-push (push) Successful in 22s
Build and Deploy Gateway / deploy (push) Successful in 7s

This commit is contained in:
2026-08-19 12:59:21 +03:00
parent 7cb25d0633
commit df865fbe3d
9 changed files with 131 additions and 23 deletions
+2 -2
View File
@@ -1,6 +1,6 @@
export const HARBOR_VERSIONS = Object.freeze({
macClient: '0.26.6',
gatewayClient: '0.27.6',
macClient: '0.26.7',
gatewayClient: '0.27.7',
gatewayBackend: '0.27.0',
});
@@ -109,6 +109,34 @@ function Status({ value, route }: { value: StatusValue; route: string }) {
</span>;
}
function RowRefresh({
label,
running,
disabled,
onRun,
}: {
label: string;
running: boolean;
disabled: boolean;
onRun: () => void;
}) {
return <span className="client-diagnostics-row-refresh-wrap client-tooltip-anchor">
<button
className={`client-diagnostics-refresh client-diagnostics-row-refresh${running ? ' is-running' : ''}`}
type="button"
aria-label={`Проверить: ${label}`}
aria-busy={running}
disabled={disabled}
onClick={onRun}
>
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M20 11a8 8 0 1 0-2.3 6.7M20 5v6h-6" />
</svg>
</button>
<Tooltip>Проверить только эту строку</Tooltip>
</span>;
}
function ipResult(path: DiagnosticPath | undefined, source: IpSourceDefinition) {
if (!path?.available) return null;
return source.family === 6
@@ -222,6 +250,7 @@ export function ConnectivityDiagnosticsPanel({
const sheetRef = useRef<HTMLDivElement>(null);
const runnerRef = useRef<HTMLSpanElement>(null);
const previousTargetRef = useRef<string | null>(null);
const retryTargetRef = useRef<string | undefined>(undefined);
const requestError = requestDetails(error);
useEffect(() => {
@@ -264,12 +293,13 @@ export function ConnectivityDiagnosticsPanel({
previousTargetRef.current = activeTarget;
}, [activeTarget]);
async function run() {
async function run(onlyTarget?: string) {
retryTargetRef.current = onlyTarget;
setStatus('running');
setError(null);
try {
let next = result;
const targets = [
const targets = onlyTarget ? [onlyTarget] : [
CONNECTIVITY_NETWORK_SOURCE.id,
...CONNECTIVITY_IP_SOURCES.map(({ id }) => `ip:${id}`),
...sites.map(({ id }) => `site:${id}`),
@@ -379,7 +409,7 @@ export function ConnectivityDiagnosticsPanel({
aria-label="Проверить маршруты"
aria-busy={pending}
disabled={pending}
onClick={run}
onClick={() => run()}
>
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M20 11a8 8 0 1 0-2.3 6.7M20 5v6h-6" />
@@ -393,7 +423,7 @@ export function ConnectivityDiagnosticsPanel({
{Boolean(error) && <div className="client-diagnostics-feedback">
<div className="client-diagnostics-error" role="alert">
<span>{requestError.message}</span>
{requestError.retryable && <button type="button" onClick={run}>Повторить</button>}
{requestError.retryable && <button type="button" onClick={() => run(retryTargetRef.current)}>Повторить</button>}
</div>
</div>}
@@ -412,7 +442,15 @@ export function ConnectivityDiagnosticsPanel({
data-diagnostic-target={CONNECTIVITY_NETWORK_SOURCE.id}
className={activeTarget === CONNECTIVITY_NETWORK_SOURCE.id ? 'is-running' : undefined}
>
<th scope="row">{CONNECTIVITY_NETWORK_SOURCE.label}</th>
<th scope="row" aria-label={CONNECTIVITY_NETWORK_SOURCE.label}><span className="client-diagnostics-row-name">
<span>{CONNECTIVITY_NETWORK_SOURCE.label}</span>
<RowRefresh
label={CONNECTIVITY_NETWORK_SOURCE.label}
running={activeTarget === CONNECTIVITY_NETWORK_SOURCE.id}
disabled={pending}
onRun={() => run(CONNECTIVITY_NETWORK_SOURCE.id)}
/>
</span></th>
<td><NetworkCell
path={result?.direct}
pending={activeTarget === CONNECTIVITY_NETWORK_SOURCE.id}
@@ -428,7 +466,15 @@ export function ConnectivityDiagnosticsPanel({
const target = `ip:${source.id}`;
const running = activeTarget === target;
return <tr key={source.id} data-diagnostic-target={target} className={running ? 'is-running' : undefined}>
<th scope="row">{source.label}</th>
<th scope="row" aria-label={source.label}><span className="client-diagnostics-row-name">
<span>{source.label}</span>
<RowRefresh
label={source.label}
running={running}
disabled={pending}
onRun={() => run(target)}
/>
</span></th>
<td><IpCell path={result?.direct} source={source} pending={running} route={`Напрямую, ${source.label}`} /></td>
<td><IpCell path={result?.vpn} source={source} pending={running} route={`VPN, ${source.label}`} /></td>
</tr>})}</tbody>
@@ -460,7 +506,15 @@ export function ConnectivityDiagnosticsPanel({
style={{ viewTransitionName: removing ? 'none' : `diagnostic-service-${site.id}` }}
inert={removing ? true : undefined}
>
<span role="rowheader" className="client-diagnostics-service-name">{site.label}</span>
<span role="rowheader" aria-label={site.label} className="client-diagnostics-service-name client-diagnostics-row-name">
<span>{site.label}</span>
<RowRefresh
label={site.label}
running={running}
disabled={pending}
onRun={() => run(`site:${site.id}`)}
/>
</span>
<span role="cell"><Status value={resultStatus(direct, running)} route={`Напрямую, ${site.label}`} /></span>
<span role="cell"><Status value={resultStatus(vpn, running, result?.vpn?.available !== false)} route={`VPN, ${site.label}`} /></span>
<button
+25
View File
@@ -79,6 +79,31 @@
transform: translate(0, 0);
}
.client-diagnostics-row-name {
min-width: 0;
display: inline-flex;
align-items: center;
gap: 4px;
max-width: 100%;
}
.client-diagnostics-row-name > span:first-child {
min-width: 0;
overflow-wrap: anywhere;
}
.client-diagnostics-row-refresh-wrap,
.client-diagnostics-row-refresh {
width: 24px;
height: 24px;
flex: 0 0 24px;
}
.client-diagnostics-row-refresh svg {
width: 13px;
height: 13px;
}
.client-diagnostics-feedback {
display: flex;
align-items: center;