feat: 🎸 filter sessions by target (#2053)

* feat: 🎸 filter sessions by target

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12066

* docs: ✏️ add missing comment

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12066

* refactor: 💡 make action reusable

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12066

* refactor: 💡 remove unnecessary block

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12066

* refactor: 💡 fix tests, add hds app state

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12066

* perf: ⚡️ set limit of targets rendered in dropdown

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12066

* fix: 🐛 change from displayName to name

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12066

* Update addons/core/addon/components/dropdown/index.hbs

Co-authored-by: Zhihe Li <zhihe.li@hashicorp.com>

* refactor: 💡 remove # from if statement and remove .slice

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12066

---------

Co-authored-by: Zhihe Li <zhihe.li@hashicorp.com>
This commit is contained in:
Lisbet AlvarezandZhihe Li authored and Zhihe Li committed 2024-01-16 10:17:46 -05:00
1 parent 47cf7995d3
commit 05553643a6
9 files changed
+285 -336

No files matched your search

@@ -3,7 +3,12 @@
SPDX-License-Identifier: MPL-2.0
}}
<Hds::Dropdown @height='314px' @onClose={{this.close}} as |DD|>
<Hds::Dropdown
@height='314px'
@onClose={{this.close}}
@listPosition={{@listPosition}}
as |DD|
>
<DD.ToggleButton
@color='secondary'
@text={{@name}}
@@ -35,7 +40,7 @@
checked={{includes itemOption.id @checkedItems}}
{{on 'change' this.selectItem}}
>
{{itemOption.displayName}}
{{if itemOption.name itemOption.name itemOption.id}}
</DD.Checkbox>
{{/each}}
{{/if}}
+3
View File
@@ -38,6 +38,7 @@ titles:
create-resources: Create Resources
choose-an-option: Choose an option
feature-edition: 'Edition: {edition}'
no-results-found: No results found
unit: Unit
year: '{numberOfYears, plural, =1 {# year} other {# years}}'
days: '{days, plural, =1 {# day} other {# days}}'
@@ -50,6 +51,8 @@ descriptions:
no-results: We found no matching records.
choose-a-provider: There are several built-in plugins for the most common providers. You can choose from one of the included plugins or use a custom plugin.
provider: This is the provider for this host catalog. The format of the host set filters are specific to each provider.
no-search-results: Sorry, we weren't able to find any {resource} for "{query}".
no-filter-results: Sorry, we weren’t able to find any {resource} within the filtered parameters.
questions:
delete-confirm: Are you sure you want to delete this resource?
remove-confirm: Are you sure you want to remove this association?
@@ -564,11 +564,6 @@ target:
connect-more-info:
title: Connect for more info
description: Additional information for this target will be provided here after your session is established.
no-search-results:
title: No results found
description: Sorry, we weren't able to find any targets for "{query}".
no-filter-results:
description: Sorry, we weren’t able to find any targets within the filtered parameters.
titles:
new: New Target
active-sessions: Active sessions
@@ -4,25 +4,58 @@
*/
import Controller from '@ember/controller';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import orderBy from 'lodash/orderBy';
import { statusTypes } from 'api/models/session';
export default class ScopesScopeProjectsSessionsIndexController extends Controller {
// =services
// =attributes
queryParams = [{ targets: { type: 'array' } }];
@tracked targets = [];
// =methods
/**
* A list of sessions filtered to the current user and sorted by created time,
* and active/pending.
* @type {SessionModel[]}
* A list of sessions sorted by created time
* @type {[SessionModel]}
*/
get sorted() {
const sessions = this.model;
// Sort sessions by created time descending (newest on top)
const sortedSessions = orderBy(sessions, 'created_time', 'desc');
return [
// then move active sessions to the top
...sortedSessions.filter((session) => session.isAvailable),
// and all others to the end
...sortedSessions.filter((session) => !session.isAvailable),
];
get sortedSessions() {
return orderBy(this.model.sessions, 'created_time', 'desc');
}
/**
* Returns targets that are associated will all sessions the user has access to
* @returns {[TargetModel]}
*/
get availableTargets() {
const uniqueSessionTargetIds = new Set(
this.model.allSessions.map((session) => session.target_id),
);
return this.model.allTargets.filter((target) =>
uniqueSessionTargetIds.has(target.id),
);
}
/**
* Returns all status types for sessions
* @returns {[string]}
*/
get sessionStatusTypes() {
return statusTypes;
}
/**
* Sets the targets query param to value of selectedTargets
* to trigger a query and closes the dropdown
* @param {object} selectedTargets
*/
@action
applyFilter(filter, selectedTargets) {
this[filter] = [...selectedTargets];
}
}
@@ -86,8 +86,8 @@ export default class ScopesScopeProjectsTargetsIndexController extends Controlle
get availableSessionOptions() {
return [
{ id: 'yes', displayName: this.intl.t('actions.yes') },
{ id: 'no', displayName: this.intl.t('actions.no') },
{ id: 'yes', name: this.intl.t('actions.yes') },
{ id: 'no', name: this.intl.t('actions.no') },
];
}
@@ -4,30 +4,25 @@
*/
import Route from '@ember/routing/route';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';
import { resourceFilter } from 'core/decorators/resource-filter';
export default class ScopesScopeProjectsSessionsIndexRoute extends Route {
// =services
@service session;
@service resourceFilterStore;
@service store;
// =attributes
@resourceFilter({
allowed: ['active', 'pending', 'canceling', 'terminated'],
defaultValue: ['active', 'pending', 'canceling'],
})
status;
queryParams = {
targets: {
refreshModel: true,
replace: true,
},
};
@resourceFilter({
allowed: (route) => route.modelFor('scopes.scope.projects'),
serialize: ({ id }) => id,
findBySerialized: ({ id }, value) => id === value,
})
project;
allSessions;
allTargets;
// =methods
@@ -39,53 +34,43 @@ export default class ScopesScopeProjectsSessionsIndexRoute extends Route {
* are now filtered on the client by projects and status,
* while user_id filtering remains server side.
*
* @return {Promise{[SessionModel]}}
* @return {Promise<{sessions: [SessionModel], allSessions: [SessionModel]}>}
*/
async model() {
const { id: scope_id } = this.modelFor('scopes.scope');
const queryOptions = {
scope_id,
recursive: true,
include_terminated: this.status?.includes('terminated'),
async model({ targets }, transition) {
const from = transition.from?.name;
const filters = {
user_id: [{ equals: this.session.data.authenticated.user_id }],
target_id: [],
};
targets.forEach((target) => {
filters.target_id.push({ equals: target });
});
// Recursively query sessions within the current scope for the current user
let sessions = await this.resourceFilterStore.queryBy(
'session',
{ user_id: this.session.data.authenticated.user_id },
queryOptions,
);
const queryOptions = { query: { filters }, force_refresh: true };
const sessions = await this.store.query('session', queryOptions);
// If project filters are selected, filter sessions by the selected projects
if (this.project?.length) {
const projectIDs = this.project.map((p) => p?.id);
sessions = sessions.filter((s) => projectIDs.includes(s?.scopeID));
// Query all sessions and all targets for defining filtering values if entering route for the first time
if (from !== 'scopes.scope.projects.sessions.index') {
const options = { pushToStore: false };
this.allSessions = await this.store.query(
'session',
{
query: {
filters: {
user_id: [{ equals: this.session.data.authenticated.user_id }],
},
},
},
options,
);
this.allTargets = await this.store.query('target', {}, options);
}
// If status filters are selected, filter sessions by the selected statuses
if (this.status?.length) {
sessions = sessions.filter((s) => this.status.includes(s?.status));
}
return sessions;
}
/**
* Sets the specified resource filter field to the specified value.
* @param {string} field
* @param value
*/
@action
filterBy(field, value) {
this[field] = value;
}
/**
* Clears and filter selections.
*/
@action
clearAllFilters() {
this.status = [];
this.project = [];
return {
sessions,
allSessions: this.allSessions,
allTargets: this.allTargets,
};
}
}
@@ -13,230 +13,152 @@
<p>{{t 'resources.session.description'}}</p>
</page.header>
<page.body>
<Rose::Toolbar as |toolbar|>
<toolbar.primary>
<Rose::Form as |form|>
{{#let
(resource-filter 'scopes.scope.projects.sessions.index' 'status')
as |filter|
<page.body class='search-filtering'>
{{#if (or @model.sessions @model.allSessions)}}
<Hds::SegmentedGroup as |S|>
<S.Generic>
<Dropdown
@name={{t 'resources.target.title'}}
@itemOptions={{this.availableTargets}}
@checkedItems={{this.targets}}
@applyFilter={{fn this.applyFilter 'targets'}}
@isSearchable={{true}}
@listPosition='bottom-left'
/>
</S.Generic>
<S.Dropdown as |DD|>
<DD.ToggleButton @color='secondary' @text={{t 'form.status.label'}} />
{{#each this.sessionStatusTypes as |status|}}
<DD.Checkbox>{{t
(concat 'resources.session.status.' status)
}}</DD.Checkbox>
{{/each}}
</S.Dropdown>
<S.Dropdown as |DD|>
<DD.ToggleButton
@color='secondary'
@text={{t 'resources.scope.title'}}
/>
</S.Dropdown>
</Hds::SegmentedGroup>
{{#if @model.sessions}}
<Hds::Table
@model={{this.sortedSessions}}
@columns={{array
(hash label=(t 'form.id.label'))
(hash label=(t 'resources.target.title'))
(hash label=(t 'form.proxy_url.label'))
(hash label=(t 'form.started.label'))
(hash label=(t 'form.status.label'))
(hash label='')
}}
<Rose::Dropdown
@text={{t (concat 'form.' filter.name '.label')}}
@count={{filter.selectedValue.length}}
@ignoreClickInside={{true}}
as |dropdown|
>
<form.checkboxGroup
@name={{filter.name}}
@items={{filter.allowedValues}}
@selectedItems={{filter.selectedValue}}
@onChange={{route-action 'filterBy' filter.name}}
as |group|
>
<dropdown.item>
<group.checkbox
@label={{t (concat 'resources.session.status.' group.item)}}
value={{group.item}}
@valign='middle'
>
<:body as |B|>
<B.Tr>
<B.Td>
{{#if B.data.isAvailable}}
<Hds::Badge
@text={{t 'resources.session.title'}}
@icon='entry-point'
@isIconOnly={{true}}
@color='success'
/>
</dropdown.item>
</form.checkboxGroup>
</Rose::Dropdown>
{{/let}}
{{#let
(resource-filter 'scopes.scope.projects.sessions.index' 'project')
as |filter|
}}
<Rose::Dropdown
@text={{t (concat 'resources.' filter.name '.title')}}
@count={{filter.selectedValue.length}}
@ignoreClickInside={{true}}
as |dropdown|
>
<ul>
{{#let
(group-by filter.allowedValues 'scopeModel')
as |groups|
}}
{{#each groups as |groupedItems|}}
<li>
<dropdown.item>
<Rose::Icon
@name='flight-icons/svg/org-16'
@size='medium'
/>
{{groupedItems.key.displayName}}
</dropdown.item>
<ul class='indent-label-2'>
<form.checkboxGroup
@name={{filter.name}}
@items={{groupedItems.items}}
@selectedItems={{filter.selectedValue}}
@onChange={{route-action 'filterBy' filter.name}}
as |group|
>
<li>
<dropdown.item>
<group.checkbox
@label={{group.item.displayName}}
value={{group.item}}
/>
</dropdown.item>
</li>
</form.checkboxGroup>
</ul>
</li>
{{/each}}
{{/let}}
</ul>
</Rose::Dropdown>
{{/let}}
{{#if
(has-resource-filter-selections
'scopes.scope.projects.sessions.index' 'status' 'project'
)
}}
<Rose::Button
@style='inline-link-action'
@iconLeft='flight-icons/svg/x-16'
{{on 'click' (route-action 'clearAllFilters')}}
>{{t 'actions.clear-all-filters'}}
</Rose::Button>
{{/if}}
</Rose::Form>
</toolbar.primary>
<toolbar.secondary>
<ToolbarRefresher @onClick={{route-action 'refreshSessions'}} />
</toolbar.secondary>
</Rose::Toolbar>
{{#if this.sorted}}
<Hds::Table
@model={{this.sorted}}
@columns={{array
(hash label=(t 'form.id.label'))
(hash label=(t 'resources.target.title'))
(hash label=(t 'form.proxy_url.label'))
(hash label=(t 'form.started.label'))
(hash label=(t 'form.status.label'))
(hash label='')
}}
@valign='middle'
>
<:body as |B|>
<B.Tr>
<B.Td>
{{#if B.data.isAvailable}}
<Hds::Badge
@text={{t 'resources.session.title'}}
@icon='entry-point'
@isIconOnly={{true}}
@color='success'
/>
<Copyable
@text={{B.data.id}}
@buttonText={{t 'actions.copy-to-clipboard'}}
@acknowledgeText={{t 'states.copied'}}
>
<LinkTo
@route='scopes.scope.projects.sessions.session.index'
@model={{B.data.id}}
data-test-session-detail-link
>
<code>{{B.data.id}}</code>
</LinkTo>
</Copyable>
{{else}}
<Hds::Badge
@text={{t 'resources.session.title'}}
@icon='entry-point'
@isIconOnly={{true}}
/>
<Copyable
@text={{B.data.id}}
@buttonText={{t 'actions.copy-to-clipboard'}}
@acknowledgeText={{t 'states.copied'}}
>
<code data-test-session-id-copy>{{B.data.id}}</code>
</Copyable>
{{/if}}
</B.Td>
<B.Td>
{{B.data.target.displayName}}
</B.Td>
{{!
Two if's here because:
1) Local proxy info is only relevant if the session is
active or pending (aka isAvailable). Once the session
is canceled or in the process thereof, the local proxy
may no longer be used.
2) The session actually has proxy information associated
with it. It's possible for the user to start sessions
from other devices (or even another tab), in which case we
won't have the local proxy info available here.
}}
<B.Td>
{{#if B.data.isAvailable}}
{{#if B.data.proxy}}
<Copyable
@text={{B.data.proxy}}
@text={{B.data.id}}
@buttonText={{t 'actions.copy-to-clipboard'}}
@acknowledgeText={{t 'states.copied'}}
>
<code>{{B.data.proxy}}</code>
<LinkTo
@route='scopes.scope.projects.sessions.session.index'
@model={{B.data.id}}
data-test-session-detail-link
>
<Hds::Text::Code>{{B.data.id}}</Hds::Text::Code>
</LinkTo>
</Copyable>
{{else}}
<Hds::Badge
@text={{t 'resources.session.title'}}
@icon='entry-point'
@isIconOnly={{true}}
/>
<Copyable
@text={{B.data.id}}
@buttonText={{t 'actions.copy-to-clipboard'}}
@acknowledgeText={{t 'states.copied'}}
>
<Hds::Text::Code
data-test-session-id-copy
>{{B.data.id}}</Hds::Text::Code>
</Copyable>
{{/if}}
{{/if}}
</B.Td>
<B.Td>
<time datetime={{format-date-iso B.data.created_time}}>
{{format-date-iso-human B.data.created_time}}
</time>
</B.Td>
<B.Td>
<SessionStatus @model={{B.data}} />
</B.Td>
<B.Td>
{{#if (can 'cancel session' B.data)}}
<Hds::Button
@text={{t 'actions.cancel'}}
@color='secondary'
{{on 'click' (route-action 'cancelSession' B.data)}}
data-test-session-cancel-button
/>
{{/if}}
</B.Td>
</B.Tr>
</:body>
</Hds::Table>
{{else if
(has-resource-filter-selections
'scopes.scope.projects.sessions.index' 'status' 'project'
)
}}
<Rose::Layout::Centered>
<Rose::Message @title={{t 'titles.no-results'}} as |message|>
<message.description>
{{t 'descriptions.no-results'}}
</message.description>
</Rose::Message>
</Rose::Layout::Centered>
</B.Td>
<B.Td>
{{B.data.target.displayName}}
</B.Td>
{{!
Two if's here because:
1) Local proxy info is only relevant if the session is
active or pending (aka isAvailable). Once the session
is canceled or in the process thereof, the local proxy
may no longer be used.
2) The session actually has proxy information associated
with it. It's possible for the user to start sessions
from other devices (or even another tab), in which case we
won't have the local proxy info available here.
}}
<B.Td>
{{#if B.data.isAvailable}}
{{#if B.data.proxy}}
<Copyable
@text={{B.data.proxy}}
@buttonText={{t 'actions.copy-to-clipboard'}}
@acknowledgeText={{t 'states.copied'}}
>
<Hds::Text::Code>{{B.data.proxy}}</Hds::Text::Code>
</Copyable>
{{/if}}
{{/if}}
</B.Td>
<B.Td>
<time datetime={{format-date-iso B.data.created_time}}>
{{format-date-iso-human B.data.created_time}}
</time>
</B.Td>
<B.Td>
<SessionStatus @model={{B.data}} />
</B.Td>
<B.Td>
{{#if (can 'cancel session' B.data)}}
<Hds::Button
@text={{t 'actions.cancel'}}
@color='secondary'
{{on 'click' (route-action 'cancelSession' B.data)}}
data-test-session-cancel-button
/>
{{/if}}
</B.Td>
</B.Tr>
</:body>
</Hds::Table>
{{else}}
<Hds::ApplicationState data-test-no-session-results as |A|>
<A.Header @title={{t 'titles.no-results-found'}} />
<A.Body
@text={{t 'descriptions.no-filter-results' resource='sessions'}}
/>
</Hds::ApplicationState>
{{/if}}
{{else}}
<Rose::Layout::Centered>
<Rose::Message
<Hds::ApplicationState data-test-no-sessions as |A|>
<A.Header
@title={{t 'resources.session.messages.none-friendly.title'}}
as |message|
>
<message.description>
{{t 'resources.session.messages.none-friendly.description'}}
</message.description>
</Rose::Message>
</Rose::Layout::Centered>
/>
<A.Body
@text={{t 'resources.session.messages.none-friendly.description'}}
/>
</Hds::ApplicationState>
{{/if}}
</page.body>
@@ -170,17 +170,16 @@
{{/if}}
{{#if this.noResults}}
<Hds::ApplicationState data-test-no-target-results as |A|>
<A.Header
@title={{t 'resources.target.messages.no-search-results.title'}}
/>
<A.Header @title={{t 'titles.no-results-found'}} />
<A.Body
@text={{t
(if
this.search
'resources.target.messages.no-search-results.description'
'resources.target.messages.no-filter-results.description'
'descriptions.no-search-results'
'descriptions.no-filter-results'
)
query=this.search
resource='targets'
}}
/>
</Hds::ApplicationState>
@@ -28,6 +28,9 @@ module('Acceptance | projects | sessions | index', function (hooks) {
setupMirage(hooks);
setupStubs(hooks);
const APP_STATE_TITLE =
'[data-test-no-sessions] .hds-application-state__title';
const instances = {
scopes: {
global: null,
@@ -114,12 +117,13 @@ module('Acceptance | projects | sessions | index', function (hooks) {
this.owner.register('service:browser/window', WindowMockIPC);
setDefaultClusterUrl(this);
this.ipcStub.withArgs('isClientDaemonRunning').returns(false);
this.ipcStub.withArgs('isClientDaemonRunning').returns(true);
this.stubClientDaemonSearch('sessions', 'sessions', 'targets');
});
test('visiting index while unauthenticated redirects to global authenticate method', async function (assert) {
invalidateSession();
assert.expect(2);
this.stubClientDaemonSearch();
await visit(urls.sessions);
await a11yAudit();
@@ -129,32 +133,32 @@ module('Acceptance | projects | sessions | index', function (hooks) {
});
test('visiting index', async function (assert) {
assert.expect(2);
const sessionsCount = this.server.schema.sessions.all().models.length;
await visit(urls.projects);
await visit(urls.sessions);
await click(`[href="${urls.sessions}"]`);
assert.strictEqual(currentURL(), urls.sessions);
assert.dom('tbody tr').exists({ count: sessionsCount });
});
test('visiting empty sessions', async function (assert) {
assert.expect(1);
this.server.get('/sessions', () => new Response(200));
this.server.db.sessions.remove();
this.stubClientDaemonSearch('sessions', 'sessions', 'targets');
await visit(urls.projects);
await visit(urls.sessions);
await click('button.rose-button-inline-link-action'); // clear all filters
await click(`[href="${urls.sessions}"]`);
await a11yAudit();
assert.dom('.rose-message-title').hasText('No Sessions Available');
assert.dom(APP_STATE_TITLE).hasText('No Sessions Available');
});
test('visiting sessions without targets is OK', async function (assert) {
assert.expect(2);
instances.session.update({ targetId: undefined });
const sessionsCount = this.server.schema.sessions.all().models.length;
await visit(urls.projects);
await visit(urls.sessions);
await click(`[href="${urls.sessions}"]`);
await a11yAudit();
assert.strictEqual(currentURL(), urls.sessions);
@@ -162,17 +166,18 @@ module('Acceptance | projects | sessions | index', function (hooks) {
});
test('visiting a session', async function (assert) {
assert.expect(1);
await visit(urls.projects);
await visit(urls.sessions);
await click(`[href="${urls.sessions}"]`);
await click('[data-test-session-detail-link]');
assert.strictEqual(currentURL(), urls.session);
});
test('can link to an active session', async function (assert) {
assert.expect(1);
await visit(urls.projects);
await click(`[href="${urls.sessions}"]`);
await visit(urls.sessions);
assert
@@ -183,10 +188,11 @@ module('Acceptance | projects | sessions | index', function (hooks) {
});
test('can link to an active session with read:self permissions', async function (assert) {
assert.expect(1);
instances.session.update({ authorized_actions: ['read:self'] });
this.stubClientDaemonSearch('sessions', 'sessions', 'targets');
await visit(urls.projects);
await visit(urls.sessions);
await click(`[href="${urls.sessions}"]`);
assert
.dom(
@@ -196,10 +202,11 @@ module('Acceptance | projects | sessions | index', function (hooks) {
});
test('can link to a pending session', async function (assert) {
assert.expect(1);
instances.session.update({ status: STATUS_SESSION_PENDING });
this.stubClientDaemonSearch('sessions', 'sessions', 'targets');
await visit(urls.projects);
await visit(urls.sessions);
await click(`[href="${urls.sessions}"]`);
assert
.dom(
@@ -209,10 +216,11 @@ module('Acceptance | projects | sessions | index', function (hooks) {
});
test('can link to session even without read permissions', async function (assert) {
assert.expect(1);
instances.session.update({ authorized_actions: [] });
this.stubClientDaemonSearch('sessions', 'sessions', 'targets');
await visit(urls.projects);
await visit(urls.sessions);
await click(`[href="${urls.sessions}"]`);
assert
.dom(
@@ -222,9 +230,8 @@ module('Acceptance | projects | sessions | index', function (hooks) {
});
test('cannot link to a canceling session', async function (assert) {
assert.expect(2);
instances.session.update({ status: STATUS_SESSION_CANCELING });
this.stubClientDaemonSearch('sessions', 'sessions', 'targets');
await visit(urls.sessions);
assert
@@ -238,11 +245,9 @@ module('Acceptance | projects | sessions | index', function (hooks) {
});
test('cannot link to a terminated session', async function (assert) {
assert.expect(2);
instances.session.update({ status: STATUS_SESSION_TERMINATED });
this.stubClientDaemonSearch('sessions', 'sessions', 'targets');
await visit(urls.sessions);
await click('button.rose-button-inline-link-action'); // clear all filters
assert
.dom(
@@ -255,9 +260,9 @@ module('Acceptance | projects | sessions | index', function (hooks) {
});
test('can cancel an active session with cancel permissions', async function (assert) {
assert.expect(1);
await visit(urls.projects);
await visit(urls.sessions);
await click(`[href="${urls.sessions}"]`);
assert
.dom('tbody tr:first-child [data-test-session-cancel-button]')
@@ -265,10 +270,11 @@ module('Acceptance | projects | sessions | index', function (hooks) {
});
test('can cancel an active session with cancel:self permissions', async function (assert) {
assert.expect(1);
instances.session.update({ authorized_actions: ['cancel:self'] });
this.stubClientDaemonSearch('sessions', 'sessions', 'targets');
await visit(urls.projects);
await visit(urls.sessions);
await click(`[href="${urls.sessions}"]`);
assert
.dom('tbody tr:first-child [data-test-session-cancel-button]')
@@ -276,10 +282,11 @@ module('Acceptance | projects | sessions | index', function (hooks) {
});
test('cannot click cancel button without cancel permissions', async function (assert) {
assert.expect(1);
instances.session.update({ authorized_actions: [] });
this.stubClientDaemonSearch('sessions', 'sessions', 'targets');
await visit(urls.projects);
await visit(urls.sessions);
await click(`[href="${urls.sessions}"]`);
assert
.dom('tbody tr:first-child [data-test-session-cancel-button]')
@@ -287,40 +294,40 @@ module('Acceptance | projects | sessions | index', function (hooks) {
});
test('cancelling a session shows success alert', async function (assert) {
assert.expect(1);
this.ipcStub.withArgs('stop');
await visit(urls.projects);
await visit(urls.sessions);
await click(`[href="${urls.sessions}"]`);
await click('tbody tr:first-child td:last-child button');
assert.dom('[role="alert"].is-success').isVisible();
});
test('cancelling a session keeps you on the sessions list screen', async function (assert) {
assert.expect(1);
this.ipcStub.withArgs('stop');
await visit(urls.projects);
await visit(urls.sessions);
await click(`[href="${urls.sessions}"]`);
await click('tbody tr:first-child td:last-child button');
assert.strictEqual(currentURL(), urls.sessions);
});
test('cancelling a session with error shows notification', async function (assert) {
assert.expect(1);
this.server.post('/sessions/:id_method', () => new Response(400));
await visit(urls.projects);
await visit(urls.sessions);
await click(`[href="${urls.sessions}"]`);
await click('tbody tr:first-child td:last-child button');
assert.dom('[role="alert"].is-error').isVisible();
});
test('cancelling a session with ipc error shows notification', async function (assert) {
assert.expect(1);
this.ipcStub.withArgs('stop').throws();
await visit(urls.projects);
await visit(urls.sessions);
await click(`[href="${urls.sessions}"]`);
await click('tbody tr:first-child td:last-child button');
assert.dom('[role="alert"].is-error').isVisible();