diff --git a/addons/core/translations/actions/en-us.yaml b/addons/core/translations/actions/en-us.yaml index 5bd43c30d..49f509ee1 100644 --- a/addons/core/translations/actions/en-us.yaml +++ b/addons/core/translations/actions/en-us.yaml @@ -70,3 +70,4 @@ hide-errors: Hide Errors edit-worker-filter: Edit Worker Filter add-worker-filter: Add Worker Filter open: Open +download: Download diff --git a/addons/core/translations/resources/en-us.yaml b/addons/core/translations/resources/en-us.yaml index 2b2f70318..147032891 100644 --- a/addons/core/translations/resources/en-us.yaml +++ b/addons/core/translations/resources/en-us.yaml @@ -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 diff --git a/ui/admin/app/components/form/role/edit-grants/index.hbs b/ui/admin/app/components/form/role/edit-grants/index.hbs index d3ee4c92d..c991e8b65 100644 --- a/ui/admin/app/components/form/role/edit-grants/index.hbs +++ b/ui/admin/app/components/form/role/edit-grants/index.hbs @@ -25,16 +25,67 @@ + {{#if this.showExportOptionsFlyout}} + + {{t 'actions.export'}} + + + {{t 'resources.role.edit-grants.export.format'}} + + {{#each this.exportOptions as |type|}} + + {{/each}} + + + + + + + + + {{/if}} + <:actions> - + {{#if this.grantStringLines}} + + {{/if}} diff --git a/ui/admin/app/components/form/role/edit-grants/index.js b/ui/admin/app/components/form/role/edit-grants/index.js new file mode 100644 index 000000000..52f4463b7 --- /dev/null +++ b/ui/admin/app/components/form/role/edit-grants/index.js @@ -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); + } +} diff --git a/ui/admin/app/components/grant-string-formats/index.js b/ui/admin/app/components/grant-string-formats/index.js index 2915008b1..fc6fc5df4 100644 --- a/ui/admin/app/components/grant-string-formats/index.js +++ b/ui/admin/app/components/grant-string-formats/index.js @@ -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; diff --git a/ui/admin/tests/acceptance/roles/edit-grants-test.js b/ui/admin/tests/acceptance/roles/edit-grants-test.js index d625f264a..0365176e2 100644 --- a/ui/admin/tests/acceptance/roles/edit-grants-test.js +++ b/ui/admin/tests/acceptance/roles/edit-grants-test.js @@ -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. + }); }); diff --git a/ui/admin/tests/acceptance/roles/selectors.js b/ui/admin/tests/acceptance/roles/selectors.js index dc50628e6..dc558ee03 100644 --- a/ui/admin/tests/acceptance/roles/selectors.js +++ b/ui/admin/tests/acceptance/roles/selectors.js @@ -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]'; diff --git a/ui/admin/tests/integration/components/grant-string-formats/index-test.js b/ui/admin/tests/integration/components/grant-string-formats/index-test.js index 6f44637ef..0c2f31ec1 100644 --- a/ui/admin/tests/integration/components/grant-string-formats/index-test.js +++ b/ui/admin/tests/integration/components/grant-string-formats/index-test.js @@ -41,7 +41,7 @@ module( assert.dom(PINNED_ID_OPTION).isNotChecked(); assert .dom(STRING_FORMAT_FIELD) - .hasValue('id=;actions='); + .hasValue('ids=;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=;type=;actions=', + 'ids=;type=;actions=', ); }); },