Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: improve UX for attaching to existing session #1607

Merged
merged 18 commits into from
Aug 18, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 27 additions & 24 deletions app/common/renderer/actions/Session.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,6 @@
import {notification} from 'antd';
import axios from 'axios';
import {
debounce,
includes,
isPlainObject,
isUndefined,
keys,
toPairs,
union,
without,
} from 'lodash';
import {includes, isPlainObject, isUndefined, keys, toPairs, union, without} from 'lodash';
import moment from 'moment';
import {v4 as UUID} from 'uuid';
import {Web2Driver} from 'web2driver';
Expand Down Expand Up @@ -713,11 +704,15 @@ export function getSavedSessions() {
}

/**
* Switch to a different tab
* Switch to a different Session Builder tab
*/
export function switchTabs(key) {
return (dispatch) => {
return (dispatch, getState) => {
dispatch({type: SWITCHED_TABS, key});
// if switching to Attach to Session tab, also retrieve the running sessions
if (key === 'attach') {
eglitise marked this conversation as resolved.
Show resolved Hide resolved
getRunningSessions()(dispatch, getState);
}
};
}

Expand Down Expand Up @@ -775,24 +770,20 @@ export function setAttachSessId(attachSessId) {
* Change the server type
*/
export function changeServerType(serverType) {
return async (dispatch, getState) => {
return async (dispatch) => {
await setSetting(SESSION_SERVER_TYPE, serverType);
dispatch({type: CHANGE_SERVER_TYPE, serverType});
const action = getRunningSessions();
action(dispatch, getState);
};
}

/**
* Set a server parameter (host, port, etc...)
*/
export function setServerParam(name, value, serverType) {
const debounceGetRunningSessions = debounce(getRunningSessions(), 5000);
return async (dispatch, getState) => {
serverType = serverType || getState().session.serverType;
await setSetting(SESSION_SERVER_TYPE, serverType);
dispatch({type: SET_SERVER_PARAM, serverType, name, value});
debounceGetRunningSessions(dispatch, getState);
};
}

Expand Down Expand Up @@ -917,15 +908,15 @@ function getSaveableState(reduxState) {
};
}

/**
* Retrieve all running sessions for the currently configured server details
*/
export function getRunningSessions() {
return async (dispatch, getState) => {
const avoidServerTypes = ['sauce'];
// Get currently running sessions for this server
const state = getState().session;
const {server, serverType} = state;
const serverInfo = server[serverType];

let {hostname, port, path, ssl, username, accessKey} = serverInfo;
const {server, serverType, attachSessId} = state;
let {hostname, port, path, ssl, username, accessKey} = server[serverType];

// if we have a standard remote server, fill out connection info based on placeholder defaults
// in case the user hasn't adjusted those fields
Expand All @@ -935,8 +926,8 @@ export function getRunningSessions() {
path = path || DEFAULT_SERVER_PATH;
}

// no need to get sessions if we don't have complete server info
if (!hostname || !port || !path) {
// no need to get sessions if we don't have complete server info
return;
}

Expand All @@ -958,7 +949,19 @@ export function getRunningSessions() {
: {}),
},
});
dispatch({type: GET_SESSIONS_DONE, sessions: res.data.value});
const sessions = res.data.value;
dispatch({type: GET_SESSIONS_DONE, sessions});

// set attachSessId if only one session found
if (sessions.length === 1) {
dispatch({type: SET_ATTACH_SESS_ID, attachSessId: sessions[0].id});
} else if (attachSessId) {
// clear attachSessId if it is no longer present in the found session list
const attachSessIdFound = sessions.find((session) => session.id === attachSessId);
if (!attachSessIdFound) {
dispatch({type: SET_ATTACH_SESS_ID, attachSessId: null});
}
}
} catch (err) {
log.warn(`Ignoring error in getting list of active sessions: ${err}`);
dispatch({type: GET_SESSIONS_DONE});
Expand Down
107 changes: 70 additions & 37 deletions app/common/renderer/components/Session/AttachToSession.jsx
Original file line number Diff line number Diff line change
@@ -1,44 +1,68 @@
import {ReloadOutlined} from '@ant-design/icons';
import {Button, Card, Col, Form, Row, Select} from 'antd';
import {Button, Card, Col, Form, Row, Select, Tooltip} from 'antd';
import _ from 'lodash';
import React from 'react';

import {ServerTypes} from '../../actions/Session';
import SessionStyles from './Session.module.css';

const formatCaps = (caps) => {
let importantCaps = [caps.app, caps.platformName, caps.deviceName];
if (caps.automationName) {
importantCaps.push(caps.automationName);
}
return importantCaps.join(', ').trim();
// Format the session string for standard Appium server connections
const formatStandardCaps = (caps) => {
let returnedCaps = [];
eglitise marked this conversation as resolved.
Show resolved Hide resolved
// add deviceName OR avd OR udid
returnedCaps.push(caps.deviceName || caps.avd || caps.udid);
// add platformName and platformVersion
const platformInfo = caps.platformVersion
? `${caps.platformName} ${caps.platformVersion}`
: caps.platformName;
returnedCaps.push(platformInfo);
// add automationName
returnedCaps.push(caps.automationName);
// add app OR bundleId OR appPackage
returnedCaps.push(caps.app || caps.bundleId || caps.appPackage);
return returnedCaps;
};

const formatCapsBrowserstack = (caps) => {
let importantCaps = formatCaps(caps).split(', ');
if (caps.sessionName) {
importantCaps.push(caps.sessionName);
// Format the session string for cloud service provider connections
const formatVendorCaps = (initialCaps, serverType) => {
let returnedCaps = [];
// LambdaTest may use a slightly different format
// and package all caps in the 'capabilities' property
const caps =
serverType === ServerTypes.lambdatest && 'capabilities' in initialCaps
mykola-mokhnach marked this conversation as resolved.
Show resolved Hide resolved
? initialCaps.capabilities
: initialCaps;
// add sessionName
returnedCaps.push(caps.sessionName);
eglitise marked this conversation as resolved.
Show resolved Hide resolved
// add deviceName OR avd OR udid
const deviceName =
serverType === ServerTypes.lambdatest && 'desired' in caps
? caps.desired.deviceName
: caps.deviceName;
returnedCaps.push(deviceName || caps.avd || caps.udid);
// add platformName and platformVersion
if (caps.platformName) {
const platformInfo = caps.platformVersion
? `${caps.platformName} ${caps.platformVersion}`
: caps.platformName;
returnedCaps.push(platformInfo);
}
return importantCaps.join(', ').trim();
};

const formatCapsLambdaTest = (caps) => {
if (caps.hasOwnProperty.call(caps, 'capabilities')) {
caps = caps.capabilities;
}
const deviceName = caps.desired ? caps.desired.deviceName : caps.deviceName;
const importantCaps = [deviceName, caps.platformName, caps.platformVersion];
return importantCaps.join(', ').trim();
// add automationName
returnedCaps.push(caps.automationName);
// add app OR bundleId OR appPackage
returnedCaps.push(caps.app || caps.bundleId || caps.appPackage);
return returnedCaps;
};

const getSessionInfo = (session, serverType) => {
eglitise marked this conversation as resolved.
Show resolved Hide resolved
switch (serverType) {
case ServerTypes.browserstack:
return `${session.id} — ${formatCapsBrowserstack(session.capabilities)}`;
case ServerTypes.lambdatest:
return `${session.id} - ${formatCapsLambdaTest(session.capabilities)}`;
default:
return `${session.id} — ${formatCaps(session.capabilities)}`;
}
const formattedCaps =
serverType in [ServerTypes.remote, ServerTypes.local]
? formatStandardCaps(session.capabilities)
eglitise marked this conversation as resolved.
Show resolved Hide resolved
: formatVendorCaps(session.capabilities, serverType);
// omit all null or undefined values
const nonEmptyCaps = _.reject(formattedCaps, _.isNil);
eglitise marked this conversation as resolved.
Show resolved Hide resolved
const formattedCapsString = nonEmptyCaps.join(' / ').trim();
return `${session.id} — ${formattedCapsString}`;
};

const AttachToSession = ({
Expand Down Expand Up @@ -69,17 +93,26 @@ const AttachToSession = ({
value={attachSessId || undefined}
onChange={(value) => setAttachSessId(value)}
>
{runningAppiumSessions.map((session) => (
<Select.Option key={session.id} value={session.id}>
<div>{getSessionInfo(session, serverType)}</div>
</Select.Option>
))}
{runningAppiumSessions
.slice()
.reverse()
.map((session) => (
// list is reversed in order to place the most recent sessions at the top
// slice() is added because reverse() mutates the original array
<Select.Option key={session.id} value={session.id}>
<div>{getSessionInfo(session, serverType)}</div>
</Select.Option>
))}
</Select>
</Col>
<Col span={1}>
<div className={SessionStyles.btnReload}>
<Button onClick={getRunningSessions} icon={<ReloadOutlined />} />
</div>
<Tooltip title={t('Reload')}>
<Button
className={SessionStyles.btnReload}
onClick={getRunningSessions}
icon={<ReloadOutlined />}
/>
</Tooltip>
</Col>
</Row>
</Form.Item>
Expand Down
2 changes: 0 additions & 2 deletions app/common/renderer/components/Session/Session.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@ const Session = (props) => {
setSavedServerParams,
setStateFromAppiumFile,
setVisibleProviders,
getRunningSessions,
bindWindowClose,
initFromQueryString,
saveFile,
Expand All @@ -78,7 +77,6 @@ const Session = (props) => {
await setSavedServerParams();
await setLocalServerParams();
await setVisibleProviders();
getRunningSessions();
initFromQueryString(loadNewSession);
await setStateFromAppiumFile();
ipcRenderer.on('open-file', (_, filePath) => setStateFromAppiumFile(filePath));
Expand Down
21 changes: 1 addition & 20 deletions app/common/renderer/reducers/Session.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,19 +103,6 @@ const INITIAL_STATE = {

let nextState;

// returns false if the attachSessId is not present in the runningSessions list
const isAttachSessIdValid = (runningSessions, attachSessId) => {
if (!attachSessId || !runningSessions) {
return false;
}
for (const session of runningSessions) {
if (session.id === attachSessId) {
return true;
}
}
return false;
};

export default function session(state = INITIAL_STATE, action) {
switch (action.type) {
case NEW_SESSION_REQUESTED:
Expand Down Expand Up @@ -294,16 +281,10 @@ export default function session(state = INITIAL_STATE, action) {
};

case GET_SESSIONS_DONE: {
const attachSessId = isAttachSessIdValid(action.sessions, state.attachSessId)
? state.attachSessId
: null;
return {
...state,
gettingSessions: false,
attachSessId:
action.sessions && action.sessions.length > 0 && !attachSessId
? action.sessions[0].id
: attachSessId,
attachSessId: action.sessions ? state.attachSessId : null,
runningAppiumSessions: action.sessions || [],
};
}
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 9 additions & 7 deletions docs/session-builder/attach-to-session.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,21 @@ Appium session using the Inspector.

![Attach to Session](assets/images/attach-to-session/attach-to-session.png)

The Inspector automatically tries to discover existing sessions when the application is opened.
If no sessions are discovered, the dropdown will be empty. But if there is at least one session,
the dropdown will populate with the session identifier and other details:
The Inspector will automatically try to discover existing sessions when the Attach to Session tab is
opened. The dropdown can then be opened to list all the discovered sessions and their details:

![Found Session](assets/images/attach-to-session/found-session.png)
![Found Sessions](assets/images/attach-to-session/found-sessions.png)

There is also a refresh button to retry the session discovery process.
The most recently created sessions will be shown at the top of the list. If exactly one session is
discovered, the dropdown will also auto-populate with the details of that session.

Additionally, a refresh button is available to retry the session discovery process.

!!! note

The session discovery process uses the current [server details](./server-details.md). Make sure
to select the correct server tab and enter the expected server details before pressing the
refresh button.
to select the correct server tab and enter the expected server details before selecting the
Attach to Server tab or pressing the refresh button.

The footer of this screen contains a link the Appium documentation, and a single button for
connecting to the selected session.