Prevent duplicate server display during profile transitions
Build and Deploy Gateway / build-and-push (push) Successful in 26s
Build and Deploy Gateway / deploy (push) Successful in 6s

This commit is contained in:
2026-08-11 08:02:17 +03:00
parent 9ad0307333
commit 444f26c401
8 changed files with 33 additions and 31 deletions
+2 -2
View File
@@ -1,6 +1,6 @@
export const HARBOR_VERSIONS = Object.freeze({
macClient: '0.23.5',
gatewayClient: '0.24.5',
macClient: '0.23.6',
gatewayClient: '0.24.6',
gatewayBackend: '0.24.0',
});
@@ -728,6 +728,7 @@ export function ClientOverviewPage({
prompt={!profile.desiredServerId}
leaving={pickerState.leaving}
revealVersion={pickerState.revealVersion}
anchorServerId={pickerState.anchorServerId}
onSelect={(serverId) => selectServer(profile, serverId)}
/>}
/>
+8 -6
View File
@@ -167,6 +167,7 @@ interface ServerPickerProps {
prompt: boolean;
leaving: boolean;
revealVersion: number;
anchorServerId?: string;
onSelect: (id: string) => unknown;
}
@@ -179,6 +180,7 @@ export function ServerPicker({
prompt,
leaving,
revealVersion,
anchorServerId = '',
onSelect,
}: ServerPickerProps) {
const favoritesKey = `${FAVORITES_KEY}:${profileId}`;
@@ -207,7 +209,7 @@ export function ServerPicker({
if (view === 'recent') return recent.flatMap((id) => found.find((server) => server.id === id) || []);
return found;
}, [servers, query, view, favorites, recent]);
const results = filtered.filter(({ id }) => id !== selectedServerId);
const results = filtered.filter(({ id }) => id !== selectedServerId && id !== anchorServerId);
const pageCount = Math.max(1, Math.ceil(results.length / SERVER_RESULT_WINDOW));
const visible = results.slice(page * SERVER_RESULT_WINDOW, (page + 1) * SERVER_RESULT_WINDOW);
const grouped = servers.length >= 10;
@@ -282,7 +284,7 @@ export function ServerPicker({
<ServerCheckButton checking={checking} onClick={checkVisible} />
</div>
<div className="client-server-grid">
<ServerRow
{servers[0].id !== anchorServerId && <ServerRow
server={servers[0]}
selected={servers[0].id === selectedServerId}
favorite={false}
@@ -290,7 +292,7 @@ export function ServerPicker({
disabled={disabled}
index={0}
onSelect={onSelect}
/>
/>}
</div>
</section>;
}
@@ -310,8 +312,8 @@ export function ServerPicker({
));
const simpleServers = [
...(selected ? [selected] : []),
...servers.filter(({ id }) => id !== selectedServerId),
...(selected && selected.id !== anchorServerId ? [selected] : []),
...servers.filter(({ id }) => id !== selectedServerId && id !== anchorServerId),
].slice(0, SIMPLE_SERVER_LIMIT);
return <section className={`client-servers is-scalable${servers.length <= SIMPLE_SERVER_LIMIT ? ' is-short' : ''}`} aria-label="Выберите сервер">
@@ -403,7 +405,7 @@ export function ServerPicker({
fallback={autoActive ? undefined : 'Первый стабильный сервер'}
/>
</button>
{selected && <ServerRow
{selected && selected.id !== anchorServerId && <ServerRow
server={selected}
selected
favorite={favorites.includes(selected.id)}
@@ -29,6 +29,7 @@ interface ServerPickerRenderState {
disabled: boolean;
leaving: boolean;
revealVersion: number;
anchorServerId: string;
}
interface SubscriptionFeatureOptions {
@@ -464,9 +465,11 @@ function ProfileGroup({
{localStatus && <div className="client-profile-local-status" role="status">{localStatus}</div>}
{!expanded && !closing && visibleServer && <button
{visibleServer && <button
className="client-profile-selected-server"
type="button"
aria-expanded={expanded}
aria-controls={`client-profile-${profile.id}`}
onClick={() => feature.toggleProfile(profile.id)}
>
<span aria-hidden="true" />
@@ -486,6 +489,7 @@ function ProfileGroup({
disabled: controlsBlocked,
leaving: false,
revealVersion: feature.revealVersions[profile.id] || 0,
anchorServerId: visibleServer?.id || '',
})}
</div>
</div>}
+1 -9
View File
@@ -82,6 +82,7 @@
min-height: 48px;
display: grid;
grid-template-columns: 18px minmax(0, 1fr);
column-gap: 6px;
align-items: center;
padding: 4px 0 4px 8px;
border: 0;
@@ -278,15 +279,6 @@
font: inherit;
text-align: left;
cursor: pointer;
animation: client-profile-selected-in 260ms cubic-bezier(0.16, 1, 0.3, 1) both;
}
@keyframes client-profile-selected-in {
from {
opacity: 0;
filter: blur(3px);
transform: translateY(-4px);
}
}
.client-profile-selected-server > span {
+3 -1
View File
@@ -131,7 +131,9 @@ test('server picker starts simple and reveals advanced controls on demand', () =
assert.match(picker, /inert={!advanced \? true : undefined}/);
assert.match(picker, /const SIMPLE_SERVER_LIMIT = 5/);
assert.match(picker, /\.slice\(0, SIMPLE_SERVER_LIMIT\)/);
assert.match(picker, /\{selected && <ServerRow/);
assert.match(picker, /selected && selected\.id !== anchorServerId && <ServerRow/);
assert.match(picker, /id !== selectedServerId && id !== anchorServerId/);
assert.match(overview, /anchorServerId=\{pickerState\.anchorServerId\}/);
assert.match(styles, /\.client-profile-body \.client-server-row \{[\s\S]*?grid-template-columns: minmax\(0, 1fr\) 64px;/);
assert.match(styles, /\.client-profile-body \.client-server-row\.is-selected \{[\s\S]*?background: color-mix\(in oklch, var\(--client-accent\) 2%, transparent\);/);
assert.match(styles, /\.client-profile-body \.client-server-check-label \{[\s\S]*?display: inline;/);
+11 -11
View File
@@ -36,23 +36,23 @@ const expectedImports = [
const sha256 = (value) => crypto.createHash('sha256').update(value).digest('hex');
const acceptedLedger = {
counts: {
cascadeEdges: 821,
cascadeEdges: 820,
customProperties: 31,
declarations: 2986,
declarations: 2983,
important: 0,
keyframes: 57,
keyframes: 56,
media: 13,
rules: 921,
rules: 920,
variableReferences: 330,
},
hashes: {
cascadeEdges: 'ac82341d26b6598c9e54a5cc2f20fd791baf0ab6a99c8bd09e838608a16567f7',
cascadeEdges: '1590610791a755c9ec3c64da5b56da82764c9906344819fae414bf871bab1bad',
customProperties: 'c7dd331e4bad898c450568999d8c9c6837e275a79c365c7680e143026fde4545',
declarations: 'a7b2ecccc36118b0f77b100a48b2fdf8d8653587090a73b527b9565c7f5a88b8',
declarations: '0bde16ab93623a2546daf42e83f037dc00d65dbf08da5851bf433d22697338b8',
duplicateKeyframes: '4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945',
duplicateSelectors: 'c0ec5c96e48cd2ceed32aa5050383bcf29c3c0b8f1b440ed6883435e3e9b964d',
keyframes: '077e69647e0bd0761ca918e8d4109a9e8b07a1d7dbd9e0702bdca0e48011aed1',
ruleDeclarationSequences: '36ea3201b45a89f3607e9336d30e2d0e5b88400f921847fa28263c2a2b8b75ac',
keyframes: '39b6fc9c9ab451d63bb77afd90caf63d2d74a30cb351a8f86503de380d76a936',
ruleDeclarationSequences: '883a3996d9d4ccd4ab6124237b6e0c00f17505dc8ff7314a8c0069fa828a2475',
selectors: 'ec10ef7ce8f812c52c7ff6b6952ae66f5bbeebbc61e582ceddcb54c7112bbf2b',
variableReferences: '8cb18a63664f73e6186e1730d3cd97c6e9f11bbe6b0f5cf04900948494a71f78',
witnesses: '204a3ad7cb5b3681ea78a2e4ef1265515c33ca97ebfe560c082d51303cd3bedb',
@@ -306,8 +306,8 @@ test('main owns one public stylesheet and the regrouped production CSS is determ
assert.equal((main.match(/import ['"][^'"]+\.css['"]/g) || []).length, 1);
const assets = fs.readdirSync(path.join(root, 'dist/assets')).filter((file) => file.endsWith('.css'));
assert.deepEqual(assets, ['index-CgoReV4i.css']);
assert.deepEqual(assets, ['index-DgGVjCSB.css']);
const built = fs.readFileSync(path.join(root, 'dist/assets', assets[0]));
assert.equal(built.byteLength, 112580);
assert.equal(sha256(built), 'cdfeb513550fab679856b62c6ecb3d9bab8cbc464a0ffaa6c707701f7c2078ae');
assert.equal(built.byteLength, 112427);
assert.equal(sha256(built), '70e5b945a85ac965b31f43c25cf5766a3860453090678c44fbcdffa9951d624e');
});
@@ -106,7 +106,8 @@ test('profiles render as flat accordion groups with scoped controls', () => {
assert.match(feature, /aria-expanded=\{expanded\}/);
assert.match(feature, /profile\.subscription\.status === 'stale'[\s\S]*profile\.subscription\.fetchedAt/);
assert.match(feature, /feature\.operations\.profileRefresh\?\.target === profile\.id/);
assert.match(feature, /!expanded && !closing && visibleServer && <button[\s\S]*client-profile-selected-server/);
assert.match(feature, /\{visibleServer && <button[\s\S]*client-profile-selected-server[\s\S]*aria-expanded=\{expanded\}/);
assert.match(feature, /anchorServerId: visibleServer\?\.id \|\| ''/);
assert.match(feature, /const visibleServerId = applied \? feature\.selection\.appliedServerId : profile\.desiredServerId/);
assert.match(feature, /const desired = !feature\.connected[\s\S]*&& !feature\.gatewayDirect/);
assert.match(feature, /const profileDomain = subscriptionDomain\(profile\.subscription\.host\)/);