Add initial server health check and update Harbor versions
All checks were successful
Build and Deploy Gateway / build-and-push (push) Successful in 14s
Build and Deploy Gateway / deploy (push) Successful in 7s

This commit is contained in:
2026-07-12 18:49:45 +03:00
parent d551d41b71
commit 5874df2fce
4 changed files with 79 additions and 19 deletions

View File

@@ -1,6 +1,6 @@
export const HARBOR_VERSIONS = Object.freeze({ export const HARBOR_VERSIONS = Object.freeze({
macClient: '0.7.21', macClient: '0.7.22',
gatewayClient: '0.7.21', gatewayClient: '0.7.22',
gatewayBackend: '0.7.1', gatewayBackend: '0.7.1',
}); });

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useMemo, useState } from 'react'; import React, { useEffect, useMemo, useRef, useState } from 'react';
import { api } from '../api.js'; import { api } from '../api.js';
import { import {
autoServer, autoServer,
@@ -89,6 +89,7 @@ export function ServerPicker({
const [collapsed, setCollapsed] = useState([]); const [collapsed, setCollapsed] = useState([]);
const [pings, setPings] = useState({}); const [pings, setPings] = useState({});
const [checking, setChecking] = useState(false); const [checking, setChecking] = useState(false);
const initialCheckStarted = useRef(false);
const serverKey = servers.map(({ id }) => id).join('|'); const serverKey = servers.map(({ id }) => id).join('|');
useEffect(() => { useEffect(() => {
@@ -156,14 +157,26 @@ export function ServerPicker({
} }
} }
useEffect(() => {
if (initialCheckStarted.current || !servers.length) return;
initialCheckStarted.current = true;
checkVisible();
}, [serverKey]);
if (servers.length === 1) { if (servers.length === 1) {
return <section className="client-servers" aria-label="Выберите сервер"> return <section className="client-servers" aria-label="Выберите сервер">
{prompt && <span className="client-server-prompt">Выберите сервер</span>} {prompt && <span className="client-server-prompt">Выберите сервер</span>}
<div className="client-server-toolbar is-single">
<button className="client-server-check" type="button" disabled={checking} onClick={checkVisible}>
{checking ? 'Проверяем…' : 'Проверить TCP'}
</button>
</div>
<div className="client-server-grid"> <div className="client-server-grid">
<ServerRow <ServerRow
server={servers[0]} server={servers[0]}
selected={servers[0].id === selectedServerId} selected={servers[0].id === selectedServerId}
favorite={false} favorite={false}
ping={pings[servers[0].id]}
disabled={disabled} disabled={disabled}
index={0} index={0}
onSelect={onSelect} onSelect={onSelect}
@@ -193,16 +206,21 @@ export function ServerPicker({
return <section className="client-servers is-scalable" aria-label="Выберите сервер"> return <section className="client-servers is-scalable" aria-label="Выберите сервер">
{prompt && <span className="client-server-prompt">Выберите сервер</span>} {prompt && <span className="client-server-prompt">Выберите сервер</span>}
<button <div className="client-server-toolbar">
className={`client-server-mode-toggle${advanced ? ' is-open' : ''}`} <button
type="button" className={`client-server-mode-toggle${advanced ? ' is-open' : ''}`}
aria-expanded={advanced} type="button"
aria-label={advanced ? 'Скрыть поиск и фильтры' : 'Показать поиск и фильтры'} aria-expanded={advanced}
onClick={() => setAdvanced((current) => !current)} aria-label={advanced ? 'Скрыть поиск и фильтры' : 'Показать поиск и фильтры'}
> onClick={() => setAdvanced((current) => !current)}
<span>Поиск и фильтры</span> >
<svg viewBox="0 0 12 8" aria-hidden="true"><path d="m1 1 5 5 5-5" /></svg> <span>Поиск и фильтры</span>
</button> <svg viewBox="0 0 12 8" aria-hidden="true"><path d="m1 1 5 5 5-5" /></svg>
</button>
<button className="client-server-check" type="button" disabled={checking || !servers.length} onClick={checkVisible}>
{checking ? 'Проверяем…' : 'Проверить TCP'}
</button>
</div>
<div className="client-server-mode-panels"> <div className="client-server-mode-panels">
<div <div
@@ -256,9 +274,6 @@ export function ServerPicker({
key={id} key={id}
onClick={() => setView(id)} onClick={() => setView(id)}
>{label}</button>)} >{label}</button>)}
<button type="button" disabled={checking || (!selectedServerId && !visible.length)} onClick={checkVisible}>
{checking ? 'Проверяем…' : 'Проверить TCP'}
</button>
</div> </div>
</div> </div>

View File

@@ -2728,13 +2728,26 @@ p {
margin-bottom: 14px; margin-bottom: 14px;
} }
.client-server-toolbar {
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
margin-bottom: 8px;
}
.client-server-toolbar.is-single {
display: flex;
justify-content: center;
}
.client-server-mode-toggle { .client-server-mode-toggle {
min-height: 32px; min-height: 32px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 7px; gap: 7px;
margin: 0 auto 8px; grid-column: 2;
margin: 0;
padding: 5px 8px; padding: 5px 8px;
border: 0; border: 0;
background: transparent; background: transparent;
@@ -2744,6 +2757,34 @@ p {
transition: color 220ms ease, text-shadow 320ms ease; transition: color 220ms ease, text-shadow 320ms ease;
} }
.client-server-check {
grid-column: 3;
justify-self: start;
width: 88px;
min-height: 32px;
padding: 5px 7px;
border: 0;
background: transparent;
color: var(--client-muted);
font: 700 8px/1.2 'JetBrains Mono', 'SF Mono', ui-monospace, Menlo, monospace;
cursor: pointer;
}
.client-server-check:hover:not(:disabled),
.client-server-check:focus-visible {
color: var(--client-accent);
}
.client-server-check:focus-visible {
outline: 2px solid var(--client-accent);
outline-offset: 1px;
}
.client-server-check:disabled {
cursor: default;
opacity: 0.55;
}
.client-server-mode-toggle:hover, .client-server-mode-toggle:hover,
.client-server-mode-toggle.is-open { .client-server-mode-toggle.is-open {
color: var(--client-accent); color: var(--client-accent);

View File

@@ -36,8 +36,11 @@ test('server picker handles 1, 30 and 300 stable-ID servers with duplicate label
assert.equal(SERVER_RESULT_WINDOW, 60); assert.equal(SERVER_RESULT_WINDOW, 60);
}); });
test('server picker keeps health manual and the rendered result window bounded', () => { test('server picker checks health once on load, keeps manual refresh and bounds the result window', () => {
assert.doesNotMatch(overview, /pingAll|servers\.ping/); assert.doesNotMatch(overview, /pingAll|servers\.ping/);
assert.match(picker, /const initialCheckStarted = useRef\(false\)/);
assert.match(picker, /if \(initialCheckStarted\.current \|\| !servers\.length\) return/);
assert.match(picker, /initialCheckStarted\.current = true;\s*checkVisible\(\)/);
assert.match(picker, /onClick={checkVisible}/); assert.match(picker, /onClick={checkVisible}/);
assert.match(picker, /\.slice\(page \* SERVER_RESULT_WINDOW, \(page \+ 1\) \* SERVER_RESULT_WINDOW\)/); assert.match(picker, /\.slice\(page \* SERVER_RESULT_WINDOW, \(page \+ 1\) \* SERVER_RESULT_WINDOW\)/);
assert.match(picker, /\.slice\(0, 30\)/); assert.match(picker, /\.slice\(0, 30\)/);
@@ -50,7 +53,8 @@ test('server picker keeps health manual and the rendered result window bounded',
test('manual health remains visible for Auto and simple rows', () => { test('manual health remains visible for Auto and simple rows', () => {
assert.match(picker, /autoActive \? serverHealthText\(pings\[selectedServerId\]\)/); assert.match(picker, /autoActive \? serverHealthText\(pings\[selectedServerId\]\)/);
assert.match(picker, /disabled={checking \|\| \(!selectedServerId && !visible\.length\)}/); assert.match(picker, /className="client-server-check"/);
assert.match(picker, /server={servers\[0\]}[\s\S]*?ping={pings\[servers\[0\]\.id\]}/);
assert.match(picker, /simpleServers\.map[\s\S]*?ping={pings\[server\.id\]}/); assert.match(picker, /simpleServers\.map[\s\S]*?ping={pings\[server\.id\]}/);
}); });