Skip to content
Open
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
36bf61b
chore: 🤖 Add initial page layout for code editor + sidebar (#3180)
lisbet-alvarez Mar 4, 2026
7343f99
Replace `Rose::CodeEditor` with `Hds::CodeEditor` (#2679)
zamoore Mar 6, 2026
4beaa6d
feat: 🎸 Create string formats component (#3191)
lisbet-alvarez Mar 6, 2026
28fa5fd
feat: 🎸 add grant schema to mirage (#3196)
cameronperera Mar 12, 2026
89ec9df
feat: 🎸 add export options flyout (#3198)
lisbet-alvarez Mar 20, 2026
3df2e64
feat: 🎸 Add autocompletion to code editor (#3207)
ZedLi Apr 16, 2026
aafa312
feat: 🎸 create grant actions component
cameronperera Apr 23, 2026
54478ab
feat: 🎸 add description to actions
cameronperera Apr 23, 2026
806a723
feat: 🎸 update default logic to not show all actions
cameronperera Apr 23, 2026
581c37a
feat: 🎸 show invalid id and type message
cameronperera Apr 24, 2026
e92af6d
test: 💍 create const for test selectors
cameronperera Apr 24, 2026
85eb53b
feat: 🎸 trigger action suggestion when cursor moves
cameronperera Apr 24, 2026
6507e86
refactor: 💡 add createGrantLineHelpers func
cameronperera Apr 24, 2026
f1cbb62
refactor: 💡 small refactors
cameronperera Apr 24, 2026
237f03c
feat: 🎸 Add CSP nonce to code editors (#3230)
bgajjala8 Apr 27, 2026
e6ef513
refactor: 💡 small refactor
cameronperera Apr 27, 2026
5a9211e
test: 💍 add more tests
cameronperera Apr 27, 2026
f3ee680
Merge branch 'llb/grants-builder' into grant-actions-card
cameronperera Apr 27, 2026
e14c553
Merge branch 'llb/grants-builder' into grant-actions-card
cameronperera Apr 28, 2026
934e767
refactor: 💡 fix merge conflicts issues
cameronperera Apr 28, 2026
591f262
refactor: 💡 updates from PR feedback
cameronperera May 6, 2026
41f2eaa
Merge branch 'llb/grants-builder' into grant-actions-card
cameronperera May 6, 2026
437b771
refactor: 💡 updates for completions and linting
cameronperera May 6, 2026
9708243
test: 💍 fix test issue with translations
cameronperera May 7, 2026
2b4f182
test: 💍 fix test issue that was already fixed once
cameronperera May 7, 2026
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
1 change: 1 addition & 0 deletions addons/core/translations/en-us.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ titles:
empty-set: No Items Yet
global: Global
actions: Actions
action: Action
documentation: Documentation
abandon-confirm: Discard unsaved changes?
welcome-to-boundary: Welcome to Boundary
Expand Down
62 changes: 62 additions & 0 deletions addons/core/translations/resources/en-us.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -696,6 +696,68 @@ role:
options:
terraform: Terraform
native-hcl: Native HCL
actions:
collection-actions: Collection Actions
id-actions: ID Actions
create: Create a new {resourceType}
list: List all {resourceType} resources
read: Read a {resourceType}
update: Update a {resourceType}
delete: Delete a {resourceType}
read:self: Read the {resourceType} associated with the current user
delete:self: Delete a {resourceType} associated with the current user
cancel: Cancel a {resourceType}
cancel:self: Cancel a {resourceType} associated with the current user
add-members: Add members to a group
set-members: Set the full list of members on a group
remove-members: Remove members from a group
add-hosts: Add hosts to a host set
set-hosts: Set the full list of hosts on a host set
remove-hosts: Remove hosts from a host set
add-worker-tags: Add tags to a worker
set-worker-tags: Set the full list of tags on a worker
remove-worker-tags: Remove tags from a worker
reinitialize-certificate-authority: Reinitialize the certificate authority that is used to authorize Boundary workers
create:controller-led: Create a worker via controller-led workflow
create:worker-led: Create a worker via worker-led workflow
read-certificate-authority: Read the certificate authority that is used to authorize Boundary workers
set-password: Set a password on an account, without requiring the current password
change-password: Change the password on an account
authenticate: Authenticate to an auth method
change-state: Change the active and visibility state of an OIDC-type auth method
add-accounts: Add accounts to a user
set-accounts: Set the full list of accounts on a user
remove-accounts: Remove accounts from a user
list-resolvable-aliases: List all aliases that point to resources the user has permission to access
add-principals: Add principals to a role
set-principals: Set the full list of principals on a role
remove-principals: Remove principals from a role
set-grants: Set the full list of grants on a role
add-grants: Add grants to a role
remove-grants: Remove grants from a role
add-grant-scopes: Add grant scopes to a role
set-grant-scopes: Set the full list of grant scopes on a role
remove-grant-scopes: Remove grant scopes from a role
authorize-session: Authorize a session via a target
add-host-sources: Add host sources to a target
set-host-sources: Set the full list of host sources on a target
remove-host-sources: Remove host sources from a target
add-credential-sources: Add credential sources to a target
set-credential-sources: Set the full list of credential sources on a target
remove-credential-sources: Remove credential sources from a target
attach-storage-policy: Attach a storage policy to all session recordings in the scope
detach-storage-policy: Detach a storage policy from all session recordings in the scope
list-keys: List the keys within a given scope
rotate-keys: Replace a scope's current KEK and DEKs with a new set of keys
list-key-version-destruction-jobs: List all pending key version destruction jobs within a scope
destroy-key-version: Destroy a key version in the scope
download: Download a session recording
reapply-storage-policy: Reapply the storage policy to a session recording
monthly-active-users: Display the number of monthly active Boundary users to help predict billing
no-actions: No actions available for this resource type.
invalid-id-or-type: Invalid ID and type. No actions available.
no-type-detected: No resource type detected.
unavailable: Grant actions information not available.
scope:
title: Scope
title_plural: Scopes
Expand Down
8 changes: 4 additions & 4 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions ui/admin/app/components/form/role/edit-grants/index.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,15 @@
<GrantStringFormats />
</:content>
</A.Item>
<A.Item @isOpen={{true}} @isStatic={{true}} data-test-grant-actions>
<:toggle>{{t 'titles.actions'}}</:toggle>
<:content>
<GrantActions
@grantsSchema={{@grantsSchema}}
@grantString={{this.currentLineText}}
/>
</:content>
</A.Item>
</Hds::Accordion>
</Hds::Layout::Grid>

Expand Down
9 changes: 8 additions & 1 deletion ui/admin/app/components/form/role/edit-grants/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
autocompletion,
completionKeymap,
keymap,
EditorView,
} from '@hashicorp/design-system-components/codemirror';

import { createGrantCompletionSource } from 'admin/utils/grant-completions';
Expand Down Expand Up @@ -47,7 +48,7 @@ export default class FormRoleEditGrantsComponent extends Component {
);

@tracked grantStringsText = (this.args.model?.grant_strings ?? []).join('\n');
@tracked currentLineText = this.args.model?.grant_strings?.[0] ?? '';
@tracked currentLineText = '';
@tracked showExportOptionsFlyout = false;
@tracked selectedExportOption = this.exportOptions[0];

Expand All @@ -58,6 +59,12 @@ export default class FormRoleEditGrantsComponent extends Component {
activateOnCompletion: (completion) => completion.type === 'keyword',
}),
keymap.of(completionKeymap),
EditorView.updateListener.of((update) => {
if (update.selectionSet) {
const line = update.state.doc.lineAt(update.state.selection.main.head);
this.currentLineText = line.text;
Comment thread
cameronperera marked this conversation as resolved.
Outdated
}
}),
];

get grantStrings() {
Expand Down
41 changes: 41 additions & 0 deletions ui/admin/app/components/grant-actions/index.hbs
Comment thread
cameronperera marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{{!
Copyright IBM Corp. 2021, 2026
SPDX-License-Identifier: BUSL-1.1
}}

{{#if this.actionRows.length}}
<Hds::Table
@model={{this.actionRows}}
@columns={{array
(hash label=(t 'titles.action'))
(hash label=(t 'form.description.label'))
}}
@valign='middle'
data-test-grant-actions-table
>
<:body as |B|>
<B.Tr>
<B.Td>{{B.data.name}}</B.Td>
<B.Td>{{B.data.description}}</B.Td>
</B.Tr>
</:body>
</Hds::Table>
{{else if this.showInvalidIdAndType}}
<Hds::Alert @type='compact' as |A|>
<A.Description data-test-grant-actions-invalid-id-or-type>
{{t 'resources.role.edit-grants.actions.invalid-id-or-type'}}
</A.Description>
</Hds::Alert>
{{else if this.showNoResourceTypeDetected}}
<Hds::Alert @type='compact' as |A|>
<A.Description data-test-grant-actions-no-type-detected>
{{t 'resources.role.edit-grants.actions.no-type-detected'}}
</A.Description>
</Hds::Alert>
{{else}}
<Hds::Alert @type='compact' as |A|>
<A.Description data-test-grant-actions-empty-state>
{{t 'resources.role.edit-grants.no-suggestions'}}
</A.Description>
</Hds::Alert>
{{/if}}
172 changes: 172 additions & 0 deletions ui/admin/app/components/grant-actions/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/

import Component from '@glimmer/component';
import { service } from '@ember/service';

import {
createGrantLineHelpers,
getCompatibleResourceTypeForIds,
normalizeGrantsSchema,
parseGrantLine,
} from 'admin/utils/grant-completions';

const ACTIONS_WITH_RESOURCE_TYPE = new Set([
Comment thread
cameronperera marked this conversation as resolved.
Outdated
'create',
'list',
'read',
'update',
'delete',
'read:self',
'delete:self',
'cancel',
'cancel:self',
]);

export default class GrantActionsIndex extends Component {
@service intl;

#normalizedSchema = normalizeGrantsSchema(this.args.grantsSchema ?? {});

#grantLineHelpers = createGrantLineHelpers(this.args.grantsSchema ?? {});

get parsedGrantLine() {
return parseGrantLine(this.args.grantString);
Comment thread
cameronperera marked this conversation as resolved.
Outdated
}

get hasSpecificIds() {
return (
this.parsedGrantLine.idsValue &&
!this.parsedGrantLine.idsValue.includes('*')
);
}

get hasExplicitType() {
return (
this.parsedGrantLine.typeValue && this.parsedGrantLine.typeValue !== '*'
);
}

get hasTemplateIds() {
if (!this.hasSpecificIds) {
return false;
}

return this.parsedGrantLine.idsValue
.split(',')
.filter(Boolean)
.some((id) => id.startsWith('{{') && id.endsWith('}}'));
}

get compatibleIdsResourceType() {
if (!this.hasSpecificIds || this.hasTemplateIds) {
Comment thread
cameronperera marked this conversation as resolved.
Outdated
return null;
}

return getCompatibleResourceTypeForIds(
this.#normalizedSchema,
this.parsedGrantLine.idsValue,
);
}

get hasInvalidType() {
return (
this.hasExplicitType &&
!this.#normalizedSchema.resourcesByType[this.parsedGrantLine.typeValue]
);
}

get hasInvalidIds() {
return (
this.hasSpecificIds &&
Comment thread
cameronperera marked this conversation as resolved.
Outdated
!this.hasTemplateIds &&
!this.compatibleIdsResourceType
);
}

get hasInvalidPinnedIdTypeCombination() {
if (
!this.hasSpecificIds ||
!this.hasExplicitType ||
this.hasInvalidIds ||
this.hasInvalidType
) {
return false;
}

const childTypes =
this.#normalizedSchema.childResourceTypesByParentType[
this.compatibleIdsResourceType
] ?? [];

return !childTypes.includes(this.parsedGrantLine.typeValue);
}

get actions() {
if (!this.parsedGrantLine.idsValue && !this.parsedGrantLine.typeValue) {
return [];
}

return this.#grantLineHelpers
.getSuggestedActions(this.args.grantString)
.filter((action) => action !== '*')
.sort((left, right) => left.localeCompare(right));
}

get descriptionResourceType() {
if (this.parsedGrantLine.typeValue === '*') {
return null;
}

return (
this.parsedGrantLine.typeValue ||
this.#grantLineHelpers.getDetectedResourceType(this.args.grantString)
Comment thread
cameronperera marked this conversation as resolved.
Outdated
);
}

get actionRows() {
return this.actions.map((action) => ({
name: action,
description: this.getActionDescription(action),
}));
}

get showNoResourceTypeDetected() {
return (
!this.showInvalidIdAndType &&
!this.actions.length &&
!this.#grantLineHelpers.getDetectedResourceType(this.args.grantString)
);
}

get showInvalidIdAndType() {
return (
!this.actions.length &&
(this.hasInvalidType ||
this.hasInvalidIds ||
this.hasInvalidPinnedIdTypeCombination)
);
}

getActionDescription(action) {
const translationKey = `resources.role.edit-grants.actions.${action}`;

if (!this.intl.exists(translationKey)) {
return action;
}

if (ACTIONS_WITH_RESOURCE_TYPE.has(action)) {
if (!this.descriptionResourceType) {
return action;
}

return this.intl.t(translationKey, {
resourceType: this.descriptionResourceType,
});
}

return this.intl.t(translationKey);
}
}
Loading
Loading