diff --git a/addons/core/translations/resources/en-us.yaml b/addons/core/translations/resources/en-us.yaml index 7ad5434d1..ff2dce9eb 100644 --- a/addons/core/translations/resources/en-us.yaml +++ b/addons/core/translations/resources/en-us.yaml @@ -104,6 +104,7 @@ host: session: title: Session title_plural: Sessions + active_plural: Active Sessions description: A session is a connection to a target initiated through Boundary. messages: none: diff --git a/ui/desktop/app/router.js b/ui/desktop/app/router.js index bcf4938e5..0af5b7e46 100644 --- a/ui/desktop/app/router.js +++ b/ui/desktop/app/router.js @@ -13,7 +13,12 @@ Router.map(function () { this.route('method', { path: ':auth_method_id' }); }); this.route('projects', function () { - this.route('targets', function () {}); + this.route('targets', function () { + this.route('target', { path: ':target_id' }, function() { + this.route('hosts'); + this.route('sessions'); + }); + }); this.route('sessions', function() {}); }); }); diff --git a/ui/desktop/app/routes/scopes/scope/projects.js b/ui/desktop/app/routes/scopes/scope/projects.js index fb77550fd..0d7ada97c 100644 --- a/ui/desktop/app/routes/scopes/scope/projects.js +++ b/ui/desktop/app/routes/scopes/scope/projects.js @@ -25,4 +25,5 @@ export default class ScopesScopeProjectsRoute extends Route { scope_id: currentScope.id, }); } + } diff --git a/ui/desktop/app/routes/scopes/scope/projects/sessions.js b/ui/desktop/app/routes/scopes/scope/projects/sessions.js index 83baf114b..1c0f133ef 100644 --- a/ui/desktop/app/routes/scopes/scope/projects/sessions.js +++ b/ui/desktop/app/routes/scopes/scope/projects/sessions.js @@ -4,6 +4,8 @@ import { all, hash } from 'rsvp'; import { task, timeout } from 'ember-concurrency'; import { A } from '@ember/array'; import config from '../../../../config/environment'; +import { action } from '@ember/object'; +import { notifySuccess, notifyError } from 'core/decorators/notify'; const POLL_TIMEOUT_SECONDS = config.sessionPollingTimeoutSeconds; @@ -99,4 +101,17 @@ export default class ScopesScopeProjectsSessionsRoute extends Route { deactivate() { this.poller.cancelAll(); } + + // =actions + + /** + * Cancels the specified session and notifies user of success or error. + * @param {SessionModel} + */ + @action + @notifyError(({ message }) => message, { catch: true }) + @notifySuccess('notifications.canceled-success') + async cancelSession(session) { + await session.cancelSession(); + } } diff --git a/ui/desktop/app/routes/scopes/scope/projects/targets/target.js b/ui/desktop/app/routes/scopes/scope/projects/targets/target.js new file mode 100644 index 000000000..45d18cdd6 --- /dev/null +++ b/ui/desktop/app/routes/scopes/scope/projects/targets/target.js @@ -0,0 +1,55 @@ +import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; +import { A } from '@ember/array'; + +export default class ScopesScopeProjectsTargetsTargetRoute extends Route { + + // =services + + @service session; + + // =methods + + /** + * Load a target and it's active sessions + * @param {object} params + * @param {string} params.target_id + * @return {{TargetModel, [SessionModel]}} + */ + async model({ target_id }) { + const userId = this.session.data.authenticated.user_id; + const target = await this.store.findRecord('target', target_id, { reload: true }); + const sessions = await this.store.query('session', { scope_id: target.scopeID }); + + // Filter to current user, current target, and active state + const filteredSessions = sessions.filter( + session => session.user_id === userId && + session.target_id === target.id && + session.isCancelable + ) + // Sort sessions by time created + const sortedSessions = + A(filteredSessions).sortBy('session.created_time').reverse(); + return { target, sessions: sortedSessions }; + } + + /** + * Renders the target-specific templates for header and navigation + * page sections. + * @override + */ + renderTemplate() { + super.renderTemplate(...arguments); + + this.render('scopes/scope/projects/targets/target/-header', { + into: 'scopes/scope/projects/targets/target', + outlet: 'header', + }); + + this.render('scopes/scope/projects/targets/target/-navigation', { + into: 'scopes/scope/projects/targets/target', + outlet: 'navigation', + }); + + } +} diff --git a/ui/desktop/app/routes/scopes/scope/projects/targets/target/hosts.js b/ui/desktop/app/routes/scopes/scope/projects/targets/target/hosts.js new file mode 100644 index 000000000..349543f4c --- /dev/null +++ b/ui/desktop/app/routes/scopes/scope/projects/targets/target/hosts.js @@ -0,0 +1,35 @@ +import Route from '@ember/routing/route'; +import { all } from 'rsvp'; + +export default class ScopesScopeProjectsTargetsTargetHostsRoute extends Route { + // =methods + + /** + * Loads all host-sets and associated hosts under the current target. + * @return {Promise{[HostModel]}} + */ + async model() { + const { target } = this.modelFor('scopes.scope.projects.targets.target'); + // Load all host-sets + const hostSets = await all( + target.host_sets.map(({ host_set_id }) => + this.store.findRecord('host-set', host_set_id) + ) + ); + + // Extract host ids from all host sets + const hostIds = hostSets.map(({host_ids}) => + host_ids.content.map((hostId) => + hostId.value + ) + ) + + // Load unique hosts + const uniqueHostIds = new Set(hostIds.flat()); + return all( + [...uniqueHostIds].map((hostId) => + this.store.findRecord('host', hostId) + ) + ); + } +} diff --git a/ui/desktop/app/routes/scopes/scope/projects/targets/target/index.js b/ui/desktop/app/routes/scopes/scope/projects/targets/target/index.js new file mode 100644 index 000000000..0f92ba860 --- /dev/null +++ b/ui/desktop/app/routes/scopes/scope/projects/targets/target/index.js @@ -0,0 +1,13 @@ +import Route from '@ember/routing/route'; + +export default class ScopesScopeProjectsTargetsTargetIndexRoute extends Route { + // =methods + + /** + * Navigate to hosts within a target. + */ + afterModel() { + this.transitionTo('scopes.scope.projects.targets.target.sessions'); + } + +} diff --git a/ui/desktop/app/routes/scopes/scope/projects/targets/target/sessions.js b/ui/desktop/app/routes/scopes/scope/projects/targets/target/sessions.js new file mode 100644 index 000000000..8b448bd47 --- /dev/null +++ b/ui/desktop/app/routes/scopes/scope/projects/targets/target/sessions.js @@ -0,0 +1,28 @@ +import Route from '@ember/routing/route'; +import { action } from '@ember/object'; +import { notifySuccess, notifyError } from 'core/decorators/notify'; + +export default class ScopesScopeProjectsTargetsTargetSessionsRoute extends Route { + // =methods + + /** + * Loads all sessions for current target. + * @return {Promise{[SessionModel]}} + */ + async model() { + return this.modelFor('scopes.scope.projects.targets.target').sessions; + } + + // =actions + + /** + * Cancels the specified session and notifies user of success or error. + * @param {SessionModel} + */ + @action + @notifyError(({ message }) => message, { catch: true }) + @notifySuccess('notifications.canceled-success') + async cancelSession(session) { + await session.cancelSession(); + } +} diff --git a/ui/desktop/app/templates/scopes/scope/projects/sessions/index.hbs b/ui/desktop/app/templates/scopes/scope/projects/sessions/index.hbs index f6d0b70e3..b6beccc53 100644 --- a/ui/desktop/app/templates/scopes/scope/projects/sessions/index.hbs +++ b/ui/desktop/app/templates/scopes/scope/projects/sessions/index.hbs @@ -16,6 +16,7 @@ {{t "form.proxy.label"}} {{t "form.started.label"}} {{t "form.status.label"}} + @@ -61,6 +62,13 @@ {{sessionAggregate.session.status}} + + {{#if sessionAggregate.session.isCancelable}} + + {{t "actions.cancel"}} + + {{/if}} + {{/each}} diff --git a/ui/desktop/app/templates/scopes/scope/projects/targets/index.hbs b/ui/desktop/app/templates/scopes/scope/projects/targets/index.hbs index 8c3c463e5..bee6e9d46 100644 --- a/ui/desktop/app/templates/scopes/scope/projects/targets/index.hbs +++ b/ui/desktop/app/templates/scopes/scope/projects/targets/index.hbs @@ -21,7 +21,12 @@ {{#each @model as |model|}} - {{model.target.name}} + + {{model.target.displayName}} +

{{model.target.description}}

diff --git a/ui/desktop/app/templates/scopes/scope/projects/targets/target.hbs b/ui/desktop/app/templates/scopes/scope/projects/targets/target.hbs new file mode 100644 index 000000000..525892043 --- /dev/null +++ b/ui/desktop/app/templates/scopes/scope/projects/targets/target.hbs @@ -0,0 +1,17 @@ +{{page-title @model.target.displayName}} + + + + + {{outlet "header"}} + + + + {{outlet "navigation"}} + + + + {{outlet}} + + + diff --git a/ui/desktop/app/templates/scopes/scope/projects/targets/target/-header.hbs b/ui/desktop/app/templates/scopes/scope/projects/targets/target/-header.hbs new file mode 100644 index 000000000..41f83d562 --- /dev/null +++ b/ui/desktop/app/templates/scopes/scope/projects/targets/target/-header.hbs @@ -0,0 +1,4 @@ +

+ {{@model.target.displayName}} +

+

{{@model.target.description}}

\ No newline at end of file diff --git a/ui/desktop/app/templates/scopes/scope/projects/targets/target/-navigation.hbs b/ui/desktop/app/templates/scopes/scope/projects/targets/target/-navigation.hbs new file mode 100644 index 000000000..12bfe97f8 --- /dev/null +++ b/ui/desktop/app/templates/scopes/scope/projects/targets/target/-navigation.hbs @@ -0,0 +1,8 @@ + + + {{t 'resources.session.active_plural'}} + + + {{t 'resources.host.title_plural'}} + + diff --git a/ui/desktop/app/templates/scopes/scope/projects/targets/target/hosts.hbs b/ui/desktop/app/templates/scopes/scope/projects/targets/target/hosts.hbs new file mode 100644 index 000000000..a925671d7 --- /dev/null +++ b/ui/desktop/app/templates/scopes/scope/projects/targets/target/hosts.hbs @@ -0,0 +1,19 @@ +{{page-title (t "resources.host-set.title_plural")}} + + + + {{#each @model as |host|}} + + + {{host.displayName}} +

{{host.description}}

+
+ {{host.id}} + {{host.attributes.address}} + + {{t (concat 'resources.host.types.' host.type)}} + +
+ {{/each}} +
+
diff --git a/ui/desktop/app/templates/scopes/scope/projects/targets/target/index.hbs b/ui/desktop/app/templates/scopes/scope/projects/targets/target/index.hbs new file mode 100644 index 000000000..c24cd6895 --- /dev/null +++ b/ui/desktop/app/templates/scopes/scope/projects/targets/target/index.hbs @@ -0,0 +1 @@ +{{outlet}} diff --git a/ui/desktop/app/templates/scopes/scope/projects/targets/target/sessions.hbs b/ui/desktop/app/templates/scopes/scope/projects/targets/target/sessions.hbs new file mode 100644 index 000000000..49a8e2585 --- /dev/null +++ b/ui/desktop/app/templates/scopes/scope/projects/targets/target/sessions.hbs @@ -0,0 +1,30 @@ + + + {{#each @model as |session|}} + + + + {{session.id}} + + + + + + {{session.status}} + + + {{#if session.isCancelable}} + + {{t "actions.cancel"}} + + {{/if}} + + + {{/each}} + + diff --git a/ui/desktop/tests/unit/routes/scopes/scope/projects/targets/target-test.js b/ui/desktop/tests/unit/routes/scopes/scope/projects/targets/target-test.js new file mode 100644 index 000000000..7794f4aa7 --- /dev/null +++ b/ui/desktop/tests/unit/routes/scopes/scope/projects/targets/target-test.js @@ -0,0 +1,11 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Route | scopes/scope/projects/targets/target', function(hooks) { + setupTest(hooks); + + test('it exists', function(assert) { + let route = this.owner.lookup('route:scopes/scope/projects/targets/target'); + assert.ok(route); + }); +}); diff --git a/ui/desktop/tests/unit/routes/scopes/scope/projects/targets/target/hosts-test.js b/ui/desktop/tests/unit/routes/scopes/scope/projects/targets/target/hosts-test.js new file mode 100644 index 000000000..e28f35db7 --- /dev/null +++ b/ui/desktop/tests/unit/routes/scopes/scope/projects/targets/target/hosts-test.js @@ -0,0 +1,11 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Route | scopes/scope/projects/targets/target/hosts', function(hooks) { + setupTest(hooks); + + test('it exists', function(assert) { + let route = this.owner.lookup('route:scopes/scope/projects/targets/target/hosts'); + assert.ok(route); + }); +}); diff --git a/ui/desktop/tests/unit/routes/scopes/scope/projects/targets/target/index-test.js b/ui/desktop/tests/unit/routes/scopes/scope/projects/targets/target/index-test.js new file mode 100644 index 000000000..d097986e4 --- /dev/null +++ b/ui/desktop/tests/unit/routes/scopes/scope/projects/targets/target/index-test.js @@ -0,0 +1,11 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Route | scopes/scope/projects/targets/target/index', function(hooks) { + setupTest(hooks); + + test('it exists', function(assert) { + let route = this.owner.lookup('route:scopes/scope/projects/targets/target/index'); + assert.ok(route); + }); +}); diff --git a/ui/desktop/tests/unit/routes/scopes/scope/projects/targets/target/sessions-test.js b/ui/desktop/tests/unit/routes/scopes/scope/projects/targets/target/sessions-test.js new file mode 100644 index 000000000..2208beec8 --- /dev/null +++ b/ui/desktop/tests/unit/routes/scopes/scope/projects/targets/target/sessions-test.js @@ -0,0 +1,11 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Route | scopes/scope/projects/targets/target/sessions', function(hooks) { + setupTest(hooks); + + test('it exists', function(assert) { + let route = this.owner.lookup('route:scopes/scope/projects/targets/target/sessions'); + assert.ok(route); + }); +});