Skip to content

Commit 4402f41

Browse files
committed
feat: 🎸 Add autocompletion to code editor (#3207)
1 parent d2f2f04 commit 4402f41

12 files changed

Lines changed: 1065 additions & 32 deletions

File tree

‎addons/core/translations/resources/en-us.yaml‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -663,6 +663,16 @@ role:
663663
edit-grants:
664664
title: Edit Grants
665665
description: Modify existing grant strings or add new grant strings to this role.
666+
no-suggestions: No suggestions
667+
completion-info:
668+
wildcard-types: Wildcard - matches all types
669+
wildcard-ids: Wildcard - matches all IDs
670+
wildcard-actions: Wildcard - matches all actions
671+
template-value: Template value
672+
all-fields: All fields
673+
editor:
674+
title: Grant Editor
675+
description: Add each new grant string on a different line
666676
string-formats:
667677
title: String format
668678
title_plural: String formats
@@ -682,6 +692,7 @@ role:
682692
insert-resource-ids: insert resource ids
683693
export:
684694
format: Format
695+
formatted-export-aria-label: Formatted export
685696
options:
686697
terraform: Terraform
687698
native-hcl: Native HCL

‎ui/admin/app/components/form/role/edit-grants/index.hbs‎

Lines changed: 50 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,58 @@
33
SPDX-License-Identifier: BUSL-1.1
44
}}
55

