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 @@
{{model.target.description}}
{{@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 @@ +{{host.description}}
+