From 4a6007742d1c2227f15207e785a9ee99649b9e55 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Wed, 13 May 2026 15:57:54 -0600 Subject: [PATCH] Backport [UI][VAULT-44640] - Extract shared code snippets logic to snippet service into ce/main (#14769) * no-op commit * [UI][VAULT-44640] - Extract shared code snippets logic to snippet service (#14760) * feat(snippet-service): extract shared logic between namespace wizard and v2 forms * feat(snippet-service): update namespace wizard and forms apply to use snippet service. * feat(tests): unit tests for snippet service * fix(copyright): add copyright header for snippet constants file --------- Co-authored-by: Nina Bucholtz --- ui/app/components/form/v2/apply.hbs | 2 +- ui/app/components/form/v2/apply.ts | 28 +-- .../wizard/namespaces/namespace-wizard.ts | 2 +- ui/app/components/wizard/namespaces/step-3.ts | 42 ++--- ui/app/services/snippet.ts | 50 ++++++ ui/app/utils/constants/snippet.ts | 10 ++ ui/tests/unit/services/snippet-test.js | 160 ++++++++++++++++++ 7 files changed, 257 insertions(+), 37 deletions(-) create mode 100644 ui/app/services/snippet.ts create mode 100644 ui/app/utils/constants/snippet.ts create mode 100644 ui/tests/unit/services/snippet-test.js diff --git a/ui/app/components/form/v2/apply.hbs b/ui/app/components/form/v2/apply.hbs index 386a34d04b..e3d424cdde 100644 --- a/ui/app/components/form/v2/apply.hbs +++ b/ui/app/components/form/v2/apply.hbs @@ -40,7 +40,7 @@ {{#if (eq this.creationMethodChoice this.methods.APICLI)}} - + {{else}} void; } -export enum CreationMethod { - TERRAFORM = 'Terraform automation', - APICLI = 'API/CLI', - UI = 'Vault UI workflow', -} - interface CreationMethodChoice { icon: string; label: CreationMethod; @@ -36,9 +31,7 @@ interface CreationMethodChoice { export default class FormV2Apply extends Component { @service declare readonly namespace: NamespaceService; - - @tracked creationMethodChoice = CreationMethod.TERRAFORM; - @tracked selectedTabIdx = 0; + @service declare readonly snippet: SnippetService; methods = CreationMethod; @@ -64,6 +57,14 @@ export default class FormV2Apply extends Component { }, ]; + get creationMethodChoice() { + return this.snippet.creationMethodChoice; + } + + get selectedTabIdx() { + return this.snippet.selectedTabIdx; + } + get requestData() { const { payload } = this.args.form; // payload has a top level key -- we need the actual data object for creating the snippets @@ -93,6 +94,11 @@ export default class FormV2Apply extends Component { @action onChange(choice: CreationMethodChoice) { - this.creationMethodChoice = choice.label; + this.snippet.setCreationMethod(choice.label, this.tfSnippet, this.customTabs); + } + + @action + onTabChange(idx: number) { + this.snippet.setSelectedTab(idx, this.tfSnippet, this.customTabs); } } diff --git a/ui/app/components/wizard/namespaces/namespace-wizard.ts b/ui/app/components/wizard/namespaces/namespace-wizard.ts index b41dbe066e..e519d19ce5 100644 --- a/ui/app/components/wizard/namespaces/namespace-wizard.ts +++ b/ui/app/components/wizard/namespaces/namespace-wizard.ts @@ -8,7 +8,7 @@ import { action } from '@ember/object'; import { tracked } from '@glimmer/tracking'; import Component from '@glimmer/component'; import { SecurityPolicy } from 'vault/components/wizard/namespaces/step-1'; -import { CreationMethod } from 'vault/components/wizard/namespaces/step-3'; +import { CreationMethod } from 'vault/utils/constants/snippet'; import { WIZARD_ID_MAP } from 'vault/utils/constants/wizard'; import type ApiService from 'vault/services/api'; diff --git a/ui/app/components/wizard/namespaces/step-3.ts b/ui/app/components/wizard/namespaces/step-3.ts index c9a3f95851..e08ac4fcc1 100644 --- a/ui/app/components/wizard/namespaces/step-3.ts +++ b/ui/app/components/wizard/namespaces/step-3.ts @@ -5,10 +5,11 @@ import Component from '@glimmer/component'; import { service } from '@ember/service'; -import { tracked } from '@glimmer/tracking'; import { action } from '@ember/object'; import { SecurityPolicy } from './step-1'; import type NamespaceService from 'vault/services/namespace'; +import type SnippetService from 'vault/services/snippet'; +import { CreationMethod } from 'vault/utils/constants/snippet'; import { generateApiSnippet, generateCliSnippet, @@ -25,12 +26,6 @@ interface Args { updateWizardState: (key: string, value: unknown) => void; } -export enum CreationMethod { - TERRAFORM = 'Terraform automation', - APICLI = 'API/CLI', - UI = 'Vault UI workflow', -} - interface CreationMethodChoice { icon: string; label: CreationMethod; @@ -40,15 +35,14 @@ interface CreationMethodChoice { export default class WizardNamespacesStep3 extends Component { @service declare readonly namespace: NamespaceService; - @tracked creationMethodChoice: CreationMethod; - @tracked selectedTabIdx = 0; + @service declare readonly snippet: SnippetService; methods = CreationMethod; policy = SecurityPolicy; constructor(owner: unknown, args: Args) { super(owner, args); - this.creationMethodChoice = this.args.wizardState.creationMethod || CreationMethod.TERRAFORM; + this.snippet.reset(this.args.wizardState.creationMethod || CreationMethod.TERRAFORM); } creationMethodOptions: CreationMethodChoice[] = [ @@ -73,6 +67,14 @@ export default class WizardNamespacesStep3 extends Component { }, ]; + get creationMethodChoice() { + return this.snippet.creationMethodChoice; + } + + get selectedTabIdx() { + return this.snippet.selectedTabIdx; + } + get tfSnippet() { const { namespacePaths } = this.args.wizardState; return generateTerraformSnippet(namespacePaths, this.namespace.path); @@ -96,28 +98,20 @@ export default class WizardNamespacesStep3 extends Component { @action onChange(choice: CreationMethodChoice) { - this.creationMethodChoice = choice.label; + this.snippet.setCreationMethod(choice.label, this.tfSnippet, this.customTabs); this.args.updateWizardState('creationMethod', choice.label); - // Update the code snippet whenever the creation method changes - this.updateCodeSnippet(); + this.args.updateWizardState('codeSnippet', this.snippet.codeSnippet); } @action onTabChange(idx: number) { - this.selectedTabIdx = idx; - - // Update the code snippet whenever the tab changes - this.updateCodeSnippet(); + this.snippet.setSelectedTab(idx, this.tfSnippet, this.customTabs); + this.args.updateWizardState('codeSnippet', this.snippet.codeSnippet); } - // Update the wizard state with the current code snippet @action updateCodeSnippet() { - if (this.creationMethodChoice === CreationMethod.TERRAFORM) { - this.args.updateWizardState('codeSnippet', this.tfSnippet); - } else if (this.creationMethodChoice === CreationMethod.APICLI) { - const snippet = this.customTabs[this.selectedTabIdx]?.snippet; - this.args.updateWizardState('codeSnippet', snippet); - } + this.snippet.persistSnippet(this.tfSnippet, this.customTabs); + this.args.updateWizardState('codeSnippet', this.snippet.codeSnippet); } } diff --git a/ui/app/services/snippet.ts b/ui/app/services/snippet.ts new file mode 100644 index 0000000000..2f34ce542b --- /dev/null +++ b/ui/app/services/snippet.ts @@ -0,0 +1,50 @@ +/** + * Copyright IBM Corp. 2016, 2026 + * SPDX-License-Identifier: BUSL-1.1 + */ + +import Service from '@ember/service'; +import { tracked } from '@glimmer/tracking'; +import { action } from '@ember/object'; +import { CreationMethod } from 'vault/utils/constants/snippet'; + +export interface SnippetTab { + key: string; + label: string; + snippet: string; +} + +export default class SnippetService extends Service { + @tracked selectedTabIdx = 0; + @tracked creationMethodChoice: CreationMethod = CreationMethod.TERRAFORM; + @tracked codeSnippet: string | null = null; + + @action + setCreationMethod(choice: CreationMethod, tfSnippet: string, customTabs: SnippetTab[]) { + this.creationMethodChoice = choice; + this.persistSnippet(tfSnippet, customTabs); + } + + @action + setSelectedTab(idx: number, tfSnippet: string, customTabs: SnippetTab[]) { + this.selectedTabIdx = idx; + this.persistSnippet(tfSnippet, customTabs); + } + + @action + persistSnippet(tfSnippet: string, customTabs: SnippetTab[]) { + if (this.creationMethodChoice === CreationMethod.TERRAFORM) { + this.codeSnippet = tfSnippet; + } else if (this.creationMethodChoice === CreationMethod.APICLI) { + this.codeSnippet = customTabs[this.selectedTabIdx]?.snippet ?? null; + } else { + this.codeSnippet = null; + } + } + + reset(initialChoice: CreationMethod = CreationMethod.TERRAFORM) { + this.selectedTabIdx = 0; + this.creationMethodChoice = initialChoice; + this.codeSnippet = null; + } +} diff --git a/ui/app/utils/constants/snippet.ts b/ui/app/utils/constants/snippet.ts new file mode 100644 index 0000000000..65a37f295b --- /dev/null +++ b/ui/app/utils/constants/snippet.ts @@ -0,0 +1,10 @@ +/** + * Copyright IBM Corp. 2016, 2026 + * SPDX-License-Identifier: BUSL-1.1 + */ + +export enum CreationMethod { + TERRAFORM = 'Terraform automation', + APICLI = 'API/CLI', + UI = 'Vault UI workflow', +} diff --git a/ui/tests/unit/services/snippet-test.js b/ui/tests/unit/services/snippet-test.js new file mode 100644 index 0000000000..26143822bb --- /dev/null +++ b/ui/tests/unit/services/snippet-test.js @@ -0,0 +1,160 @@ +/** + * Copyright IBM Corp. 2016, 2026 + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; +import { CreationMethod } from 'vault/utils/constants/snippet'; + +const TF_SNIPPET = 'resource "vault_mount" "example" {}'; +const API_SNIPPET = 'curl --header "X-Vault-Token: $VAULT_TOKEN" $VAULT_ADDR/v1/test'; +const CLI_SNIPPET = 'vault write test/path key=value'; + +const CUSTOM_TABS = [ + { key: 'api', label: 'API', snippet: API_SNIPPET }, + { key: 'cli', label: 'CLI', snippet: CLI_SNIPPET }, +]; + +module('Unit | Service | snippet', function (hooks) { + setupTest(hooks); + + hooks.beforeEach(function () { + this.service = this.owner.lookup('service:snippet'); + }); + + module('default state', function () { + test('selectedTabIdx defaults to 0', function (assert) { + assert.strictEqual(this.service.selectedTabIdx, 0); + }); + + test('creationMethodChoice defaults to TERRAFORM', function (assert) { + assert.strictEqual(this.service.creationMethodChoice, CreationMethod.TERRAFORM); + }); + + test('codeSnippet defaults to null', function (assert) { + assert.strictEqual(this.service.codeSnippet, null); + }); + }); + + module('#reset', function () { + test('resets service state to defaults', function (assert) { + this.service.selectedTabIdx = 1; + this.service.creationMethodChoice = CreationMethod.APICLI; + this.service.codeSnippet = TF_SNIPPET; + + this.service.reset(); + + assert.strictEqual(this.service.selectedTabIdx, 0); + assert.strictEqual(this.service.creationMethodChoice, CreationMethod.TERRAFORM); + assert.strictEqual(this.service.codeSnippet, null); + }); + + test('accepts an initial creation method', function (assert) { + this.service.reset(CreationMethod.APICLI); + + assert.strictEqual(this.service.creationMethodChoice, CreationMethod.APICLI); + assert.strictEqual(this.service.selectedTabIdx, 0); + assert.strictEqual(this.service.codeSnippet, null); + }); + }); + + module('#persistSnippet', function () { + test('sets codeSnippet to tfSnippet when method is TERRAFORM', function (assert) { + this.service.creationMethodChoice = CreationMethod.TERRAFORM; + + this.service.persistSnippet(TF_SNIPPET, CUSTOM_TABS); + + assert.strictEqual(this.service.codeSnippet, TF_SNIPPET); + }); + + test('sets codeSnippet to the selected tab snippet when method is APICLI', function (assert) { + this.service.creationMethodChoice = CreationMethod.APICLI; + this.service.selectedTabIdx = 0; + + this.service.persistSnippet(TF_SNIPPET, CUSTOM_TABS); + + assert.strictEqual(this.service.codeSnippet, API_SNIPPET); + }); + + test('sets codeSnippet to the CLI tab when selectedTabIdx is 1', function (assert) { + this.service.creationMethodChoice = CreationMethod.APICLI; + this.service.selectedTabIdx = 1; + + this.service.persistSnippet(TF_SNIPPET, CUSTOM_TABS); + + assert.strictEqual(this.service.codeSnippet, CLI_SNIPPET); + }); + + test('sets codeSnippet to null when method is UI', function (assert) { + this.service.creationMethodChoice = CreationMethod.UI; + + this.service.persistSnippet(TF_SNIPPET, CUSTOM_TABS); + + assert.strictEqual(this.service.codeSnippet, null); + }); + + test('sets codeSnippet to null when APICLI tab index is out of range', function (assert) { + this.service.creationMethodChoice = CreationMethod.APICLI; + this.service.selectedTabIdx = 99; + + this.service.persistSnippet(TF_SNIPPET, CUSTOM_TABS); + + assert.strictEqual(this.service.codeSnippet, null); + }); + }); + + module('#setCreationMethod', function () { + test('updates creationMethodChoice', function (assert) { + this.service.setCreationMethod(CreationMethod.APICLI, TF_SNIPPET, CUSTOM_TABS); + + assert.strictEqual(this.service.creationMethodChoice, CreationMethod.APICLI); + }); + + test('persists the correct snippet after changing to TERRAFORM', function (assert) { + this.service.setCreationMethod(CreationMethod.TERRAFORM, TF_SNIPPET, CUSTOM_TABS); + + assert.strictEqual(this.service.codeSnippet, TF_SNIPPET); + }); + + test('persists the correct snippet after changing to APICLI', function (assert) { + this.service.selectedTabIdx = 0; + this.service.setCreationMethod(CreationMethod.APICLI, TF_SNIPPET, CUSTOM_TABS); + + assert.strictEqual(this.service.codeSnippet, API_SNIPPET); + }); + + test('clears codeSnippet when changing to UI', function (assert) { + this.service.codeSnippet = TF_SNIPPET; + this.service.setCreationMethod(CreationMethod.UI, TF_SNIPPET, CUSTOM_TABS); + + assert.strictEqual(this.service.codeSnippet, null); + }); + }); + + module('#setSelectedTab', function () { + test('updates selectedTabIdx', function (assert) { + this.service.creationMethodChoice = CreationMethod.APICLI; + + this.service.setSelectedTab(1, TF_SNIPPET, CUSTOM_TABS); + + assert.strictEqual(this.service.selectedTabIdx, 1); + }); + + test('persists the snippet for the newly selected tab', function (assert) { + this.service.creationMethodChoice = CreationMethod.APICLI; + + this.service.setSelectedTab(1, TF_SNIPPET, CUSTOM_TABS); + + assert.strictEqual(this.service.codeSnippet, CLI_SNIPPET); + }); + + test('persists tfSnippet if method is still TERRAFORM when tab changes', function (assert) { + this.service.creationMethodChoice = CreationMethod.TERRAFORM; + + this.service.setSelectedTab(1, TF_SNIPPET, CUSTOM_TABS); + + assert.strictEqual(this.service.codeSnippet, TF_SNIPPET); + }); + }); +});