6-
<Rose::Form class='full-width' as |form|>
6+
<Rose::Form
7+
class='full-width'
8+
@onSubmit={{fn @submit this.grantStrings}}
9+
@cancel={{@cancel}}
10+
@disabled={{@model.isSaving}}
11+
as |form|
12+
>
713
<Hds::Layout::Grid
814
@columnWidth={{hash sm='100%' md='33.33%'}}
915
@gap='12'
1016
as |LG|
1117
>
12-
<LG.Item @colspan={{hash sm=1 md=2}}>
13-
{{! TODO: Add grants code editor. }}
18+
<LG.Item @colspan={{hash sm=1 md=2}} @rowspan='2'>
19+
<Hds::CodeEditor
20+
class='grant-editor'
21+
@hasFullScreenButton={{true}}
22+
@ariaLabel={{t 'form.secret-editor.label'}}
23+
@value={{this.grantStringsText}}
24+
@onInput={{this.onInput}}
25+
@customExtensions={{this.customExtensions}}
26+
data-test-code-editor
27+
as |CE|
28+
>
29+
<CE.Title>
30+
{{t 'resources.role.edit-grants.editor.title'}}
31+
</CE.Title>
32+
<CE.Description>
33+
{{t 'resources.role.edit-grants.editor.description'}}
34+
</CE.Description>
35+
</Hds::CodeEditor>
36+
<form.actions class='margin-top-l'>
37+
<:actions>
38+
<Hds::ButtonSet>
39+
<Hds::Button type='submit' @text={{t 'actions.save'}} />
40+
<Hds::Button
41+
type='button'
42+
@text={{t 'actions.cancel'}}
43+
@color='secondary'
44+
{{on 'click' @cancel}}
45+
/>
46+
{{#if this.grantStringsText}}
47+
<Hds::Button
48+
@text={{t 'actions.export'}}
49+
@color='tertiary'
50+
@icon='upload'
51+
data-test-export-grants-btn
52+
{{on 'click' this.toggleExportOptionsFlyout}}
53+
/>
54+
{{/if}}
55+
</Hds::ButtonSet>
56+
</:actions>
57+
</form.actions>
1458
</LG.Item>
1559

1660
<Hds::Accordion @size='large' as |A|>
@@ -55,7 +99,9 @@
5599
</Hds::Form::Select::Field>
56100

57101
<Hds::CodeBlock
58-
@ariaLabel='formatted-export'
102+
@ariaLabel={{t
103+
'resources.role.edit-grants.export.formatted-export-aria-label'
104+
}}
59105
@hasCopyButton={{true}}
60106
@language='hcl'
61107
@value={{this.formattedExport}}
@@ -71,22 +117,4 @@
71117
</M.Body>
72118
</Hds::Flyout>
73119
{{/if}}
74-
75-
<form.actions class='margin-top-l'>
76-
<:actions>
77-
<Hds::ButtonSet>
78-
<Hds::Button type='submit' @text={{t 'actions.save'}} />
79-
<Hds::Button @text={{t 'actions.cancel'}} @color='secondary' />
80-
{{#if this.grantStringLines}}
81-
<Hds::Button
82-
@text={{t 'actions.export'}}
83-
@color='tertiary'
84-
@icon='upload'
85-
data-test-export-grants-btn
86-
{{on 'click' this.toggleExportOptionsFlyout}}
87-
/>
88-
{{/if}}
89-
</Hds::ButtonSet>
90-
</:actions>
91-
</form.actions>
92120
</Rose::Form>

‎ui/admin/app/components/form/role/edit-grants/index.js‎

Lines changed: 62 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,69 @@
44
*/
55

66
import Component from '@glimmer/component';
7-
import { tracked } from '@glimmer/tracking';
87
import { action } from '@ember/object';
8+
import { service } from '@ember/service';
9+
import { tracked } from '@glimmer/tracking';
10+
import {
11+
autocompletion,
12+
completionKeymap,
13+
keymap,
14+
} from '@hashicorp/design-system-components/codemirror';
15+
16+
import { createGrantCompletionSource } from 'admin/utils/grant-completions';
17+
18+
export default class FormRoleEditGrantsComponent extends Component {
19+
@service intl;
920

10-
export default class FormRoleEditGrants extends Component {
1121
// =attributes
1222

1323
exportOptionsMap = { terraform: 'terraform', nativeHCL: 'native-hcl' };
1424
exportOptions = Object.values(this.exportOptionsMap);
25+
completionTranslatedStrings = {
26+
noSuggestions: this.intl.t('resources.role.edit-grants.no-suggestions'),
27+
wildcardTypes: this.intl.t(
28+
'resources.role.edit-grants.completion-info.wildcard-types',
29+
),
30+
wildcardIds: this.intl.t(
31+
'resources.role.edit-grants.completion-info.wildcard-ids',
32+
),
33+
templateValue: this.intl.t(
34+
'resources.role.edit-grants.completion-info.template-value',
35+
),
36+
wildcardActions: this.intl.t(
37+
'resources.role.edit-grants.completion-info.wildcard-actions',
38+
),
39+
allFields: this.intl.t(
40+
'resources.role.edit-grants.completion-info.all-fields',
41+
),
42+
};
1543

16-
// TODO: Replace with actual grant lines from code editor once implemented.
17-
grantStringLines =
18-
'ids=hc_123;type=host-catalog;actions=read,create,list\nids=ttcp_123;type=target;actions=list\ntype=credential;actions=create';
44+
completionSource = createGrantCompletionSource(
45+
this.args.grantsSchema,
46+
this.completionTranslatedStrings,
47+
);
1948

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

54+
customExtensions = [
55+
autocompletion({
56+
override: [this.completionSource],
57+
// Trigger autocompletion when the user completes a grant field (which we labeled as keywords)
58+
activateOnCompletion: (completion) => completion.type === 'keyword',
59+
}),
60+
keymap.of(completionKeymap),
61+
];
62+
63+
get grantStrings() {
64+
return this.grantStringsText
65+
.split('\n')
66+
.map((grantString) => grantString.trim())
67+
.filter(Boolean);
68+
}
69+
2370
/**
2471
* Returns the formatted export based on the selected export option.
2572
* @type {string}
@@ -37,7 +84,7 @@ export default class FormRoleEditGrants extends Component {
3784
*/
3885
get terraformFormattedExport() {
3986
let formatted = `grant_strings = [ \n`;
40-
this.grantStringLines.split('\n').forEach((line) => {
87+
this.grantStringsText.split('\n').forEach((line) => {
4188
formatted += ` "${line}",\n`;
4289
});
4390
formatted += `]\n`;
@@ -50,7 +97,7 @@ export default class FormRoleEditGrants extends Component {
5097
*/
5198
get nativeHclFormattedExport() {
5299
let formatted = `[ \n`;
53-
this.grantStringLines.split('\n').forEach((line) => {
100+
this.grantStringsText.split('\n').forEach((line) => {
54101
formatted += ` "${line}",\n`;
55102
});
56103
formatted += `]\n`;
@@ -59,6 +106,14 @@ export default class FormRoleEditGrants extends Component {
59106

60107
// =actions
61108

109+
@action
110+
onInput(value, view) {
111+
this.grantStringsText = value;
112+
113+
const line = view.state.doc.lineAt(view.state.selection.main.head);
114+
this.currentLineText = line.text;
115+
}
116+
62117
/**
63118
* Toggles the export options flyout open and closed.
64119
*/
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/**
2+
* Copyright IBM Corp. 2021, 2026
3+
* SPDX-License-Identifier: BUSL-1.1
4+
*/
5+
6+
import Controller from '@ember/controller';
7+
import { service } from '@ember/service';
8+
import { action } from '@ember/object';
9+
import { loading } from 'ember-loading';
10+
import { notifySuccess, notifyError } from 'core/decorators/notify';
11+
12+
export default class ScopesScopeRolesRoleEditGrantsController extends Controller {
13+
@service router;
14+
15+
/**
16+
* Save grant strings on a role.
17+
* @param {RoleModel} role
18+
* @param {[string]} grantStrings
19+
*/
20+
@action
21+
@loading
22+
@notifyError(({ message }) => message, { catch: true })
23+
@notifySuccess('notifications.save-success')
24+
async save(role, grantStrings) {
25+
await role.saveGrantStrings(grantStrings);
26+
this.router.replaceWith('scopes.scope.roles.role.grants');
27+
}
28+
29+
/**
30+
* Rollback changes on a role.
31+
* @param {RoleModel} role
32+
*/
33+
@action
34+
async cancel(role) {
35+
role.rollbackAttributes();
36+
this.router.replaceWith('scopes.scope.roles.role.grants');
37+
}
38+
}

‎ui/admin/app/routes/scopes/scope/roles/role/edit-grants.js‎

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,33 @@
44
*/
55

66
import Route from '@ember/routing/route';
7+
import { service } from '@ember/service';
78

8-
export default class ScopesScopeRolesRoleEditGrantsRoute extends Route {}
9+
export default class ScopesScopeRolesRoleEditGrantsRoute extends Route {
10+
@service store;
11+
12+
async model() {
13+
const role = this.modelFor('scopes.scope.roles.role');
14+
let grantsSchema = { resource_types: [] };
15+
16+
try {
17+
grantsSchema = await this.fetchGrantsSchema();
18+
} catch {
19+
// TODO: Return an error message
20+
}
21+
22+
return { role, grantsSchema };
23+
}
24+
25+
async fetchGrantsSchema() {
26+
const adapter = this.store.adapterFor('application');
27+
const url = `${adapter.host ?? ''}/grants-schema.json`;
28+
const response = await fetch(url);
29+
30+
if (!response.ok) {
31+
throw new Error('Failed to fetch grants schema');
32+
}
33+
34+
return response.json();
35+
}
36+
}

‎ui/admin/app/styles/app.scss‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -996,3 +996,9 @@
996996
}
997997
}
998998
}
999+
1000+
.grant-editor {
1001+
.hds-code-editor__editor {
1002+
height: 200px;
1003+
}
1004+
}

‎ui/admin/app/templates/scopes/scope/roles/role/edit-grants.hbs‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,12 @@
2828
</page.header>
2929

3030
<page.body>
31-
<Form::Role::EditGrants />
31+
<Form::Role::EditGrants
32+
@model={{@model.role}}
33+
@grantsSchema={{@model.grantsSchema}}
34+
@submit={{fn this.save @model.role}}
35+
@cancel={{fn this.cancel @model.role}}
36+
/>
3237
</page.body>
3338

3439
</Rose::Layout::Page>

0 commit comments

Comments
 (0)