feat: 🎸 add export options flyout (#3198)

* feat: 🎸 add export options flyout

* fix: 🐛 update id to be ids

* test: 💍 add a11y ignores

* test: 💍 fix tests for grant string format

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-18524
This commit is contained in:
Lisbet Alvarez authored and Zhihe Li committed 2026-05-14 15:21:28 -04:00
1 parent 759c1f5555
commit add2a9d6c4
8 files changed
+211 -10

No files matched your search

@@ -70,3 +70,4 @@ hide-errors: Hide Errors
edit-worker-filter: Edit Worker Filter
add-worker-filter: Add Worker Filter
open: Open
download: Download
@@ -680,6 +680,11 @@ role:
insert-resource-types: insert resource types
insert-id: insert id
insert-resource-ids: insert resource ids
export:
format: Format
options:
terraform: Terraform
native-hcl: Native HCL
scope:
title: Scope
title_plural: Scopes
@@ -25,16 +25,67 @@
</Hds::Accordion>
</Hds::Layout::Grid>
{{#if this.showExportOptionsFlyout}}
<Hds::Flyout
id='export-options-flyout'
@size='large'
@onClose={{this.toggleExportOptionsFlyout}}
data-test-export-options-flyout
as |M|
>
<M.Header>{{t 'actions.export'}}</M.Header>
<M.Body>
<Hds::Form::Select::Field
name='export-format'
@width='100%'
{{on 'change' (set-from-event this 'selectedExportOption')}}
as |F|
>
<F.Label>{{t 'resources.role.edit-grants.export.format'}}</F.Label>
<F.Options>
{{#each this.exportOptions as |type|}}
<option
value={{type}}
selected={{eq type this.selectedExportOption}}
>
{{t (concat 'resources.role.edit-grants.export.options.' type)}}
</option>
{{/each}}
</F.Options>
</Hds::Form::Select::Field>
<Hds::CodeBlock
@ariaLabel='formatted-export'
@hasCopyButton={{true}}
@language='hcl'
@value={{this.formattedExport}}
/>
<Hds::Button
class='margin-top-l'
@text={{t 'actions.download'}}
@icon='download'
@color='secondary'
{{on 'click' this.downloadFormattedExport}}
/>
</M.Body>
</Hds::Flyout>
{{/if}}
<form.actions class='margin-top-l'>
<:actions>
<Hds::ButtonSet>
<Hds::Button type='submit' @text={{t 'actions.save'}} />
<Hds::Button @text={{t 'actions.cancel'}} @color='secondary' />
<Hds::Button
@text={{t 'actions.export'}}
@color='tertiary'
@icon='upload'
/>
{{#if this.grantStringLines}}
<Hds::Button
@text={{t 'actions.export'}}
@color='tertiary'
@icon='upload'
data-test-export-grants-btn
{{on 'click' this.toggleExportOptionsFlyout}}
/>
{{/if}}
</Hds::ButtonSet>
</:actions>
</form.actions>
@@ -0,0 +1,92 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
export default class FormRoleEditGrants extends Component {
// =attributes
exportOptionsMap = { terraform: 'terraform', nativeHCL: 'native-hcl' };
exportOptions = Object.values(this.exportOptionsMap);
// TODO: Replace with actual grant lines from code editor once implemented.
grantStringLines =
'ids=hc_123;type=host-catalog;actions=read,create,list\nids=ttcp_123;type=target;actions=list\ntype=credential;actions=create';
@tracked showExportOptionsFlyout = false;
@tracked selectedExportOption = this.exportOptions[0];
/**
* Returns the formatted export based on the selected export option.
* @type {string}
*/
get formattedExport() {
if (this.selectedExportOption === this.exportOptionsMap.terraform) {
return this.terraformFormattedExport;
}
return this.nativeHclFormattedExport;
}
/**
* Formats grant strings into a Terraform format.
* @type {string}
*/
get terraformFormattedExport() {
let formatted = `grant_strings = [ \n`;
this.grantStringLines.split('\n').forEach((line) => {
formatted += ` "${line}",\n`;
});
formatted += `]\n`;
return formatted;
}
/**
* Formats grant strings into a Native HCL format.
* @type {string}
*/
get nativeHclFormattedExport() {
let formatted = `[ \n`;
this.grantStringLines.split('\n').forEach((line) => {
formatted += ` "${line}",\n`;
});
formatted += `]\n`;
return formatted;
}
// =actions
/**
* Toggles the export options flyout open and closed.
*/
@action
toggleExportOptionsFlyout() {
this.showExportOptionsFlyout = !this.showExportOptionsFlyout;
}
/**
* Triggers the download of the formatted export as a file.
*/
@action
downloadFormattedExport() {
const fileName =
this.selectedExportOption === this.exportOptionsMap.terraform
? 'grant-strings.tf'
: 'grant-strings.hcl';
const blob = new Blob([this.formattedExport], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = fileName;
const flyoutElement = document.getElementById('export-options-flyout');
flyoutElement.appendChild(a);
a.click();
flyoutElement.removeChild(a);
URL.revokeObjectURL(url);
}
}
@@ -24,9 +24,9 @@ export default class GrantStringFormatsIndex extends Component {
[this.stringFormatTypes.resourceType]:
`type=<${this.intl.t('resources.role.edit-grants.string-formats.insert-resource-types')}>;actions=<${this.intl.t('resources.role.edit-grants.string-formats.insert-actions')}>`,
[this.stringFormatTypes.resource]:
`id=<${this.intl.t('resources.role.edit-grants.string-formats.insert-resource-ids')}>;actions=<${this.intl.t('resources.role.edit-grants.string-formats.insert-actions')}>`,
`ids=<${this.intl.t('resources.role.edit-grants.string-formats.insert-resource-ids')}>;actions=<${this.intl.t('resources.role.edit-grants.string-formats.insert-actions')}>`,
[this.stringFormatTypes.pinnedId]:
`id=<${this.intl.t('resources.role.edit-grants.string-formats.insert-id')}>;type=<${this.intl.t('resources.role.edit-grants.string-formats.insert-resource-types')}>;actions=<${this.intl.t('resources.role.edit-grants.string-formats.insert-actions')}>`,
`ids=<${this.intl.t('resources.role.edit-grants.string-formats.insert-id')}>;type=<${this.intl.t('resources.role.edit-grants.string-formats.insert-resource-types')}>;actions=<${this.intl.t('resources.role.edit-grants.string-formats.insert-actions')}>`,
};
@tracked selectedFormatType = this.stringFormatTypes.resourceType;
@@ -4,7 +4,7 @@
*/
import { module, test } from 'qunit';
import { visit, click, currentURL } from '@ember/test-helpers';
import { visit, click, currentURL, select } from '@ember/test-helpers';
import { setupApplicationTest } from 'admin/tests/helpers';
import * as selectors from './selectors';
import { setRunOptions } from 'ember-a11y-testing/test-support';
@@ -76,4 +76,50 @@ module('Acceptance | roles/edit grants', function (hooks) {
assert.dom(selectors.MANAGE_DROPDOWN_EDIT_GRANTS).doesNotExist();
assert.strictEqual(currentURL(), urls.role);
});
test('user can export a terraform formatted grant string', async function (assert) {
setRunOptions({
rules: {
'color-contrast': {
// [ember-a11y-ignore]: axe rule "color-contrast" automatically ignored on 2026-03-17
enabled: false,
},
},
});
await visit(urls.role);
await click(selectors.MANAGE_DROPDOWN_ROLES);
await click(selectors.MANAGE_DROPDOWN_EDIT_GRANTS);
await click(selectors.EXPORT_GRANTS_BTN);
assert.dom(selectors.EXPORT_OPTIONS_FLYOUT).isVisible();
assert.dom(selectors.EXPORT_FORMAT_SELECT).hasValue('terraform');
// TODO: Update to check for the actual formatted grant string.
assert
.dom(selectors.FORMATTED_EXPORT_CODE_BLOCK)
.includesText('grant_strings');
});
test('user can export a hcl formatted grant string', async function (assert) {
setRunOptions({
rules: {
'color-contrast': {
// [ember-a11y-ignore]: axe rule "color-contrast" automatically ignored on 2026-03-17
enabled: false,
},
},
});
await visit(urls.role);
await click(selectors.MANAGE_DROPDOWN_ROLES);
await click(selectors.MANAGE_DROPDOWN_EDIT_GRANTS);
await click(selectors.EXPORT_GRANTS_BTN);
await select(selectors.EXPORT_FORMAT_SELECT, 'native-hcl');
assert.dom(selectors.EXPORT_OPTIONS_FLYOUT).isVisible();
assert.dom(selectors.EXPORT_FORMAT_SELECT).hasValue('native-hcl');
// TODO: Update to check for the actual formatted grant string.
});
});
@@ -60,6 +60,12 @@ export const FIELD_GRANT = 'form:nth-child(2) [name="grant"]';
export const FIELD_GRANT_VALUE = 'ids=123,action=delete';
export const FIELD_GRANT_REMOVE_BTN = 'form:nth-child(2) button[type="button"]';
export const GRANTS_FORM = 'form:nth-child(2)';
export const EXPORT_OPTIONS_FLYOUT = '[data-test-export-options-flyout]';
export const EXPORT_FORMAT_SELECT =
'[data-test-export-options-flyout] [name="export-format"]';
export const FORMATTED_EXPORT_CODE_BLOCK =
'[data-test-export-options-flyout] .hds-code-block code';
// We need to use a different selector for the grants form because the
// grants form has multiple submit buttons
export const SAVE_BTN = '.rose-form-actions [type=submit]';
export const EXPORT_GRANTS_BTN = '[data-test-export-grants-btn]';
@@ -41,7 +41,7 @@ module(
assert.dom(PINNED_ID_OPTION).isNotChecked();
assert
.dom(STRING_FORMAT_FIELD)
.hasValue('id=<insert resource ids>;actions=<insert actions>');
.hasValue('ids=<insert resource ids>;actions=<insert actions>');
});
test('it renders string format when type is changed to pinned id', async function (assert) {
@@ -55,7 +55,7 @@ module(
assert
.dom(STRING_FORMAT_FIELD)
.hasValue(
'id=<insert id>;type=<insert resource types>;actions=<insert actions>',
'ids=<insert id>;type=<insert resource types>;actions=<insert actions>',
);
});
},