Skip to content

Commit 38adc0f

Browse files
Hide console tab list (#6555)
Adds capability for console tab list to hide when one or no session is present. Addresses: #6556 --------- Co-authored-by: Marie Idleman <[email protected]>
1 parent adf5ab4 commit 38adc0f

File tree

8 files changed

+105
-30
lines changed

8 files changed

+105
-30
lines changed

src/vs/platform/positronActionBar/browser/components/actionBarButton.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export type ActionBarButtonProps = {
3939
readonly maxTextWidth?: number;
4040
readonly align?: 'left' | 'right';
4141
readonly tooltip?: string | (() => string | undefined);
42+
readonly dataTestId?: string;
4243
readonly dropdownTooltip?: string | (() => string | undefined);
4344
readonly checked?: boolean;
4445
readonly disabled?: boolean;
@@ -92,7 +93,7 @@ export const ActionBarButton = forwardRef<
9293
*/
9394
const ActionBarButtonFace = () => {
9495
return (
95-
<div aria-hidden='true' className='action-bar-button-face'>
96+
<div aria-hidden='true' className='action-bar-button-face' data-testid={props.dataTestId}>
9697
{props.iconId && (
9798
<div
9899
className={positronClassNames(

src/vs/workbench/contrib/positronConsole/browser/components/actionBar.tsx

+21
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ const kPaddingRight = 8;
4040
*/
4141
interface ActionBarProps {
4242
readonly reactComponentContainer: IReactComponentContainer;
43+
readonly showDeleteButton?: boolean;
4344
}
4445

4546
/**
@@ -61,6 +62,7 @@ const positronClearConsole = localize('positronClearConsole', "Clear console");
6162
const positronRestartConsole = localize('positronRestartConsole', "Restart console");
6263
const positronShutdownConsole = localize('positronShutdownConsole', "Shutdown console");
6364
const positronStartConsole = localize('positronStartConsole', "Start console");
65+
const positronDeleteConsole = localize('positronDeleteConsole', "Delete console");
6466
const positronCurrentWorkingDirectory = localize('positronCurrentWorkingDirectory', "Current Working Directory");
6567

6668
/**
@@ -327,6 +329,14 @@ export const ActionBar = (props: ActionBarProps) => {
327329
'User-requested restart from console action bar');
328330
};
329331

332+
const deleteSessionHandler = async () => {
333+
if (!positronConsoleContext.activePositronConsoleInstance) {
334+
return;
335+
}
336+
337+
await positronConsoleContext.runtimeSessionService.deleteSession(positronConsoleContext.activePositronConsoleInstance.session.sessionId);
338+
};
339+
330340
/**
331341
* CurrentWorkingDirectoryProps interface.
332342
*/
@@ -442,6 +452,17 @@ export const ActionBar = (props: ActionBarProps) => {
442452
tooltip={positronRestartConsole}
443453
onPressed={restartConsoleHandler}
444454
/>
455+
{props.showDeleteButton &&
456+
<ActionBarButton
457+
align='right'
458+
ariaLabel={positronDeleteConsole}
459+
dataTestId='trash-session'
460+
disabled={!(canShutdown || canStart)}
461+
iconId='trash'
462+
tooltip={positronDeleteConsole}
463+
onPressed={deleteSessionHandler}
464+
/>
465+
}
445466
{multiSessionsEnabled &&
446467
<>
447468
<ConsoleInstanceInfoButton />

src/vs/workbench/contrib/positronConsole/browser/components/consoleCore.tsx

+8-3
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,11 @@ export const ConsoleCore = (props: ConsoleCoreProps) => {
6767
// The maximum tab list width is 1/5 of the total available width
6868
const MAXIMUM_CONSOLE_TAB_LIST_WIDTH = Math.trunc(props.width / 5);
6969

70+
if (positronConsoleContext.consoleSessionListCollapsed) {
71+
setConsolePaneWidth(props.width);
72+
return;
73+
}
74+
7075
// Initialize the width for the console pane and console tab list if it hasn't been
7176
if (consoleWidth === 0) {
7277
setConsoleTabListWidth(MAXIMUM_CONSOLE_TAB_LIST_WIDTH)
@@ -88,7 +93,7 @@ export const ConsoleCore = (props: ConsoleCoreProps) => {
8893

8994
// Track the console width to accurately resize in future
9095
setConsoleWidth(props.width)
91-
}, [consolePaneWidth, consoleTabListWidth, consoleWidth, props.width])
96+
}, [consolePaneWidth, consoleTabListWidth, consoleWidth, props.width, positronConsoleContext.consoleSessionListCollapsed])
9297

9398
/**
9499
* onBeginResize handler.
@@ -130,7 +135,7 @@ export const ConsoleCore = (props: ConsoleCoreProps) => {
130135
<div
131136
style={{ height: props.height, width: consolePaneWidth }}
132137
>
133-
<ActionBar {...props} />
138+
<ActionBar {...props} showDeleteButton={positronConsoleContext.consoleSessionListCollapsed} />
134139
<div className='console-instances-container'>
135140
{positronConsoleContext.positronConsoleInstances.map(positronConsoleInstance =>
136141
<ConsoleInstance
@@ -149,7 +154,7 @@ export const ConsoleCore = (props: ConsoleCoreProps) => {
149154
onBeginResize={handleBeginResize}
150155
onResize={handleResize}
151156
/>
152-
<ConsoleTabList height={props.height} width={consoleTabListWidth} />
157+
{!positronConsoleContext.consoleSessionListCollapsed && <ConsoleTabList height={props.height} width={consoleTabListWidth} />}
153158
</>
154159
: <>
155160
<ActionBar {...props} />

src/vs/workbench/contrib/positronConsole/browser/positronConsoleState.tsx

+10-1
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ export interface PositronConsoleServices extends PositronActionBarServices {
5757
export interface PositronConsoleState extends PositronConsoleServices {
5858
readonly positronConsoleInstances: IPositronConsoleInstance[];
5959
readonly activePositronConsoleInstance?: IPositronConsoleInstance;
60+
61+
readonly consoleSessionListCollapsed: boolean;
6062
}
6163

6264
/**
@@ -71,6 +73,7 @@ export const usePositronConsoleState = (services: PositronConsoleServices): Posi
7173
const [activePositronConsoleInstance, setActivePositronConsoleInstance] = useState<IPositronConsoleInstance | undefined>(
7274
services.positronConsoleService.activePositronConsoleInstance
7375
);
76+
const [consoleSessionListCollapsed, setConsoleSessionListCollapsed] = useState<boolean>(positronConsoleInstances.length <= 1);
7477

7578
// Add event handlers.
7679
useEffect(() => {
@@ -104,10 +107,16 @@ export const usePositronConsoleState = (services: PositronConsoleServices): Posi
104107
return () => disposableStore.dispose();
105108
}, [services.positronConsoleService, services.runtimeSessionService, setActivePositronConsoleInstance]);
106109

110+
useEffect(() => {
111+
setConsoleSessionListCollapsed(positronConsoleInstances.length <= 1);
112+
}, [positronConsoleInstances]);
113+
114+
107115
// Return the Positron console state.
108116
return {
109117
...services,
118+
consoleSessionListCollapsed,
110119
positronConsoleInstances,
111-
activePositronConsoleInstance: activePositronConsoleInstance
120+
activePositronConsoleInstance: activePositronConsoleInstance,
112121
};
113122
};

test/e2e/pages/console.ts

+2
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export class Console {
2727
barPowerButton: Locator;
2828
barRestartButton: Locator;
2929
barClearButton: Locator;
30+
barTrashButton: Locator;
3031
consoleRestartButton: Locator;
3132
activeConsole: Locator;
3233
suggestionList: Locator;
@@ -40,6 +41,7 @@ export class Console {
4041
this.barPowerButton = this.code.driver.page.getByLabel('Shutdown console');
4142
this.barRestartButton = this.code.driver.page.getByLabel('Restart console');
4243
this.barClearButton = this.code.driver.page.getByLabel('Clear console');
44+
this.barTrashButton = this.code.driver.page.getByTestId('trash-session');
4345
this.consoleRestartButton = this.code.driver.page.locator(CONSOLE_RESTART_BUTTON);
4446
this.activeConsole = this.code.driver.page.locator(ACTIVE_CONSOLE_INSTANCE);
4547
this.suggestionList = this.code.driver.page.locator(SUGGESTION_LIST);

test/e2e/pages/sessions.ts

+40-6
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export class Sessions {
2525
private quickPick: SessionQuickPick;
2626
private trashButton: (sessionId: string) => Locator;
2727
private newConsoleButton: Locator;
28-
private restartButton: Locator;
28+
restartButton: Locator;
2929
private shutDownButton: Locator;
3030
sessionTabs: Locator;
3131
currentSessionTab: Locator;
@@ -211,13 +211,28 @@ export class Sessions {
211211
async deleteDisconnectedSessions() {
212212
await test.step('Delete all disconnected sessions', async () => {
213213
const sessionIds = await this.getAllSessionIds();
214+
const disconnectedSessions: string[] = [];
214215

216+
// Collect all disconnected session IDs
215217
for (const sessionId of sessionIds) {
216218
const status = await this.getStatus(sessionId);
217219
if (status === 'disconnected') {
218-
await this.delete(sessionId);
220+
disconnectedSessions.push(sessionId);
219221
}
220222
}
223+
224+
if (disconnectedSessions.length === 0) { return; } // Nothing to delete
225+
226+
// Delete all but the last one
227+
for (let i = 0; i < disconnectedSessions.length - 1; i++) {
228+
await this.delete(disconnectedSessions[i]);
229+
}
230+
231+
// Handle the last one separately because there is no tab list trash icon to click on
232+
const { state } = await this.getMetadata();
233+
if (state === 'disconnected' || state === 'exited') {
234+
await this.console.barTrashButton.click();
235+
}
221236
});
222237
}
223238

@@ -385,11 +400,22 @@ export class Sessions {
385400
* Note: Sessions that are disconnected are filtered out
386401
*/
387402
async getActiveSessions(): Promise<QuickPickSessionInfo[]> {
388-
const allSessions = await this.sessionTabs.all();
403+
const allSessionTabs = await this.sessionTabs.all();
404+
const metadataButtonExists = await this.metadataButton.isVisible();
405+
406+
if (allSessionTabs.length === 0) {
407+
// No active sessions
408+
if (!metadataButtonExists) { return []; }
409+
410+
// One session exists but the tab list is hidden
411+
const { path, name, state } = await this.getMetadata();
412+
return state === 'disconnected' || state === 'exited' ? [] : [{ path, name }];
413+
}
389414

415+
// Multiple sessions are present
390416
const activeSessions = (
391417
await Promise.all(
392-
allSessions.map(async session => {
418+
allSessionTabs.map(async session => {
393419
const isDisconnected = await session.locator('.codicon-positron-status-disconnected').isVisible();
394420
if (isDisconnected) { return null; }
395421

@@ -515,9 +541,17 @@ export class Sessions {
515541
const activeSessionsFromConsole = await this.getActiveSessions();
516542
expect(activeSessionsFromConsole).toHaveLength(count);
517543
} else {
518-
await expect(this.sessionTabs).toHaveCount(count);
544+
if (count === 0) {
545+
await expect(this.sessionTabs).not.toBeVisible();
546+
await expect(this.metadataButton).not.toBeVisible();
547+
} else if (count === 1) {
548+
await expect(this.sessionTabs).not.toBeVisible();
549+
await expect(this.metadataButton).toBeVisible();
550+
} else {
551+
await expect(this.sessionTabs).toHaveCount(count);
552+
}
519553
}
520-
}).toPass({ timeout: 5000 });
554+
}).toPass({ timeout: 45000 });
521555
});
522556
}
523557

test/e2e/tests/sessions/sessions.test.ts

+9-13
Original file line numberDiff line numberDiff line change
@@ -44,23 +44,18 @@ test.describe('Sessions', {
4444

4545
// Start Python session
4646
pythonSession.id = await sessions.launch({ ...pythonSession, waitForReady: false });
47+
rSession.id = await sessions.launch({ ...rSession, waitForReady: false });
4748

48-
// Verify Python session is visible and transitions from active --> idle
49-
await sessions.expectStatusToBe(pythonSession.id, 'active');
49+
// Verify R session transitions from active --> idle while Python session remains idle
50+
await sessions.expectStatusToBe(rSession.id, 'active');
51+
await sessions.expectStatusToBe(rSession.id, 'idle');
5052
await sessions.expectStatusToBe(pythonSession.id, 'idle');
5153

52-
// Restart Python session and confirm state returns to active --> idle
54+
// Restart Python session, verify Python transitions to active --> idle and R remains idle
5355
await sessions.restart(pythonSession.id, false);
5456
await sessions.expectStatusToBe(pythonSession.id, 'active');
5557
await sessions.expectStatusToBe(pythonSession.id, 'idle');
56-
57-
// Start R session
58-
rSession.id = await sessions.launch({ ...rSession, waitForReady: false });
59-
60-
// Verify R session transitions from active --> idle while Python session remains idle
61-
await sessions.expectStatusToBe(rSession.id, 'active');
6258
await sessions.expectStatusToBe(rSession.id, 'idle');
63-
await sessions.expectStatusToBe(pythonSession.id, 'idle');
6459

6560
// Shutdown Python session, verify Python transitions to disconnected while R remains idle
6661
await sessions.select(pythonSession.id);
@@ -201,14 +196,15 @@ test.describe('Sessions', {
201196
await sessions.expectSessionListsToMatch();
202197

203198
// Restart Python session and verify active sessions
204-
await sessions.restart(pythonSession.name);
205-
await sessions.expectSessionCountToBe(1, 'active');
199+
await console.barClearButton.click();
200+
await sessions.restartButton.click();
206201
await sessions.expectSessionListsToMatch();
207202
});
208203

209204
test('Validate can delete sessions', async function ({ app }) {
210205
const sessions = app.workbench.sessions;
211206
const variables = app.workbench.variables;
207+
const console = app.workbench.console;
212208

213209
// Ensure sessions exist and are idle
214210
await sessions.reuseSessionIfExists(pythonSession);
@@ -221,7 +217,7 @@ test.describe('Sessions', {
221217
await variables.expectRuntimeToBe(rSession.name);
222218

223219
// Delete 2nd session and verify no active sessions or runtime in session picker
224-
await sessions.delete(rSession.name);
220+
await console.barTrashButton.click();
225221
await expect(sessions.startSessionButton).toHaveText('Start Session');
226222
await sessions.expectSessionCountToBe(0);
227223
await sessions.expectSessionListsToMatch();

test/e2e/tests/top-action-bar/session-button.test.ts

+13-6
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6+
import { expect } from '@playwright/test';
67
import { SessionInfo } from '../../infra';
78
import { test, tags } from '../_test.setup';
89

@@ -30,14 +31,20 @@ test.describe('Top Action Bar: Session Button', {
3031
});
3132

3233
test('Python - Verify session starts and displays as running', async function ({ app }) {
33-
pythonSession.id = await app.workbench.sessions.launch({ ...pythonSession, triggerMode: 'session-picker' });
34-
await app.workbench.sessions.expectSessionPickerToBe(pythonSession);
35-
await app.workbench.sessions.expectStatusToBe(pythonSession.id, 'idle');
34+
const sessions = app.workbench.sessions;
35+
36+
pythonSession.id = await sessions.launch({ ...pythonSession, triggerMode: 'session-picker' });
37+
await sessions.expectSessionPickerToBe(pythonSession);
38+
const { state } = await sessions.getMetadata();
39+
expect(state).toBe('idle');
3640
});
3741

3842
test('R - Verify session starts and displays as running', async function ({ app }) {
39-
rSession.id = await app.workbench.sessions.launch({ ...rSession, triggerMode: 'session-picker' });
40-
await app.workbench.sessions.expectSessionPickerToBe(rSession);
41-
await app.workbench.sessions.expectStatusToBe(rSession.id, 'idle');
43+
const sessions = app.workbench.sessions;
44+
45+
rSession.id = await sessions.launch({ ...rSession, triggerMode: 'session-picker' });
46+
await sessions.expectSessionPickerToBe(rSession);
47+
const { state } = await sessions.getMetadata();
48+
expect(state).toBe('idle');
4249
});
4350
});

0 commit comments

Comments
 (0)