Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ on:
tags:
- 'v*'
pull_request:
branches: [ main ]
branches: [ main, 'release/v*', 'dev-v*' ]

env:
NODE_VERSION: 22.x
Expand Down
8 changes: 2 additions & 6 deletions RELEASE.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
> v0.1.11 ~ "RELEASE_NOTES_PLACEHOLDER — replace this line with the release title"
> v0.1.12 ~ "IAM dialogs open from anywhere"

---
## Highlights

RELEASE_NOTES_PLACEHOLDER

Describe what changed in this release. The first line above must name the version
being released, and both placeholder markers must be gone, or the release workflow
refuses to tag.
- **IAM dialogs are reusable services.** New `user-actions`, `group-actions`, `role-actions` and `policy-actions` services hold the create, edit, invite and permission dialogs that lived in the index controllers, so other engines can open them — Fleetbase AI uses this to open **New User** from its confirmation cards. The IAM pages delegate to the same services, so buttons, row actions and deep links behave as before. ([#33](https://github.com/fleetbase/iam-engine/pull/33))

---
## Need help?
Expand Down
2 changes: 1 addition & 1 deletion addon/components/modals/invite-user.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<div class="grid grid-cols-1">
<InputGroup @name={{t "iam.common.email"}} @type="email" @value={{@options.email}} @placeholder={{t "iam.users.invite.email-placeholder"}} />
<InputGroup @name={{t "iam.common.name"}} @value={{@options.name}} @placeholder={{t "iam.users.invite.name-placeholder"}} @helpText={{t "iam.users.invite.name-help"}} />
<InputGroup @name={{t "iam.common.role"}}>
<InputGroup @name={{t "iam.common.role"}} @required={{true}}>
<ModelSelect
@modelName="role"
@selectedModel={{@options.role}}
Expand Down
2 changes: 1 addition & 1 deletion addon/components/modals/user-form.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
placeholder={{t "iam.common.country"}}
/>
</InputGroup>
<InputGroup @name={{t "iam.common.role"}}>
<InputGroup @name={{t "iam.common.role"}} @required={{true}}>
<ModelSelect
@modelName="role"
@selectedModel={{@options.user.role}}
Expand Down
9 changes: 9 additions & 0 deletions addon/components/table/cell/verification-status.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{{#let (get @row @column.valuePath) as |verifiedAt|}}
{{#if verifiedAt}}
<Badge @status="success" @text={{t "iam.users.index.verified"}} @helpText={{format-date-fns verifiedAt}} />
{{else if (get @row @column.contactPath)}}
<Badge @status="warning" @text={{t "iam.users.index.unverified"}} />
{{else}}
<span class="text-gray-400 dark:text-gray-500">-</span>
{{/if}}
{{/let}}
8 changes: 8 additions & 0 deletions addon/components/table/cell/verification-status.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import Component from '@glimmer/component';

/**
* Table cell showing whether a user's email or phone is verified. The column's
* `valuePath` is the verified-at date and `contactPath` the email/phone it
* belongs to, so a user without that contact shows a dash instead.
*/
export default class TableCellVerificationStatusComponent extends Component {}
33 changes: 2 additions & 31 deletions addon/controllers/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,41 +19,12 @@ export default class ApplicationController extends Controller {
},
{
label: this.intl.t('iam.common.user'),
description: 'Manage identities and account types.',
description: 'Manage team member accounts.',
icon: 'id-card',
route: 'console.iam.users',
permission: 'iam list user',
visible: this.can('iam see user'),
keywords: ['identity', 'accounts', 'members'],
children: [
{
label: 'Users',
description: 'All console users.',
icon: 'user',
route: 'console.iam.users.index',
permission: 'iam list user',
visible: this.can('iam see user'),
keywords: ['people', 'members'],
},
{
label: 'Drivers',
description: 'Driver accounts.',
icon: 'id-card',
route: 'console.iam.users.drivers',
permission: 'iam list user',
visible: this.can('iam see user'),
keywords: ['fleet operators'],
},
{
label: 'Customers',
description: 'Customer accounts.',
icon: 'users',
route: 'console.iam.users.customers',
permission: 'iam list user',
visible: this.can('iam see user'),
keywords: ['clients'],
},
],
keywords: ['identity', 'accounts', 'members', 'people', 'team'],
},
{
label: this.intl.t('iam.common.group'),
Expand Down
106 changes: 8 additions & 98 deletions addon/controllers/groups/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { isBlank } from '@ember/utils';
import { timeout, task } from 'ember-concurrency';
import getWithDefault from '@fleetbase/ember-core/utils/get-with-default';

export default class GroupsIndexController extends Controller {
@service groupActions;
@controller('users.index') usersIndexController;
@service store;
@service intl;
Expand Down Expand Up @@ -170,107 +170,17 @@ export default class GroupsIndexController extends Controller {
this.crud.export('group');
}

/**
* Toggles modal to create a new group
*
* @void
*/
@action createGroup() {
const formPermission = 'iam create group';
const group = this.store.createRecord('group', { users: [] });

this.editGroup(group, {
title: this.intl.t('iam.groups.index.new-group'),
acceptButtonText: this.intl.t('common.confirm'),
acceptButtonIcon: 'check',
acceptButtonDisabled: this.abilities.cannot(formPermission),
acceptButtonHelpText: this.abilities.cannot(formPermission) ? this.intl.t('common.unauthorized') : null,
formPermission,
group,
confirm: async (modal) => {
modal.startLoading();

if (this.abilities.cannot(formPermission)) {
return this.notifications.warning(this.intl.t('common.permissions-required-for-changes'));
}
try {
await group.save();
this.notifications.success(this.intl.t('iam.groups.index.new-group-created'));
return this.hostRouter.refresh();
} catch (error) {
this.notifications.serverError(error);
modal.stopLoading();
}
},
});
// Dialog and lifecycle actions live in the group-actions service so other engines can open them too.
@action createGroup(...args) {
return this.groupActions.createGroup(...args);
}

/**
* Toggles modal to edit a group
*
* @void
*/
@action editGroup(group, options = {}) {
const formPermission = 'iam update group';
this.modalsManager.show('modals/group-form', {
title: this.intl.t('iam.groups.index.edit-group-title'),
acceptButtonText: this.intl.t('common.save-changes'),
acceptButtonIcon: 'save',
acceptButtonDisabled: this.abilities.cannot(formPermission),
acceptButtonHelpText: this.abilities.cannot(formPermission) ? this.intl.t('common.unauthorized') : null,
formPermission,
group,
lastSelectedUser: null,
removeUser: (user) => {
group.users.removeObject(user);
},
addUser: (user) => {
group.users.pushObject(user);
this.modalsManager.setOption('lastSelectedUser', null);
},
confirm: async (modal) => {
modal.startLoading();

if (this.abilities.cannot(formPermission)) {
return this.notifications.warning(this.intl.t('common.permissions-required-for-changes'));
}

try {
await group.save();
this.notifications.success(this.intl.t('iam.groups.index.changes-group-save'));
return this.hostRouter.refresh();
} catch (error) {
this.notifications.serverError(error);
modal.stopLoading();
}
},
...options,
});
@action editGroup(...args) {
return this.groupActions.editGroup(...args);
}

/**
* Toggles dialog to delete a group
*
* @void
*/
@action deleteGroup(group) {
const groupName = getWithDefault(group, 'name', this.intl.t('iam.groups.index.untitled'));

this.modalsManager.confirm({
title: this.intl.t('iam.groups.index.delete-group-title', { groupName }),
body: this.intl.t('iam.groups.index.data-assosciated-this-group-deleted'),
confirm: async (modal) => {
modal.startLoading();
try {
await group.destroyRecord();
this.notifications.success(this.intl.t('iam.groups.index.delete-group-success-message', { name: group.name }));
return this.hostRouter.refresh();
} catch (error) {
this.notifications.serverError(error);
modal.stopLoading();
}
},
});
@action deleteGroup(...args) {
return this.groupActions.deleteGroup(...args);
}

@action async openDeepLinkedResource() {
Expand Down
126 changes: 10 additions & 116 deletions addon/controllers/policies/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { isBlank } from '@ember/utils';
import { timeout, task } from 'ember-concurrency';

export default class PoliciesIndexController extends Controller {
@service policyActions;
@service store;
@service intl;
@service notifications;
Expand Down Expand Up @@ -198,128 +199,21 @@ export default class PoliciesIndexController extends Controller {
});
}

/**
* Toggles modal to create a new API key
*
* @void
*/
@action createPolicy() {
const formPermission = 'iam create policy';
const policy = this.store.createRecord('policy', {
is_mutable: true,
is_deletable: true,
});

this.editPolicy(policy, {
title: this.intl.t('iam.policies.index.new-policy'),
acceptButtonText: this.intl.t('common.confirm'),
acceptButtonIcon: 'check',
acceptButtonDisabled: this.abilities.cannot(formPermission),
acceptButtonHelpText: this.abilities.cannot(formPermission) ? this.intl.t('common.unauthorized') : null,
formPermission,
confirm: async (modal) => {
modal.startLoading();

if (this.abilities.cannot(formPermission)) {
return this.notifications.warning(this.intl.t('common.permissions-required-for-changes'));
}

try {
await policy.save();
this.notifications.success(this.intl.t('iam.policies.index.new-policy-created'));
return this.hostRouter.refresh();
} catch (error) {
this.notifications.serverError(error);
modal.stopLoading();
}
},
});
// Dialog and lifecycle actions live in the policy-actions service so other engines can open them too.
@action createPolicy(...args) {
return this.policyActions.createPolicy(...args);
}

/**
* Toggles modal to create a new API key
*
* @param {PolicyModel} policy
* @memberof PoliciesIndexController
* @void
*/
@action editPolicy(policy, options = {}) {
if (!policy.is_mutable) {
return this.viewPolicyPermissions(policy, options);
}

const formPermission = 'iam update policy';
this.modalsManager.show('modals/policy-form', {
title: this.intl.t('iam.policies.index.edit-policy-title'),
acceptButtonText: this.intl.t('common.save-changes'),
acceptButtonIcon: 'save',
acceptButtonDisabled: this.abilities.cannot(formPermission),
acceptButtonHelpText: this.abilities.cannot(formPermission) ? this.intl.t('common.unauthorized') : null,
formPermission,
policy,
confirm: async (modal) => {
modal.startLoading();

if (this.abilities.cannot(formPermission)) {
return this.notifications.warning(this.intl.t('common.permissions-required-for-changes'));
}

try {
await policy.save();
this.notifications.success(this.intl.t('iam.policies.index.changes-policy-saved-success'));
return this.hostRouter.refresh();
} catch (error) {
this.notifications.serverError(error);
modal.stopLoading();
}
},
...options,
});
@action editPolicy(...args) {
return this.policyActions.editPolicy(...args);
}

/**
* View policy permissions
*
* @param {PolicyModel} policy
* @memberof PoliciesIndexController
*/
@action viewPolicyPermissions(policy, options = {}) {
this.modalsManager.show('modals/view-policy-permissions', {
title: this.intl.t('iam.components.modals.view-policy-permissions.view-permissions', { policyName: policy.name }),
hideDeclineButton: true,
acceptButtonText: this.intl.t('common.done'),
policy,
...options,
});
@action viewPolicyPermissions(...args) {
return this.policyActions.viewPolicyPermissions(...args);
}

/**
* Toggles dialog to delete API key
*
* @param {PolicyModel} policy
* @memberof PoliciesIndexController
* @void
*/
@action deletePolicy(policy) {
if (!policy.is_deletable) {
return this.notifications.warning(this.intl.t('iam.policies.index.unable-delete-policy-warning', { policyType: policy.type }));
}

this.modalsManager.confirm({
title: `Delete (${policy.name || 'Untitled'}) policy`,
body: this.intl.t('iam.policies.index.data-assosciated-this-policy-deleted'),
confirm: async (modal) => {
modal.startLoading();
try {
await policy.destroyRecord();
this.notifications.success(this.intl.t('iam.policies.index.policy-deleted', { policyName: policy.name }));
return this.hostRouter.refresh();
} catch (error) {
this.notifications.serverError(error);
modal.stopLoading();
}
},
});
@action deletePolicy(...args) {
return this.policyActions.deletePolicy(...args);
}

/**
Expand Down
Loading
Loading