feat(tmux): collapse windows setup steps by default
This commit is contained in:
parent
a9668ff15d
commit
dd7b729520
4 changed files with 175 additions and 3 deletions
|
|
@ -26,6 +26,7 @@ export interface TmuxInstallerBannerViewModel {
|
||||||
progressPercent: number | null;
|
progressPercent: number | null;
|
||||||
logs: string[];
|
logs: string[];
|
||||||
manualHints: TmuxInstallHint[];
|
manualHints: TmuxInstallHint[];
|
||||||
|
manualHintsCollapsible: boolean;
|
||||||
primaryGuideUrl: string | null;
|
primaryGuideUrl: string | null;
|
||||||
installSupported: boolean;
|
installSupported: boolean;
|
||||||
installDisabled: boolean;
|
installDisabled: boolean;
|
||||||
|
|
@ -79,6 +80,8 @@ export class TmuxInstallerBannerAdapter {
|
||||||
: null;
|
: null;
|
||||||
const versionLabel =
|
const versionLabel =
|
||||||
status?.effective.version ?? status?.host.version ?? status?.wsl?.tmuxVersion ?? null;
|
status?.effective.version ?? status?.host.version ?? status?.wsl?.tmuxVersion ?? null;
|
||||||
|
const manualHints = status?.autoInstall.manualHints ?? [];
|
||||||
|
const manualHintsCollapsible = status?.platform === 'win32' && manualHints.length > 0;
|
||||||
const installLabel =
|
const installLabel =
|
||||||
snapshot.phase === 'idle' &&
|
snapshot.phase === 'idle' &&
|
||||||
status?.platform === 'win32' &&
|
status?.platform === 'win32' &&
|
||||||
|
|
@ -104,7 +107,8 @@ export class TmuxInstallerBannerAdapter {
|
||||||
phase: snapshot.phase,
|
phase: snapshot.phase,
|
||||||
progressPercent: formatTmuxInstallerProgress(snapshot.phase),
|
progressPercent: formatTmuxInstallerProgress(snapshot.phase),
|
||||||
logs: snapshot.logs,
|
logs: snapshot.logs,
|
||||||
manualHints: status?.autoInstall.manualHints ?? [],
|
manualHints,
|
||||||
|
manualHintsCollapsible,
|
||||||
primaryGuideUrl,
|
primaryGuideUrl,
|
||||||
installSupported: status?.autoInstall.supported ?? false,
|
installSupported: status?.autoInstall.supported ?? false,
|
||||||
installDisabled:
|
installDisabled:
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,7 @@ describe('TmuxInstallerBannerAdapter', () => {
|
||||||
expect(result.primaryGuideUrl).toBeNull();
|
expect(result.primaryGuideUrl).toBeNull();
|
||||||
expect(result.progressPercent).toBeNull();
|
expect(result.progressPercent).toBeNull();
|
||||||
expect(result.manualHints).toHaveLength(1);
|
expect(result.manualHints).toHaveLength(1);
|
||||||
|
expect(result.manualHintsCollapsible).toBe(false);
|
||||||
expect(result.body).toContain('persistent teammate reliability');
|
expect(result.body).toContain('persistent teammate reliability');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -143,6 +144,7 @@ describe('TmuxInstallerBannerAdapter', () => {
|
||||||
expect(result.platformLabel).toBe('Windows');
|
expect(result.platformLabel).toBe('Windows');
|
||||||
expect(result.primaryGuideUrl).toBe('https://learn.microsoft.com/en-us/windows/wsl/install');
|
expect(result.primaryGuideUrl).toBe('https://learn.microsoft.com/en-us/windows/wsl/install');
|
||||||
expect(result.progressPercent).toBe(100);
|
expect(result.progressPercent).toBe(100);
|
||||||
|
expect(result.manualHintsCollapsible).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('keeps the banner visible when tmux is installed but runtime is not ready yet', () => {
|
it('keeps the banner visible when tmux is installed but runtime is not ready yet', () => {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,14 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { AlertTriangle, ExternalLink, RefreshCw, Wrench, XCircle } from 'lucide-react';
|
import {
|
||||||
|
AlertTriangle,
|
||||||
|
ChevronDown,
|
||||||
|
ChevronUp,
|
||||||
|
ExternalLink,
|
||||||
|
RefreshCw,
|
||||||
|
Wrench,
|
||||||
|
XCircle,
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
import { useTmuxInstallerBanner } from '../hooks/useTmuxInstallerBanner';
|
import { useTmuxInstallerBanner } from '../hooks/useTmuxInstallerBanner';
|
||||||
|
|
||||||
|
|
@ -28,6 +36,7 @@ export function TmuxInstallerBannerView(): React.JSX.Element | null {
|
||||||
const { viewModel, install, cancel, submitInput, refresh, toggleDetails, openExternal } =
|
const { viewModel, install, cancel, submitInput, refresh, toggleDetails, openExternal } =
|
||||||
useTmuxInstallerBanner();
|
useTmuxInstallerBanner();
|
||||||
const [inputValue, setInputValue] = React.useState('');
|
const [inputValue, setInputValue] = React.useState('');
|
||||||
|
const [manualHintsExpanded, setManualHintsExpanded] = React.useState(false);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!viewModel.acceptsInput) {
|
if (!viewModel.acceptsInput) {
|
||||||
|
|
@ -35,10 +44,19 @@ export function TmuxInstallerBannerView(): React.JSX.Element | null {
|
||||||
}
|
}
|
||||||
}, [viewModel.acceptsInput]);
|
}, [viewModel.acceptsInput]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!viewModel.manualHintsCollapsible) {
|
||||||
|
setManualHintsExpanded(false);
|
||||||
|
}
|
||||||
|
}, [viewModel.manualHintsCollapsible]);
|
||||||
|
|
||||||
if (!viewModel.visible) {
|
if (!viewModel.visible) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const manualHintsVisible =
|
||||||
|
viewModel.manualHints.length > 0 && (!viewModel.manualHintsCollapsible || manualHintsExpanded);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="mb-6 rounded-lg border-l-4 px-4 py-3"
|
className="mb-6 rounded-lg border-l-4 px-4 py-3"
|
||||||
|
|
@ -114,6 +132,23 @@ export function TmuxInstallerBannerView(): React.JSX.Element | null {
|
||||||
Manual guide
|
Manual guide
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
{viewModel.manualHintsCollapsible && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setManualHintsExpanded((current) => !current)}
|
||||||
|
className="inline-flex items-center gap-2 rounded-md border px-3 py-2 text-sm transition-colors hover:bg-white/5"
|
||||||
|
style={{ borderColor: 'var(--color-border)' }}
|
||||||
|
>
|
||||||
|
{manualHintsExpanded ? (
|
||||||
|
<ChevronUp className="size-4" />
|
||||||
|
) : (
|
||||||
|
<ChevronDown className="size-4" />
|
||||||
|
)}
|
||||||
|
{manualHintsExpanded
|
||||||
|
? 'Hide setup steps'
|
||||||
|
: `Show setup steps (${viewModel.manualHints.length})`}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => void refresh()}
|
onClick={() => void refresh()}
|
||||||
|
|
@ -193,7 +228,7 @@ export function TmuxInstallerBannerView(): React.JSX.Element | null {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{viewModel.manualHints.length > 0 && (
|
{manualHintsVisible && (
|
||||||
<div className="mt-3 grid gap-2 lg:grid-cols-2">
|
<div className="mt-3 grid gap-2 lg:grid-cols-2">
|
||||||
{viewModel.manualHints.map((hint) => (
|
{viewModel.manualHints.map((hint) => (
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,131 @@
|
||||||
|
import React, { act } from 'react';
|
||||||
|
import { createRoot } from 'react-dom/client';
|
||||||
|
|
||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
import { TmuxInstallerBannerView } from '../TmuxInstallerBannerView';
|
||||||
|
|
||||||
|
import type { TmuxInstallerBannerViewModel } from '../../adapters/TmuxInstallerBannerAdapter';
|
||||||
|
|
||||||
|
const { mockUseTmuxInstallerBanner } = vi.hoisted(() => ({
|
||||||
|
mockUseTmuxInstallerBanner: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../hooks/useTmuxInstallerBanner', () => ({
|
||||||
|
useTmuxInstallerBanner: mockUseTmuxInstallerBanner,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const baseViewModel: TmuxInstallerBannerViewModel = {
|
||||||
|
visible: true,
|
||||||
|
loading: false,
|
||||||
|
title: 'tmux is not installed',
|
||||||
|
body: 'WSL is available, but no Linux distribution is installed yet.',
|
||||||
|
error: null,
|
||||||
|
platformLabel: 'Windows',
|
||||||
|
locationLabel: null,
|
||||||
|
runtimeReadyLabel: null,
|
||||||
|
versionLabel: null,
|
||||||
|
phase: 'idle',
|
||||||
|
progressPercent: null,
|
||||||
|
logs: [],
|
||||||
|
manualHints: [
|
||||||
|
{
|
||||||
|
title: 'Install WSL',
|
||||||
|
description: 'Install Windows Subsystem for Linux.',
|
||||||
|
command: 'wsl --install --no-distribution',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Install Ubuntu',
|
||||||
|
description: 'Recommended WSL distro.',
|
||||||
|
command: 'wsl --install -d Ubuntu --no-launch',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
manualHintsCollapsible: true,
|
||||||
|
primaryGuideUrl: 'https://example.com/guide',
|
||||||
|
installSupported: true,
|
||||||
|
installDisabled: false,
|
||||||
|
installLabel: 'Install Ubuntu in WSL',
|
||||||
|
canCancel: false,
|
||||||
|
acceptsInput: false,
|
||||||
|
inputPrompt: null,
|
||||||
|
inputSecret: false,
|
||||||
|
detailsOpen: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
function renderBanner(viewModel: TmuxInstallerBannerViewModel): {
|
||||||
|
host: HTMLDivElement;
|
||||||
|
root: ReturnType<typeof createRoot>;
|
||||||
|
} {
|
||||||
|
const host = document.createElement('div');
|
||||||
|
document.body.appendChild(host);
|
||||||
|
const root = createRoot(host);
|
||||||
|
mockUseTmuxInstallerBanner.mockReturnValue({
|
||||||
|
viewModel,
|
||||||
|
install: vi.fn(),
|
||||||
|
cancel: vi.fn(),
|
||||||
|
submitInput: vi.fn(),
|
||||||
|
refresh: vi.fn(),
|
||||||
|
toggleDetails: vi.fn(),
|
||||||
|
openExternal: vi.fn(),
|
||||||
|
});
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
root.render(React.createElement(TmuxInstallerBannerView));
|
||||||
|
});
|
||||||
|
|
||||||
|
return { host, root };
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('TmuxInstallerBannerView', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
mockUseTmuxInstallerBanner.mockReset();
|
||||||
|
document.body.innerHTML = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps Windows setup steps collapsed by default and expands them on demand', async () => {
|
||||||
|
const { host, root } = renderBanner(baseViewModel);
|
||||||
|
|
||||||
|
expect(host.textContent).toContain('Show setup steps (2)');
|
||||||
|
expect(host.textContent).not.toContain('wsl --install --no-distribution');
|
||||||
|
|
||||||
|
const toggleButton = [...host.querySelectorAll('button')].find((button) =>
|
||||||
|
button.textContent?.includes('Show setup steps')
|
||||||
|
);
|
||||||
|
expect(toggleButton).toBeDefined();
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
toggleButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(host.textContent).toContain('Hide setup steps');
|
||||||
|
expect(host.textContent).toContain('wsl --install --no-distribution');
|
||||||
|
expect(host.textContent).toContain('wsl --install -d Ubuntu --no-launch');
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
root.unmount();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows setup hints immediately on non-Windows platforms', () => {
|
||||||
|
const { host, root } = renderBanner({
|
||||||
|
...baseViewModel,
|
||||||
|
platformLabel: 'macOS',
|
||||||
|
manualHintsCollapsible: false,
|
||||||
|
manualHints: [
|
||||||
|
{ title: 'Homebrew', description: 'Recommended', command: 'brew install tmux' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(host.textContent).toContain('brew install tmux');
|
||||||
|
expect(host.textContent).not.toContain('Show setup steps');
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
root.unmount();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue