fix(tmux): polish installer banner state
This commit is contained in:
parent
898a795182
commit
ff9344c85a
3 changed files with 102 additions and 30 deletions
|
|
@ -50,6 +50,8 @@ interface AdaptInput {
|
||||||
detailsOpen: boolean;
|
detailsOpen: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const RESTART_REQUIRED_PATTERNS = ['restart', 'reboot', 'перезагруз', 'требуется перезагрузка'];
|
||||||
|
|
||||||
export class TmuxInstallerBannerAdapter {
|
export class TmuxInstallerBannerAdapter {
|
||||||
static create(): TmuxInstallerBannerAdapter {
|
static create(): TmuxInstallerBannerAdapter {
|
||||||
return new TmuxInstallerBannerAdapter();
|
return new TmuxInstallerBannerAdapter();
|
||||||
|
|
@ -58,19 +60,20 @@ export class TmuxInstallerBannerAdapter {
|
||||||
adapt(input: AdaptInput): TmuxInstallerBannerViewModel {
|
adapt(input: AdaptInput): TmuxInstallerBannerViewModel {
|
||||||
const status = input.status;
|
const status = input.status;
|
||||||
const snapshot = input.snapshot;
|
const snapshot = input.snapshot;
|
||||||
|
const displayPhase = this.#resolveDisplayPhase(snapshot, status);
|
||||||
const hasActiveInstallFlow =
|
const hasActiveInstallFlow =
|
||||||
snapshot.phase !== 'idle' && snapshot.phase !== 'completed' && snapshot.phase !== 'cancelled';
|
displayPhase !== 'idle' && displayPhase !== 'completed' && displayPhase !== 'cancelled';
|
||||||
const tmuxMissing = status ? !status.effective.available : !input.loading;
|
const tmuxMissing = status ? !status.effective.available : !input.loading;
|
||||||
const visible =
|
const visible =
|
||||||
hasActiveInstallFlow || (snapshot.phase !== 'completed' && !input.loading && tmuxMissing);
|
hasActiveInstallFlow || (displayPhase !== 'completed' && !input.loading && tmuxMissing);
|
||||||
const title =
|
const title =
|
||||||
snapshot.message &&
|
snapshot.message &&
|
||||||
(snapshot.phase === 'pending_external_elevation' ||
|
(displayPhase === 'pending_external_elevation' ||
|
||||||
snapshot.phase === 'waiting_for_external_step' ||
|
displayPhase === 'waiting_for_external_step' ||
|
||||||
snapshot.phase === 'needs_restart' ||
|
displayPhase === 'needs_restart' ||
|
||||||
snapshot.phase === 'needs_manual_step')
|
displayPhase === 'needs_manual_step')
|
||||||
? snapshot.message
|
? snapshot.message
|
||||||
: formatTmuxInstallerTitle(snapshot.phase);
|
: formatTmuxInstallerTitle(displayPhase);
|
||||||
const primaryGuideUrl =
|
const primaryGuideUrl =
|
||||||
status?.autoInstall.manualHints.find((hint) => typeof hint.url === 'string')?.url ?? null;
|
status?.autoInstall.manualHints.find((hint) => typeof hint.url === 'string')?.url ?? null;
|
||||||
const body =
|
const body =
|
||||||
|
|
@ -95,7 +98,7 @@ export class TmuxInstallerBannerAdapter {
|
||||||
const manualHints = status?.autoInstall.manualHints ?? [];
|
const manualHints = status?.autoInstall.manualHints ?? [];
|
||||||
const manualHintsCollapsible = status?.platform === 'win32' && manualHints.length > 0;
|
const manualHintsCollapsible = status?.platform === 'win32' && manualHints.length > 0;
|
||||||
const installLabel =
|
const installLabel =
|
||||||
snapshot.phase === 'idle' &&
|
displayPhase === 'idle' &&
|
||||||
status?.platform === 'win32' &&
|
status?.platform === 'win32' &&
|
||||||
status.autoInstall.strategy === 'wsl' &&
|
status.autoInstall.strategy === 'wsl' &&
|
||||||
status.autoInstall.supported
|
status.autoInstall.supported
|
||||||
|
|
@ -104,16 +107,16 @@ export class TmuxInstallerBannerAdapter {
|
||||||
: !status.wsl?.distroName
|
: !status.wsl?.distroName
|
||||||
? 'Install Ubuntu in WSL'
|
? 'Install Ubuntu in WSL'
|
||||||
: 'Install tmux in WSL'
|
: 'Install tmux in WSL'
|
||||||
: formatInstallButtonLabel(snapshot.phase);
|
: formatInstallButtonLabel(displayPhase);
|
||||||
const installDisabled =
|
const installDisabled =
|
||||||
input.loading ||
|
input.loading ||
|
||||||
snapshot.phase === 'preparing' ||
|
displayPhase === 'preparing' ||
|
||||||
snapshot.phase === 'checking' ||
|
displayPhase === 'checking' ||
|
||||||
snapshot.phase === 'requesting_privileges' ||
|
displayPhase === 'requesting_privileges' ||
|
||||||
snapshot.phase === 'pending_external_elevation' ||
|
displayPhase === 'pending_external_elevation' ||
|
||||||
snapshot.phase === 'waiting_for_external_step' ||
|
displayPhase === 'waiting_for_external_step' ||
|
||||||
snapshot.phase === 'installing' ||
|
displayPhase === 'installing' ||
|
||||||
snapshot.phase === 'verifying';
|
displayPhase === 'verifying';
|
||||||
const installButtonPrimary =
|
const installButtonPrimary =
|
||||||
!installDisabled && (installLabel.startsWith('Install') || installLabel.startsWith('Retry'));
|
!installDisabled && (installLabel.startsWith('Install') || installLabel.startsWith('Retry'));
|
||||||
const showRefreshButton =
|
const showRefreshButton =
|
||||||
|
|
@ -131,8 +134,8 @@ export class TmuxInstallerBannerAdapter {
|
||||||
locationLabel: formatTmuxLocationLabel(status?.effective.location ?? null),
|
locationLabel: formatTmuxLocationLabel(status?.effective.location ?? null),
|
||||||
runtimeReadyLabel,
|
runtimeReadyLabel,
|
||||||
versionLabel,
|
versionLabel,
|
||||||
phase: snapshot.phase,
|
phase: displayPhase,
|
||||||
progressPercent: formatTmuxInstallerProgress(snapshot.phase),
|
progressPercent: formatTmuxInstallerProgress(displayPhase),
|
||||||
logs: snapshot.logs,
|
logs: snapshot.logs,
|
||||||
manualHints,
|
manualHints,
|
||||||
manualHintsCollapsible,
|
manualHintsCollapsible,
|
||||||
|
|
@ -149,4 +152,23 @@ export class TmuxInstallerBannerAdapter {
|
||||||
detailsOpen: input.detailsOpen,
|
detailsOpen: input.detailsOpen,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#resolveDisplayPhase(
|
||||||
|
snapshot: TmuxInstallerSnapshot,
|
||||||
|
status: TmuxStatus | null
|
||||||
|
): TmuxInstallerSnapshot['phase'] {
|
||||||
|
if (snapshot.phase !== 'waiting_for_external_step') {
|
||||||
|
return snapshot.phase;
|
||||||
|
}
|
||||||
|
|
||||||
|
const combinedSignals = [snapshot.message, snapshot.detail, status?.wsl?.statusDetail, ...snapshot.logs]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join('\n')
|
||||||
|
.toLowerCase();
|
||||||
|
const restartRequired =
|
||||||
|
status?.wsl?.rebootRequired === true ||
|
||||||
|
RESTART_REQUIRED_PATTERNS.some((pattern) => combinedSignals.includes(pattern));
|
||||||
|
|
||||||
|
return restartRequired ? 'needs_restart' : snapshot.phase;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -350,4 +350,47 @@ describe('TmuxInstallerBannerAdapter', () => {
|
||||||
expect(result.installDisabled).toBe(true);
|
expect(result.installDisabled).toBe(true);
|
||||||
expect(result.showRefreshButton).toBe(true);
|
expect(result.showRefreshButton).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('shows a restart state when external-step details already require a reboot', () => {
|
||||||
|
const adapter = TmuxInstallerBannerAdapter.create();
|
||||||
|
|
||||||
|
const result = adapter.adapt({
|
||||||
|
status: {
|
||||||
|
...baseStatus,
|
||||||
|
platform: 'win32',
|
||||||
|
autoInstall: {
|
||||||
|
...baseStatus.autoInstall,
|
||||||
|
supported: true,
|
||||||
|
strategy: 'wsl',
|
||||||
|
},
|
||||||
|
wsl: {
|
||||||
|
wslInstalled: true,
|
||||||
|
rebootRequired: false,
|
||||||
|
distroName: null,
|
||||||
|
distroVersion: null,
|
||||||
|
distroBootstrapped: false,
|
||||||
|
innerPackageManager: null,
|
||||||
|
tmuxAvailableInsideWsl: false,
|
||||||
|
tmuxVersion: null,
|
||||||
|
tmuxBinaryPath: null,
|
||||||
|
statusDetail: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
snapshot: {
|
||||||
|
...idleSnapshot,
|
||||||
|
phase: 'waiting_for_external_step',
|
||||||
|
strategy: 'wsl',
|
||||||
|
message: 'Checking WSL after the administrator step...',
|
||||||
|
detail:
|
||||||
|
'Требуемая операция выполнена успешно. Чтобы сделанные изменения вступили в силу, следует перезагрузить систему.',
|
||||||
|
},
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
detailsOpen: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.phase).toBe('needs_restart');
|
||||||
|
expect(result.progressPercent).toBe(96);
|
||||||
|
expect(result.installLabel).toBe('Re-check after restart');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,7 @@ export function TmuxInstallerBannerView(): React.JSX.Element | null {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`mb-6 rounded-lg border-l-4 px-4 py-3 ${BANNER_MIN_H}`}
|
className={`mb-6 rounded-lg border-l-4 px-3 ${expanded ? `py-3 ${BANNER_MIN_H}` : 'py-2.5'}`}
|
||||||
style={{
|
style={{
|
||||||
borderLeftColor: viewModel.error ? '#ef4444' : '#f59e0b',
|
borderLeftColor: viewModel.error ? '#ef4444' : '#f59e0b',
|
||||||
backgroundColor: 'rgba(245, 158, 11, 0.08)',
|
backgroundColor: 'rgba(245, 158, 11, 0.08)',
|
||||||
|
|
@ -90,28 +90,35 @@ export function TmuxInstallerBannerView(): React.JSX.Element | null {
|
||||||
type="button"
|
type="button"
|
||||||
aria-expanded={expanded}
|
aria-expanded={expanded}
|
||||||
onClick={() => setExpanded((current) => !current)}
|
onClick={() => setExpanded((current) => !current)}
|
||||||
className="flex min-h-[1.75rem] w-full items-center justify-between gap-3 text-left"
|
className="group flex w-full items-center justify-between gap-3 rounded-md px-1 py-0.5 text-left transition-colors hover:bg-white/[0.03]"
|
||||||
>
|
>
|
||||||
<span className="flex min-w-0 items-center gap-3">
|
<span className="flex min-w-0 items-center gap-2.5">
|
||||||
<span className="shrink-0">
|
<span className="inline-flex shrink-0 items-center justify-center">
|
||||||
{viewModel.error ? (
|
{viewModel.error ? (
|
||||||
<AlertTriangle className="size-4 text-red-300" />
|
<AlertTriangle className="size-3.5 text-red-300" />
|
||||||
) : (
|
) : (
|
||||||
<Wrench className="size-4 text-amber-300" />
|
<Wrench className="size-3.5 text-amber-300" />
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
className="truncate text-sm leading-5"
|
className="truncate text-xs font-medium leading-5"
|
||||||
style={{ color: 'var(--color-text)' }}
|
style={{ color: 'var(--color-text)' }}
|
||||||
>
|
>
|
||||||
{SUMMARY_TITLE}
|
{SUMMARY_TITLE}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
{expanded ? (
|
<span
|
||||||
<ChevronUp className="size-4 shrink-0" />
|
className="inline-flex size-6 shrink-0 items-center justify-center rounded-md transition-colors group-hover:bg-white/[0.03]"
|
||||||
) : (
|
style={{
|
||||||
<ChevronDown className="size-4 shrink-0" />
|
color: 'var(--color-text-secondary)',
|
||||||
)}
|
}}
|
||||||
|
>
|
||||||
|
{expanded ? (
|
||||||
|
<ChevronUp className="size-3.5 shrink-0" />
|
||||||
|
) : (
|
||||||
|
<ChevronDown className="size-3.5 shrink-0" />
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{expanded && (
|
{expanded && (
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue