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:
Cameron Perera authored and Zhihe Li committed 2023-09-22 18:58:13 -04:00
1 parent 0100f8b497
commit 340429fdb5
7 files changed
+254 -52

No files matched your search

+23
View File
@@ -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;
}
}
+12 -3
View File
@@ -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.
+6
View File
@@ -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));
});
});
-4
View File
@@ -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();
});
});