Icu 10521 add route logic from sessions list to session detail screen (#1834)
* feat: 🎸 add canRead ability for session model * refactor: 💡 create constant values for session status types * feat: 🎸 add link to session detail from sessions list ✅ Closes: ICU-10521
This commit is contained in:
1 parent
0100f8b497
commit
340429fdb5
7 files changed
+254
-52
No files matched your search
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
import ModelAbility from './model';
|
||||
|
||||
/**
|
||||
* Provides abilities for sessions.
|
||||
*/
|
||||
export default class SessionAbility extends ModelAbility {
|
||||
// =services
|
||||
|
||||
// =permissions
|
||||
|
||||
/**
|
||||
* Only "active" or "pending" sessions may be read.
|
||||
* @type {boolean}
|
||||
*/
|
||||
get canRead() {
|
||||
return (this.model.isActive || this.model.isPending) && super.canRead;
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,16 @@ import { tracked } from '@glimmer/tracking';
|
||||
import { A } from '@ember/array';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export const statusTypes = ['active', 'pending', 'canceling', 'terminated'];
|
||||
export const STATUS_SESSION_ACTIVE = 'active';
|
||||
export const STATUS_SESSION_PENDING = 'pending';
|
||||
export const STATUS_SESSION_CANCELING = 'canceling';
|
||||
export const STATUS_SESSION_TERMINATED = 'terminated';
|
||||
export const statusTypes = [
|
||||
STATUS_SESSION_ACTIVE,
|
||||
STATUS_SESSION_PENDING,
|
||||
STATUS_SESSION_CANCELING,
|
||||
STATUS_SESSION_TERMINATED,
|
||||
];
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -103,8 +112,8 @@ export default class SessionModel extends GeneratedSessionModel {
|
||||
@tracked proxy_port;
|
||||
credentials = A();
|
||||
|
||||
@equal('status', 'active') isActive;
|
||||
@equal('status', 'pending') isPending;
|
||||
@equal('status', STATUS_SESSION_ACTIVE) isActive;
|
||||
@equal('status', STATUS_SESSION_PENDING) isPending;
|
||||
|
||||
/**
|
||||
* True if status is an unknown string.
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
export { default } from 'api/abilities/session';
|
||||
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
import {
|
||||
STATUS_SESSION_ACTIVE,
|
||||
STATUS_SESSION_PENDING,
|
||||
STATUS_SESSION_CANCELING,
|
||||
STATUS_SESSION_TERMINATED,
|
||||
} from 'api/models/session';
|
||||
|
||||
module('Unit | Ability | session', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
let canService;
|
||||
let store;
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
canService = this.owner.lookup('service:can');
|
||||
store = this.owner.lookup('service:store');
|
||||
});
|
||||
|
||||
test('it exists', function (assert) {
|
||||
const ability = this.owner.lookup('ability:session');
|
||||
assert.ok(ability);
|
||||
});
|
||||
|
||||
test('can read a session that is active', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const session = store.createRecord('session', {
|
||||
authorized_actions: ['read'],
|
||||
status: STATUS_SESSION_ACTIVE,
|
||||
});
|
||||
|
||||
assert.true(canService.can('read session', session));
|
||||
});
|
||||
|
||||
test('can read a session that is pending', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const session = store.createRecord('session', {
|
||||
authorized_actions: ['read'],
|
||||
status: STATUS_SESSION_PENDING,
|
||||
});
|
||||
|
||||
assert.true(canService.can('read session', session));
|
||||
});
|
||||
|
||||
test('cannot read a session that is not authorized', function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
const activeSession = store.createRecord('session', {
|
||||
authorized_actions: [],
|
||||
status: STATUS_SESSION_ACTIVE,
|
||||
});
|
||||
const pendingSession = store.createRecord('session', {
|
||||
authorized_actions: [],
|
||||
status: STATUS_SESSION_PENDING,
|
||||
});
|
||||
const cancelingSession = store.createRecord('session', {
|
||||
authorized_actions: [],
|
||||
status: STATUS_SESSION_CANCELING,
|
||||
});
|
||||
const terminatedSession = store.createRecord('session', {
|
||||
authorized_actions: [],
|
||||
status: STATUS_SESSION_TERMINATED,
|
||||
});
|
||||
|
||||
assert.false(canService.can('read session', activeSession));
|
||||
assert.false(canService.can('read session', pendingSession));
|
||||
assert.false(canService.can('read session', cancelingSession));
|
||||
assert.false(canService.can('read session', terminatedSession));
|
||||
});
|
||||
|
||||
test('cannot read a session that is canceling', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const session = store.createRecord('session', {
|
||||
authorized_actions: ['read'],
|
||||
status: STATUS_SESSION_CANCELING,
|
||||
});
|
||||
|
||||
assert.false(canService.can('read session', session));
|
||||
});
|
||||
|
||||
test('cannot read a session that is terminated', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const session = store.createRecord('session', {
|
||||
authorized_actions: ['read'],
|
||||
status: STATUS_SESSION_TERMINATED,
|
||||
});
|
||||
|
||||
assert.false(canService.can('read session', session));
|
||||
});
|
||||
});
|
||||
@@ -233,10 +233,6 @@
|
||||
color: var(--ui-gray-subtler-2);
|
||||
}
|
||||
|
||||
.session-status-active {
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.nowrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -130,31 +130,38 @@
|
||||
<:body as |B|>
|
||||
<B.Tr>
|
||||
<B.Td>
|
||||
{{#if (can 'read model' B.data)}}
|
||||
{{#if B.data.isActive}}
|
||||
<Hds::Badge
|
||||
@text={{t 'resources.session.title'}}
|
||||
@icon='entry-point'
|
||||
@isIconOnly={{true}}
|
||||
@color='success'
|
||||
{{! there are plans to remove this class. It is here to make the test
|
||||
pass. This is going to change later so Cameron suggested it's ok to
|
||||
have it for now.}}
|
||||
class='session-status-active'
|
||||
/>
|
||||
{{else}}
|
||||
<Hds::Badge
|
||||
@text={{t 'resources.session.title'}}
|
||||
@icon='entry-point'
|
||||
@isIconOnly={{true}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{#if (can 'read session' B.data)}}
|
||||
<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'}}
|
||||
>
|
||||
<code>{{B.data.id}}</code>
|
||||
<LinkTo
|
||||
@route='scopes.scope.projects.sessions.session'
|
||||
@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>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import { visit, currentURL, click, find, findAll } from '@ember/test-helpers';
|
||||
import { visit, currentURL, click } from '@ember/test-helpers';
|
||||
import { setupApplicationTest } from 'ember-qunit';
|
||||
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
|
||||
import { Response } from 'miragejs';
|
||||
@@ -15,6 +15,12 @@ import {
|
||||
invalidateSession,
|
||||
} from 'ember-simple-auth/test-support';
|
||||
import sinon from 'sinon';
|
||||
import {
|
||||
STATUS_SESSION_ACTIVE,
|
||||
STATUS_SESSION_PENDING,
|
||||
STATUS_SESSION_CANCELING,
|
||||
STATUS_SESSION_TERMINATED,
|
||||
} from 'api/models/session';
|
||||
|
||||
module('Acceptance | projects | sessions', function (hooks) {
|
||||
setupApplicationTest(hooks);
|
||||
@@ -57,6 +63,7 @@ module('Acceptance | projects | sessions', function (hooks) {
|
||||
},
|
||||
projects: null,
|
||||
sessions: null,
|
||||
session: null,
|
||||
};
|
||||
|
||||
const setDefaultClusterUrl = (test) => {
|
||||
@@ -99,7 +106,7 @@ module('Acceptance | projects | sessions', function (hooks) {
|
||||
{
|
||||
scope: instances.scopes.project,
|
||||
target: instances.target,
|
||||
status: 'active',
|
||||
status: STATUS_SESSION_ACTIVE,
|
||||
user: instances.user,
|
||||
},
|
||||
'withAssociations'
|
||||
@@ -111,6 +118,7 @@ module('Acceptance | projects | sessions', function (hooks) {
|
||||
urls.authenticate.methods.global = `${urls.authenticate.global}/${instances.authMethods.global.id}`;
|
||||
urls.projects = `${urls.scopes.org}/projects`;
|
||||
urls.sessions = `${urls.projects}/sessions`;
|
||||
urls.session = `${urls.projects}/sessions/${instances.session.id}`;
|
||||
|
||||
class MockIPC {
|
||||
clusterUrl = null;
|
||||
@@ -153,8 +161,10 @@ module('Acceptance | projects | sessions', function (hooks) {
|
||||
test('visiting index while unauthenticated redirects to global authenticate method', async function (assert) {
|
||||
invalidateSession();
|
||||
assert.expect(2);
|
||||
|
||||
await visit(urls.sessions);
|
||||
await a11yAudit();
|
||||
|
||||
assert.notOk(currentSession().isAuthenticated);
|
||||
assert.strictEqual(currentURL(), urls.authenticate.methods.global);
|
||||
});
|
||||
@@ -162,79 +172,130 @@ module('Acceptance | projects | sessions', function (hooks) {
|
||||
test('visiting index', async function (assert) {
|
||||
assert.expect(2);
|
||||
const sessionsCount = this.server.schema.sessions.all().models.length;
|
||||
|
||||
await visit(urls.sessions);
|
||||
|
||||
assert.strictEqual(currentURL(), urls.sessions);
|
||||
assert.strictEqual(findAll('tbody tr').length, sessionsCount);
|
||||
assert.dom('tbody tr').exists({ count: sessionsCount });
|
||||
});
|
||||
|
||||
test('visiting empty sessions', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.get('/sessions', () => new Response(200));
|
||||
|
||||
await visit(urls.sessions);
|
||||
await click('button.rose-button-inline-link-action'); // clear all filters
|
||||
await a11yAudit();
|
||||
assert.ok(
|
||||
find('.rose-message-title').textContent.trim(),
|
||||
'No Sessions Available'
|
||||
);
|
||||
|
||||
assert.dom('.rose-message-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.sessions);
|
||||
await a11yAudit();
|
||||
|
||||
assert.strictEqual(currentURL(), urls.sessions);
|
||||
assert.strictEqual(findAll('tbody tr').length, sessionsCount);
|
||||
assert.dom('tbody tr').exists({ count: sessionsCount });
|
||||
});
|
||||
|
||||
test('can identify active sessions', async function (assert) {
|
||||
test('visiting a session', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
await visit(urls.sessions);
|
||||
assert.ok(
|
||||
find('tbody tr:first-child td:first-child .session-status-active')
|
||||
);
|
||||
await click('[data-test-session-detail-link]');
|
||||
|
||||
assert.strictEqual(currentURL(), urls.session);
|
||||
});
|
||||
|
||||
test('cannot identify pending sessions', async function (assert) {
|
||||
test('can link to an active session', async function (assert) {
|
||||
assert.expect(1);
|
||||
instances.session.update({ status: 'pending' });
|
||||
|
||||
await visit(urls.sessions);
|
||||
assert.notOk(
|
||||
find('tbody tr:first-child th:first-child .session-status-active')
|
||||
);
|
||||
|
||||
assert
|
||||
.dom(
|
||||
'tbody tr:first-child td:first-child [data-test-session-detail-link]'
|
||||
)
|
||||
.isVisible();
|
||||
});
|
||||
|
||||
test('cannot identify terminated sessions', async function (assert) {
|
||||
test('can link to a pending session', async function (assert) {
|
||||
assert.expect(1);
|
||||
instances.session.update({ status: 'terminated' });
|
||||
instances.session.update({ status: STATUS_SESSION_PENDING });
|
||||
|
||||
await visit(urls.sessions);
|
||||
assert.notOk(
|
||||
find('tbody tr:first-child th:first-child .session-status-active')
|
||||
);
|
||||
|
||||
assert
|
||||
.dom(
|
||||
'tbody tr:first-child td:first-child [data-test-session-detail-link]'
|
||||
)
|
||||
.isVisible();
|
||||
});
|
||||
|
||||
test('cannot link to a canceling session', async function (assert) {
|
||||
assert.expect(2);
|
||||
instances.session.update({ status: STATUS_SESSION_CANCELING });
|
||||
|
||||
await visit(urls.sessions);
|
||||
|
||||
assert
|
||||
.dom(
|
||||
'tbody tr:first-child td:first-child [data-test-session-detail-link]'
|
||||
)
|
||||
.doesNotExist();
|
||||
assert
|
||||
.dom('tbody tr:first-child td:first-child [data-test-session-id-copy]')
|
||||
.isVisible();
|
||||
});
|
||||
|
||||
test('cannot link to a terminated session', async function (assert) {
|
||||
assert.expect(2);
|
||||
instances.session.update({ status: STATUS_SESSION_TERMINATED });
|
||||
|
||||
await visit(urls.sessions);
|
||||
await click('button.rose-button-inline-link-action'); // clear all filters
|
||||
|
||||
assert
|
||||
.dom(
|
||||
'tbody tr:first-child td:first-child [data-test-session-detail-link]'
|
||||
)
|
||||
.doesNotExist();
|
||||
assert
|
||||
.dom('tbody tr:first-child td:first-child [data-test-session-id-copy]')
|
||||
.isVisible();
|
||||
});
|
||||
|
||||
test('cancelling a session', async function (assert) {
|
||||
assert.expect(1);
|
||||
stubs.ipcService.withArgs('stop');
|
||||
|
||||
await visit(urls.sessions);
|
||||
await click('tbody tr:first-child td:last-child button');
|
||||
assert.ok(find('[role="alert"].is-success'));
|
||||
|
||||
assert.dom('[role="alert"].is-success').isVisible();
|
||||
});
|
||||
|
||||
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.sessions);
|
||||
await click('tbody tr:first-child td:last-child button');
|
||||
assert.ok(find('[role="alert"].is-error'));
|
||||
|
||||
assert.dom('[role="alert"].is-error').isVisible();
|
||||
});
|
||||
|
||||
test('cancelling a session with ipc error shows notification', async function (assert) {
|
||||
assert.expect(1);
|
||||
stubs.ipcService.withArgs('stop').throws();
|
||||
|
||||
await visit(urls.sessions);
|
||||
await click('tbody tr:first-child td:last-child button');
|
||||
assert.ok(find('[role="alert"].is-error'));
|
||||
|
||||
assert.dom('[role="alert"].is-error').isVisible();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user