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:
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}}
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user