diff --git a/addon/controllers/groups/index.js b/addon/controllers/groups/index.js
index a79b287..afae690 100644
--- a/addon/controllers/groups/index.js
+++ b/addon/controllers/groups/index.js
@@ -23,7 +23,7 @@ export default class GroupsIndexController extends Controller {
*
* @var {Array}
*/
- queryParams = ['view_group', 'page', 'limit', 'sort', 'query', 'type', 'created_by', 'updated_by', 'status'];
+ queryParams = this.groupActions.queryParamsFor(['view_group', 'page', 'limit', 'sort', 'query', 'type', 'created_by', 'updated_by', 'status']);
/**
* The current page of data being viewed
@@ -54,6 +54,50 @@ export default class GroupsIndexController extends Controller {
*/
@tracked sort = '-created_at';
+ /**
+ * Header buttons. Extensions add to them through `iam:table:group:actions`.
+ *
+ * @var {Array}
+ */
+ get actionButtons() {
+ return [
+ {
+ id: 'create',
+ text: this.intl.t('iam.common.new'),
+ type: 'primary',
+ icon: 'plus',
+ iconPrefix: 'fas',
+ onClick: this.createGroup,
+ permission: 'iam create group',
+ },
+ {
+ id: 'export',
+ text: this.intl.t('iam.common.export'),
+ icon: 'long-arrow-up',
+ iconClass: 'rotate-icon-45',
+ onClick: this.exportGroups,
+ permission: 'iam export group',
+ },
+ ];
+ }
+
+ /**
+ * Bulk actions for the selected rows. Extensions add to them through `iam:table:group:bulk-actions`.
+ *
+ * @var {Array}
+ */
+ get bulkActions() {
+ return [
+ {
+ id: 'bulk-delete',
+ label: this.intl.t('iam.groups.index.delete-groups'),
+ class: 'text-red-500',
+ fn: this.bulkDeleteGroups,
+ permission: 'iam delete group',
+ },
+ ];
+ }
+
/**
* All columns applicable for groups
*
@@ -61,6 +105,7 @@ export default class GroupsIndexController extends Controller {
*/
@tracked columns = [
{
+ id: 'name',
label: this.intl.t('iam.common.name'),
valuePath: 'name',
cellComponent: 'table/cell/anchor',
@@ -70,12 +115,14 @@ export default class GroupsIndexController extends Controller {
sortable: false,
},
{
+ id: 'description',
label: this.intl.t('iam.common.description'),
valuePath: 'description',
sortable: false,
width: '25%',
},
{
+ id: 'users',
label: this.intl.t('iam.common.member'),
valuePath: 'users',
cellComponent: 'table/cell/group-members',
@@ -86,6 +133,7 @@ export default class GroupsIndexController extends Controller {
width: '35%',
},
{
+ id: 'created-at',
label: this.intl.t('iam.groups.index.created'),
valuePath: 'createdAt',
sortable: false,
@@ -94,6 +142,7 @@ export default class GroupsIndexController extends Controller {
cellClassNames: 'overflow-visible',
},
{
+ id: 'row-actions',
label: '',
cellComponent: 'table/cell/dropdown',
ddButtonText: false,
@@ -105,11 +154,13 @@ export default class GroupsIndexController extends Controller {
width: '10%',
actions: [
{
+ id: 'edit-group',
label: this.intl.t('iam.groups.index.edit-group'),
fn: this.editGroup,
permission: 'iam view group',
},
{
+ id: 'delete-group',
label: this.intl.t('iam.groups.index.delete-group-label'),
fn: this.deleteGroup,
className: 'text-red-700 hover:text-red-800',
@@ -154,7 +205,7 @@ export default class GroupsIndexController extends Controller {
this.crud.bulkDelete(selected, {
modelNamePath: `name`,
- acceptButtonText: this.intl.t('aim.groups.index.delete-group'),
+ acceptButtonText: this.intl.t('iam.groups.index.delete-group'),
onSuccess: () => {
return this.hostRouter.refresh();
},
diff --git a/addon/controllers/policies/index.js b/addon/controllers/policies/index.js
index 4c79c90..cb55e4e 100644
--- a/addon/controllers/policies/index.js
+++ b/addon/controllers/policies/index.js
@@ -24,7 +24,7 @@ export default class PoliciesIndexController extends Controller {
*
* @var {Array}
*/
- queryParams = ['view_policy', 'page', 'limit', 'sort', 'query', 'type', 'created_by', 'updated_by', 'status', 'service', 'type'];
+ queryParams = this.policyActions.queryParamsFor(['view_policy', 'page', 'limit', 'sort', 'query', 'type', 'created_by', 'updated_by', 'status', 'service', 'type']);
/**
* The current page of data being viewed
@@ -69,6 +69,42 @@ export default class PoliciesIndexController extends Controller {
*/
@tracked types = this.iam.schemeTypes;
+ /**
+ * Header buttons. Extensions add to them through `iam:table:policy:actions`.
+ *
+ * @var {Array}
+ */
+ get actionButtons() {
+ return [
+ {
+ id: 'create',
+ text: this.intl.t('iam.common.new'),
+ type: 'primary',
+ icon: 'plus',
+ iconPrefix: 'fas',
+ onClick: this.createPolicy,
+ permission: 'iam create policy',
+ },
+ ];
+ }
+
+ /**
+ * Bulk actions for the selected rows. Extensions add to them through `iam:table:policy:bulk-actions`.
+ *
+ * @var {Array}
+ */
+ get bulkActions() {
+ return [
+ {
+ id: 'bulk-delete',
+ label: this.intl.t('iam.policies.index.delete-policies'),
+ class: 'text-red-500',
+ fn: this.bulkDeletePolicies,
+ permission: 'iam delete policy',
+ },
+ ];
+ }
+
/**
* All columns applicable for roles
*
@@ -76,6 +112,7 @@ export default class PoliciesIndexController extends Controller {
*/
@tracked columns = [
{
+ id: 'name',
label: this.intl.t('iam.common.name'),
valuePath: 'name',
cellComponent: 'table/cell/anchor',
@@ -85,12 +122,14 @@ export default class PoliciesIndexController extends Controller {
sortable: false,
},
{
+ id: 'description',
label: this.intl.t('iam.common.description'),
valuePath: 'description',
sortable: false,
width: '35%',
},
{
+ id: 'service',
label: this.intl.t('iam.common.service'),
valuePath: 'service',
sortable: false,
@@ -100,6 +139,7 @@ export default class PoliciesIndexController extends Controller {
filterOptions: this.services,
},
{
+ id: 'type',
label: this.intl.t('iam.common.type'),
valuePath: 'type',
sortable: false,
@@ -111,6 +151,7 @@ export default class PoliciesIndexController extends Controller {
filterOptions: this.types,
},
{
+ id: 'created-at',
label: this.intl.t('iam.common.create'),
valuePath: 'createdAt',
sortable: false,
@@ -119,6 +160,7 @@ export default class PoliciesIndexController extends Controller {
cellClassNames: 'overflow-visible',
},
{
+ id: 'row-actions',
label: '',
cellComponent: 'table/cell/dropdown',
ddButtonText: false,
@@ -130,11 +172,13 @@ export default class PoliciesIndexController extends Controller {
width: '15%',
actions: [
{
+ id: 'edit-policy',
label: this.intl.t('iam.policies.index.edit-policy'),
fn: this.editPolicy,
permission: 'iam view policy',
},
{
+ id: 'delete-policy',
label: this.intl.t('iam.policies.index.delete-policy'),
fn: this.deletePolicy,
className: 'text-red-700 hover:text-red-800',
diff --git a/addon/controllers/roles/index.js b/addon/controllers/roles/index.js
index 9c8980a..b5191ef 100644
--- a/addon/controllers/roles/index.js
+++ b/addon/controllers/roles/index.js
@@ -24,7 +24,7 @@ export default class RolesIndexController extends Controller {
*
* @var {Array}
*/
- queryParams = ['view_role', 'page', 'limit', 'sort', 'query', 'service', 'type'];
+ queryParams = this.roleActions.queryParamsFor(['view_role', 'page', 'limit', 'sort', 'query', 'service', 'type']);
/**
* The current page of data being viewed
@@ -69,6 +69,42 @@ export default class RolesIndexController extends Controller {
*/
@tracked types = this.iam.schemeTypes;
+ /**
+ * Header buttons. Extensions add to them through `iam:table:role:actions`.
+ *
+ * @var {Array}
+ */
+ get actionButtons() {
+ return [
+ {
+ id: 'create',
+ text: this.intl.t('iam.common.new'),
+ type: 'primary',
+ icon: 'plus',
+ iconPrefix: 'fas',
+ onClick: this.createRole,
+ permission: 'iam create role',
+ },
+ ];
+ }
+
+ /**
+ * Bulk actions for the selected rows. Extensions add to them through `iam:table:role:bulk-actions`.
+ *
+ * @var {Array}
+ */
+ get bulkActions() {
+ return [
+ {
+ id: 'bulk-delete',
+ label: this.intl.t('iam.roles.index.delete-roles'),
+ class: 'text-red-500',
+ fn: this.bulkDeleteRoles,
+ permission: 'iam delete role',
+ },
+ ];
+ }
+
/**
* All columns applicable for roles
*
@@ -76,6 +112,7 @@ export default class RolesIndexController extends Controller {
*/
@tracked columns = [
{
+ id: 'name',
label: this.intl.t('iam.common.name'),
valuePath: 'name',
cellComponent: 'table/cell/anchor',
@@ -85,12 +122,14 @@ export default class RolesIndexController extends Controller {
sortable: false,
},
{
+ id: 'description',
label: this.intl.t('iam.common.description'),
valuePath: 'description',
sortable: false,
width: '28%',
},
{
+ id: 'service',
label: this.intl.t('iam.common.service'),
valuePath: 'service',
sortable: false,
@@ -100,6 +139,7 @@ export default class RolesIndexController extends Controller {
filterOptions: this.services,
},
{
+ id: 'type',
label: this.intl.t('iam.common.type'),
valuePath: 'type',
sortable: false,
@@ -111,6 +151,7 @@ export default class RolesIndexController extends Controller {
filterOptions: this.types,
},
{
+ id: 'created-at',
label: this.intl.t('iam.common.create'),
valuePath: 'createdAt',
sortable: false,
@@ -119,6 +160,7 @@ export default class RolesIndexController extends Controller {
cellClassNames: 'overflow-visible',
},
{
+ id: 'row-actions',
label: '',
cellComponent: 'table/cell/dropdown',
ddButtonText: false,
@@ -130,11 +172,13 @@ export default class RolesIndexController extends Controller {
width: '15%',
actions: [
{
+ id: 'edit-role',
label: this.intl.t('iam.roles.index.edit-role'),
fn: this.editRole,
permission: 'iam view role',
},
{
+ id: 'delete-role',
label: this.intl.t('iam.roles.index.delete-role'),
fn: this.deleteRole,
className: 'text-red-700 hover:text-red-800',
diff --git a/addon/controllers/users/index.js b/addon/controllers/users/index.js
index 00ad0b0..998b915 100644
--- a/addon/controllers/users/index.js
+++ b/addon/controllers/users/index.js
@@ -1,6 +1,6 @@
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
-import { tracked } from '@glimmer/tracking';
+import { tracked, cached } from '@glimmer/tracking';
import { action } from '@ember/object';
import { isBlank } from '@ember/utils';
import { timeout, task } from 'ember-concurrency';
@@ -23,11 +23,13 @@ export default class UsersIndexController extends Controller {
get actionButtons() {
return [
{
+ id: 'refresh',
icon: 'refresh',
onClick: () => this.hostRouter.refresh(),
helpText: this.intl.t('common.refresh'),
},
{
+ id: 'invite-user',
text: this.intl.t('iam.users.index.invite-user'),
type: 'default',
icon: 'paper-plane',
@@ -35,6 +37,7 @@ export default class UsersIndexController extends Controller {
onClick: this.inviteUser,
},
{
+ id: 'create-user',
text: this.intl.t('common.new'),
type: 'primary',
icon: 'plus',
@@ -42,6 +45,7 @@ export default class UsersIndexController extends Controller {
onClick: this.createUser,
},
{
+ id: 'export-users',
text: this.intl.t('common.export'),
icon: 'long-arrow-up',
iconClass: 'rotate-icon-45',
@@ -58,6 +62,7 @@ export default class UsersIndexController extends Controller {
return [
{
+ id: 'bulk-delete-users',
label: this.intl.t('common.delete-selected-count', { count: selected.length }),
class: 'text-red-500',
fn: this.bulkDeleteUsers,
@@ -65,7 +70,7 @@ export default class UsersIndexController extends Controller {
];
}
- queryParams = [
+ queryParams = this.userActions.queryParamsFor([
'view_user',
'page',
'limit',
@@ -83,7 +88,7 @@ export default class UsersIndexController extends Controller {
'phone_verified',
'country',
'timezone',
- ];
+ ]);
@tracked page = 1;
@tracked limit;
@tracked query;
@@ -104,8 +109,9 @@ export default class UsersIndexController extends Controller {
*
* @var {Array}
*/
- @tracked columns = [
+ @tracked baseColumns = [
{
+ id: 'name',
sticky: true,
label: this.intl.t('iam.common.name'),
valuePath: 'name',
@@ -119,6 +125,7 @@ export default class UsersIndexController extends Controller {
filterComponent: 'filter/string',
},
{
+ id: 'email',
sticky: true,
label: this.intl.t('iam.common.email'),
valuePath: 'email',
@@ -129,6 +136,7 @@ export default class UsersIndexController extends Controller {
filterComponent: 'filter/string',
},
{
+ id: 'phone',
label: this.intl.t('iam.common.phone'),
valuePath: 'phone',
cellComponent: 'click-to-copy',
@@ -138,6 +146,7 @@ export default class UsersIndexController extends Controller {
filterComponent: 'filter/string',
},
{
+ id: 'role-name',
label: this.intl.t('iam.common.role'),
valuePath: 'role.name',
sortable: false,
@@ -148,6 +157,7 @@ export default class UsersIndexController extends Controller {
model: 'role',
},
{
+ id: 'session-status',
label: this.intl.t('iam.common.status'),
valuePath: 'session_status',
sortable: false,
@@ -158,6 +168,7 @@ export default class UsersIndexController extends Controller {
filterOptions: ['pending', 'active', 'inactive'],
},
{
+ id: 'email-verified-at',
label: this.intl.t('iam.users.index.email-verified'),
valuePath: 'email_verified_at',
contactPath: 'email',
@@ -169,6 +180,7 @@ export default class UsersIndexController extends Controller {
filterOptions: this.verificationFilterOptions,
},
{
+ id: 'phone-verified-at',
label: this.intl.t('iam.users.index.phone-verified'),
valuePath: 'phone_verified_at',
contactPath: 'phone',
@@ -180,6 +192,7 @@ export default class UsersIndexController extends Controller {
filterOptions: this.verificationFilterOptions,
},
{
+ id: 'country',
label: this.intl.t('iam.common.country'),
valuePath: 'country',
cellComponent: 'table/cell/country',
@@ -191,6 +204,7 @@ export default class UsersIndexController extends Controller {
filterParam: 'country',
},
{
+ id: 'timezone',
label: this.intl.t('iam.users.index.timezone'),
valuePath: 'timezone',
hidden: true,
@@ -201,6 +215,7 @@ export default class UsersIndexController extends Controller {
filterParam: 'timezone',
},
{
+ id: 'date-of-birth',
label: this.intl.t('iam.users.index.date-of-birth'),
valuePath: 'date_of_birth',
hidden: true,
@@ -209,6 +224,7 @@ export default class UsersIndexController extends Controller {
filterable: false,
},
{
+ id: 'ip-address',
label: this.intl.t('iam.users.index.ip-address'),
valuePath: 'ip_address',
cellComponent: 'click-to-copy',
@@ -218,6 +234,7 @@ export default class UsersIndexController extends Controller {
filterable: false,
},
{
+ id: 'last-login',
label: this.intl.t('iam.users.index.last-login'),
valuePath: 'lastLogin',
resizable: true,
@@ -226,6 +243,7 @@ export default class UsersIndexController extends Controller {
filterComponent: 'filter/date',
},
{
+ id: 'created-at',
label: this.intl.t('iam.users.index.created-at'),
valuePath: 'createdAt',
sortParam: 'created_at',
@@ -235,6 +253,7 @@ export default class UsersIndexController extends Controller {
filterComponent: 'filter/date',
},
{
+ id: 'updated-at',
label: this.intl.t('iam.users.index.updated-at'),
valuePath: 'updatedAt',
sortParam: 'updated_at',
@@ -245,6 +264,7 @@ export default class UsersIndexController extends Controller {
filterComponent: 'filter/date',
},
{
+ id: 'row-actions',
label: '',
cellComponent: 'table/cell/dropdown',
ddButtonText: false,
@@ -257,22 +277,26 @@ export default class UsersIndexController extends Controller {
width: 60,
actions: [
{
+ id: 'edit-user',
label: this.intl.t('iam.users.index.edit-user'),
fn: this.editUser,
permission: 'iam view user',
},
{
+ id: 'view-user-permissions',
label: this.intl.t('iam.users.index.view-user-permissions'),
fn: this.viewUserPermissions,
permission: 'iam view user',
},
{
+ id: 'resend-invitation',
label: this.intl.t('iam.users.index.re-send-invitation'),
fn: this.resendInvitation,
permission: 'iam update user',
isVisible: (user) => user.get('session_status') === 'pending',
},
{
+ id: 'deactivate-user',
label: this.intl.t('iam.users.index.deactivate-user'),
fn: this.deactivateUser,
className: 'text-danger',
@@ -280,6 +304,7 @@ export default class UsersIndexController extends Controller {
isVisible: (user) => user.get('session_status') === 'active',
},
{
+ id: 'activate-user',
label: this.intl.t('iam.users.index.activate-user'),
fn: this.activateUser,
className: 'text-danger',
@@ -287,18 +312,21 @@ export default class UsersIndexController extends Controller {
isVisible: (user) => user.get('session_status') === 'inactive' || (this.currentUser.user.is_admin && user.get('session_status') === 'pending'),
},
{
+ id: 'send-email-verification',
label: this.intl.t('iam.users.index.send-email-verification'),
fn: (user) => this.userActions.sendVerification(user, 'email'),
permission: 'iam verify user',
isVisible: (user) => this.canVerify(user, 'email'),
},
{
+ id: 'send-phone-verification',
label: this.intl.t('iam.users.index.send-phone-verification'),
fn: (user) => this.userActions.sendVerification(user, 'phone'),
permission: 'iam verify user',
isVisible: (user) => this.canVerify(user, 'phone'),
},
{
+ id: 'mark-email-verified',
label: this.intl.t('iam.users.index.mark-email-verified'),
fn: (user) => this.userActions.markVerified(user, 'email'),
className: 'text-danger',
@@ -306,6 +334,7 @@ export default class UsersIndexController extends Controller {
isVisible: (user) => this.canVerify(user, 'email'),
},
{
+ id: 'mark-phone-verified',
label: this.intl.t('iam.users.index.mark-phone-verified'),
fn: (user) => this.userActions.markVerified(user, 'phone'),
className: 'text-danger',
@@ -313,18 +342,21 @@ export default class UsersIndexController extends Controller {
isVisible: (user) => this.canVerify(user, 'phone'),
},
{
+ id: 'change-user-password',
label: this.intl.t('iam.users.index.change-user-password'),
fn: this.changeUserPassword,
className: 'text-danger',
isVisible: (user) => this.abilities.can('iam change-password-for user') || user.role_name === 'Administrator' || user.is_admin === true,
},
{
+ id: 'change-user-email',
label: this.intl.t('iam.users.index.change-user-email'),
fn: this.changeUserEmail,
className: 'text-danger',
isVisible: () => this.abilities.can('iam change-email-for user') || this.currentUser.user.role_name === 'Administrator' || this.currentUser.user.is_admin === true,
},
{
+ id: 'delete-user',
label: this.intl.t('iam.users.index.delete-user'),
fn: this.deleteUser,
className: 'text-danger',
@@ -338,6 +370,16 @@ export default class UsersIndexController extends Controller {
},
];
+ /**
+ * The columns with what extensions registered under `iam:table:user` merged in.
+ * The table and its header (`users.hbs`) render separately, so both read this.
+ *
+ * @var {Array}
+ */
+ @cached get columns() {
+ return this.userActions.mergeRegisteredColumns(this.baseColumns, { controller: this });
+ }
+
/**
* Options for the email/phone verified column filters.
*
diff --git a/addon/extension.js b/addon/extension.js
index d89149b..ec20d2b 100644
--- a/addon/extension.js
+++ b/addon/extension.js
@@ -182,5 +182,9 @@ export default {
// Register dashboard and widgets
registerWidgets(widgetService);
+
+ // Declare the registries extensions can add table columns, actions and
+ // buttons to, e.g. `iam:table:user:columns` or `iam:table:role:row-actions`.
+ universe.getService('universe/resource-view-service')?.declare('iam', ['group', 'policy', 'role', 'user']);
},
};
diff --git a/addon/services/group-actions.js b/addon/services/group-actions.js
index c2ce88e..cacd279 100644
--- a/addon/services/group-actions.js
+++ b/addon/services/group-actions.js
@@ -1,4 +1,5 @@
import ResourceActionService from '@fleetbase/ember-core/services/resource-action';
+import { mergeHeaderButtons } from '@fleetbase/ember-ui/utils/resource-view';
import { action } from '@ember/object';
import getWithDefault from '@fleetbase/ember-core/utils/get-with-default';
@@ -65,6 +66,8 @@ export default class GroupActionsService extends ResourceActionService {
const formPermission = 'iam update group';
this.modalsManager.show('modals/group-form', {
title: this.intl.t('iam.groups.index.edit-group-title'),
+ // What extensions registered under `iam:details:group` (buttons and "…" menu items).
+ actionButtons: mergeHeaderButtons(this.resourceView, this.detailsRegistry, [], { resource: group }, { withMenu: true }),
acceptButtonText: this.intl.t('common.save-changes'),
acceptButtonIcon: 'save',
acceptButtonDisabled: this.abilities.cannot(formPermission),
diff --git a/addon/services/policy-actions.js b/addon/services/policy-actions.js
index 830a154..99ae9f2 100644
--- a/addon/services/policy-actions.js
+++ b/addon/services/policy-actions.js
@@ -1,4 +1,5 @@
import ResourceActionService from '@fleetbase/ember-core/services/resource-action';
+import { mergeHeaderButtons } from '@fleetbase/ember-ui/utils/resource-view';
import { action } from '@ember/object';
/**
@@ -74,6 +75,8 @@ export default class PolicyActionsService extends ResourceActionService {
const formPermission = 'iam update policy';
this.modalsManager.show('modals/policy-form', {
title: this.intl.t('iam.policies.index.edit-policy-title'),
+ // What extensions registered under `iam:details:policy` (buttons and "…" menu items).
+ actionButtons: mergeHeaderButtons(this.resourceView, this.detailsRegistry, [], { resource: policy }, { withMenu: true }),
acceptButtonText: this.intl.t('common.save-changes'),
acceptButtonIcon: 'save',
acceptButtonDisabled: this.abilities.cannot(formPermission),
diff --git a/addon/services/role-actions.js b/addon/services/role-actions.js
index ce234af..d47bd66 100644
--- a/addon/services/role-actions.js
+++ b/addon/services/role-actions.js
@@ -1,4 +1,5 @@
import ResourceActionService from '@fleetbase/ember-core/services/resource-action';
+import { mergeHeaderButtons } from '@fleetbase/ember-ui/utils/resource-view';
import { action } from '@ember/object';
/**
@@ -83,6 +84,8 @@ export default class RoleActionsService extends ResourceActionService {
const formPermission = 'iam update role';
this.modalsManager.show('modals/role-form', {
title: this.intl.t('iam.roles.index.edit-role-title'),
+ // What extensions registered under `iam:details:role` (buttons and "…" menu items).
+ actionButtons: mergeHeaderButtons(this.resourceView, this.detailsRegistry, [], { resource: role }, { withMenu: true }),
acceptButtonText: this.intl.t('common.save-changes'),
acceptButtonIcon: 'save',
acceptButtonDisabled: this.abilities.cannot(formPermission),
diff --git a/addon/services/user-actions.js b/addon/services/user-actions.js
index 0008eff..92c1f8f 100644
--- a/addon/services/user-actions.js
+++ b/addon/services/user-actions.js
@@ -1,4 +1,5 @@
import ResourceActionService from '@fleetbase/ember-core/services/resource-action';
+import { mergeHeaderButtons } from '@fleetbase/ember-ui/utils/resource-view';
import { action } from '@ember/object';
/**
@@ -180,6 +181,8 @@ export default class UserActionsService extends ResourceActionService {
const formPermission = 'iam update user';
this.modalsManager.show('modals/user-form', {
title: this.intl.t('iam.users.index.edit-user-title'),
+ // What extensions registered under `iam:details:user` (buttons and "…" menu items).
+ actionButtons: mergeHeaderButtons(this.resourceView, this.detailsRegistry, [], { resource: user }, { withMenu: true }),
modalClass: 'modal-lg',
acceptButtonText: this.intl.t('common.save-changes'),
acceptButtonIcon: 'save',
diff --git a/addon/templates/groups/index.hbs b/addon/templates/groups/index.hbs
index 2e747bc..d278b96 100644
--- a/addon/templates/groups/index.hbs
+++ b/addon/templates/groups/index.hbs
@@ -1,31 +1,17 @@
-
- {{#if (safe-has this.table "selectedRows")}}
-
-
-
- {{/if}}
-
-
-
-
-
-
-
+
{{outlet}}
\ No newline at end of file
diff --git a/addon/templates/policies/index.hbs b/addon/templates/policies/index.hbs
index 57e4de0..d4c5fde 100644
--- a/addon/templates/policies/index.hbs
+++ b/addon/templates/policies/index.hbs
@@ -1,40 +1,18 @@
-
-
-
-
- {{#if (safe-has this.table "selectedRows")}}
-
-
-
- {{/if}}
-
-
-
-
-
-
+
{{outlet}}
\ No newline at end of file
diff --git a/addon/templates/roles/index.hbs b/addon/templates/roles/index.hbs
index 22ae341..31b33b6 100644
--- a/addon/templates/roles/index.hbs
+++ b/addon/templates/roles/index.hbs
@@ -1,40 +1,18 @@
-
-
-
-
- {{#if (safe-has this.table "selectedRows")}}
-
-
-
- {{/if}}
-
-
-
-
-
-
+
{{outlet}}
\ No newline at end of file
diff --git a/addon/templates/users.hbs b/addon/templates/users.hbs
index 90845fb..91b0d92 100644
--- a/addon/templates/users.hbs
+++ b/addon/templates/users.hbs
@@ -1,5 +1,6 @@