Handle expired subscriptions in client profile UI
Build and Deploy Gateway / build-and-push (push) Successful in 21s
Build and Deploy Gateway / deploy (push) Successful in 6s

This commit is contained in:
2026-08-13 15:06:13 +03:00
parent 0ea2f9d548
commit 019930924d
8 changed files with 68 additions and 39 deletions
+2 -2
View File
@@ -1,6 +1,6 @@
export const HARBOR_VERSIONS = Object.freeze({
macClient: '0.25.8',
gatewayClient: '0.26.7',
macClient: '0.25.9',
gatewayClient: '0.26.8',
gatewayBackend: '0.26.5',
});
+7 -2
View File
@@ -482,12 +482,17 @@ export function ClientOverviewPage({
&& (canonicalSwitch || localSwitch)
&& Boolean(operationProfile && operationServer)
&& (operationProfile?.id !== appliedProfile?.id || operationServer?.id !== appliedServer?.id);
const subscriptionError = error?.context === 'subscription'
&& profiles.some((profile) => profile.id === error.profileId
&& profile.subscription.errorCode === 'SUBSCRIPTION_EXPIRED')
? null
: error;
const subscriptionFeature = useSubscriptionFeature({
profiles,
selection: state.selection,
connected,
operations: visibleOperations,
error,
error: subscriptionError,
isGateway,
gatewayDirect,
onAdd: onAddProfile,
@@ -716,7 +721,7 @@ export function ClientOverviewPage({
<SubscriptionPanel
feature={subscriptionFeature}
statusSlot={<>
<InlineError error={subscriptionFeature.error || error} context="subscription" />
<InlineError error={subscriptionFeature.error} context="subscription" />
</>}
renderServerPicker={(profile, pickerState) => <ServerPicker
profileId={profile.id}
@@ -378,7 +378,8 @@ function ProfileGroup({
profile: ProfileSnapshot;
renderServerPicker: (profile: ProfileSnapshot, state: ServerPickerRenderState) => ReactNode;
}) {
const expanded = feature.expanded.includes(profile.id);
const expired = profile.subscription.errorCode === 'SUBSCRIPTION_EXPIRED';
const expanded = !expired && feature.expanded.includes(profile.id);
const closing = feature.closing.includes(profile.id);
const visited = feature.visited.includes(profile.id);
const applied = feature.connected
@@ -401,18 +402,29 @@ function ProfileGroup({
const visibleServerLocation = [visibleServer?.city, visibleServer?.country]
.filter((value): value is string => typeof value === 'string' && Boolean(value))
.join(' · ');
const localStatus = profileError?.message || (profile.subscription.status === 'stale'
const localStatus = !expired && (profileError?.message || (profile.subscription.status === 'stale'
? `Последнее обновление не удалось. Данные от ${profile.subscription.fetchedAt
? new Date(profile.subscription.fetchedAt).toLocaleString('ru-RU', { dateStyle: 'short', timeStyle: 'short' })
: 'предыдущей загрузки'}.`
: '');
: ''));
const profileDomain = subscriptionDomain(profile.subscription.host);
const profileDetails = `Серверов: ${profile.servers.length}${profile.subscription.fetchedAt
? ` · обновлено ${new Date(profile.subscription.fetchedAt).toLocaleDateString('ru-RU')}`
: ''}`;
return <section className={`client-profile-group${expanded ? ' is-expanded' : ''}${applied ? ' is-active' : ''}`}>
const profileDetails = expired
? 'Срок действия подписки истёк'
: `Серверов: ${profile.servers.length}${profile.subscription.fetchedAt
? ` · обновлено ${new Date(profile.subscription.fetchedAt).toLocaleDateString('ru-RU')}`
: ''}`;
const profileTitle = <span className="client-profile-title">
<span>
<strong>{profileDomain}</strong>
{(applied || desired) && <em>{applied ? 'АКТИВНА' : 'ВЫБРАНА'}</em>}
</span>
<small>{profileDetails}</small>
</span>;
return <section className={`client-profile-group${expanded ? ' is-expanded' : ''}${applied ? ' is-active' : ''}${expired ? ' is-expired' : ''}`}>
<div className="client-profile-header">
<button
{expired ? <div className="client-profile-disclosure is-static">
{profileTitle}
</div> : <button
className="client-profile-disclosure"
type="button"
aria-expanded={expanded}
@@ -420,15 +432,9 @@ function ProfileGroup({
onClick={() => feature.toggleProfile(profile.id)}
>
<span className="client-profile-chevron" aria-hidden="true"></span>
<span className="client-profile-title">
<span>
<strong>{profileDomain}</strong>
{(applied || desired) && <em>{applied ? 'АКТИВНА' : 'ВЫБРАНА'}</em>}
</span>
<small>{profileDetails}</small>
</span>
</button>
{(applied || desired) && <ProfileUsage profile={profile} />}
{profileTitle}
</button>}
{!expired && (applied || desired) && <ProfileUsage profile={profile} />}
<button
className={`client-profile-refresh${refreshing ? ' is-refreshing' : ''}`}
type="button"
@@ -456,7 +462,7 @@ function ProfileGroup({
{localStatus && <div className="client-profile-local-status" role="status">{localStatus}</div>}
{visibleServer && <button
{!expired && visibleServer && <button
className="client-profile-selected-server"
type="button"
aria-expanded={expanded}
@@ -468,7 +474,7 @@ function ProfileGroup({
{visibleServerLocation && <small>{visibleServerLocation}</small>}
</button>}
{visited && <div
{!expired && visited && <div
id={`client-profile-${profile.id}`}
className={`client-profile-body${expanded ? ' is-open' : ''}${closing ? ' is-closing' : ''}`}
aria-hidden={!expanded}
+1 -1
View File
@@ -651,7 +651,7 @@
}
.client-profile-body.is-open .client-server-toolbar {
animation: client-profile-ping-in 300ms 620ms cubic-bezier(0.16, 1, 0.3, 1) both;
animation: client-profile-ping-in 420ms cubic-bezier(0.16, 1, 0.3, 1) both;
}
.client-profile-body.is-closing .client-server-toolbar {
+13 -1
View File
@@ -105,6 +105,14 @@
transform: rotate(90deg);
}
.client-profile-disclosure.is-static {
cursor: default;
}
.client-profile-disclosure.is-static .client-profile-title {
grid-column: 2;
}
.client-profile-title {
min-width: 0;
display: grid;
@@ -139,6 +147,10 @@
text-transform: var(--type-label-transform);
}
.client-profile-group.is-expired .client-profile-title small {
color: oklch(0.68 0.14 72);
}
.client-profile-title em {
padding: 0;
color: var(--client-accent);
@@ -276,7 +288,7 @@
.client-profile-body-inner {
min-height: 0;
overflow: hidden;
padding-bottom: 4px;
padding: 0 0 4px 24px;
}
.client-profile-body .client-servers {