diff --git a/addons/core/translations/resources/en-us.yaml b/addons/core/translations/resources/en-us.yaml
index 259824292..91e85111e 100644
--- a/addons/core/translations/resources/en-us.yaml
+++ b/addons/core/translations/resources/en-us.yaml
@@ -254,6 +254,7 @@ target:
create: New Target
delete: Delete Target
add-host-sets: Add Host Sets
+ add-credential-libraries: Add Credential Libraries
types:
tcp: TCP
host-set:
@@ -272,6 +273,9 @@ target:
welcome:
title: Welcome to Credential Libraries
description: No credential libraries in this target.
+ add:
+ title: Add Credential Libraries
+ description: Select credential libraries to assign to this target.
credential-store:
title: Credential Store
title_plural: Credential Stores
diff --git a/ui/admin/app/components/form/target/add-credential-libraries/index.hbs b/ui/admin/app/components/form/target/add-credential-libraries/index.hbs
new file mode 100644
index 000000000..1c76a5bdb
--- /dev/null
+++ b/ui/admin/app/components/form/target/add-credential-libraries/index.hbs
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+ {{t 'form.id.label'}}
+ {{t 'form.name.label'}}
+
+
+
+ {{#each this.filteredCredentialLibraries as |credentialLibrary|}}
+
+
+
+
+ {{credentialLibrary.name}}
+
+ {{/each}}
+
+
+
\ No newline at end of file
diff --git a/ui/admin/app/components/form/target/add-credential-libraries/index.js b/ui/admin/app/components/form/target/add-credential-libraries/index.js
new file mode 100644
index 000000000..b8177dab0
--- /dev/null
+++ b/ui/admin/app/components/form/target/add-credential-libraries/index.js
@@ -0,0 +1,55 @@
+import Component from '@glimmer/component';
+import { computed, action } from '@ember/object';
+import { A } from '@ember/array';
+
+export default class FormTargetAddCredentialLibrariesIndexComponent extends Component {
+ // =properties
+
+ /**
+ * Array of selected credential library IDs.
+ * @type {EmberArray}
+ */
+ selectedCredentialLibraryIDs = A();
+
+ /**
+ * Filter out credential libraries not already added to the target.
+ * @type {[CredentialLibraryModel]}
+ */
+ @computed(
+ 'args.{credentialLibraries.[],model.application_credential_library_ids.[]}'
+ )
+ get filteredCredentialLibraries() {
+ // Get IDs for credential libraries already added to the current target
+ const currentCredentialLibraryIDs =
+ this.args.model.application_credential_library_ids.map(
+ (credentialLibrary) => credentialLibrary.value
+ );
+ const notAddedCredentialLibraries = this.args.credentialLibraries.filter(
+ ({ id }) => !currentCredentialLibraryIDs.includes(id)
+ );
+ return notAddedCredentialLibraries;
+ }
+
+ // =actions
+
+ /**
+ * Add/Remove credential library to current selection
+ * @param CredentialLibraryModel
+ */
+ @action
+ toggleCredentialLibrary(credentialLibrary) {
+ if (!this.selectedCredentialLibraryIDs.includes(credentialLibrary.id)) {
+ this.selectedCredentialLibraryIDs.addObject(credentialLibrary.id);
+ } else {
+ this.selectedCredentialLibraryIDs.removeObject(credentialLibrary.id);
+ }
+ }
+
+ /**
+ * Submit selected credential library ids
+ */
+ @action
+ submit(fn) {
+ fn(this.selectedCredentialLibraryIDs);
+ }
+}
diff --git a/ui/admin/app/router.js b/ui/admin/app/router.js
index 8638e4e48..6b70bd999 100644
--- a/ui/admin/app/router.js
+++ b/ui/admin/app/router.js
@@ -66,6 +66,7 @@ Router.map(function () {
this.route('host-sets', function () {});
this.route('add-host-sets');
this.route('credential-libraries');
+ this.route('add-credential-libraries');
});
this.route('new');
});
diff --git a/ui/admin/app/routes/scopes/scope/targets/target/add-credential-libraries.js b/ui/admin/app/routes/scopes/scope/targets/target/add-credential-libraries.js
new file mode 100644
index 000000000..f2cb1eca2
--- /dev/null
+++ b/ui/admin/app/routes/scopes/scope/targets/target/add-credential-libraries.js
@@ -0,0 +1,94 @@
+import Route from '@ember/routing/route';
+import { inject as service } from '@ember/service';
+import { all } from 'rsvp';
+import { action } from '@ember/object';
+import loading from 'ember-loading/decorator';
+import { notifySuccess, notifyError } from 'core/decorators/notify';
+
+export default class ScopesScopeTargetsTargetAddCredentialLibrariesRoute extends Route {
+ // =services
+
+ @service intl;
+ @service notify;
+
+ // =methods
+
+ /**
+ * Empty out any previously loaded credential libraries.
+ */
+ beforeModel() {
+ this.store.unloadAll('credential-library');
+ }
+
+ /**
+ * Returns the current target and all credential libraries.
+ * @return {{target: TargetModel, credentialLibraries: [CredentialLibraryModel]}}
+ */
+ async model() {
+ const target = this.modelFor('scopes.scope.targets.target');
+ const { id: scope_id } = this.modelFor('scopes.scope');
+ const credentialStores = await this.store.query('credential-store', {
+ scope_id,
+ });
+ await all(
+ credentialStores.map(({ id: credential_store_id }) =>
+ this.store.query('credential-library', { credential_store_id })
+ )
+ );
+ const credentialLibraries = this.store.peekAll('credential-library');
+ return {
+ target,
+ credentialLibraries,
+ };
+ }
+
+ /**
+ * Renders credential libraries specific header template.
+ * @override
+ */
+ renderTemplate() {
+ super.renderTemplate(...arguments);
+
+ this.render(
+ 'scopes/scope/targets/target/add-credential-libraries/-header',
+ {
+ into: 'scopes/scope/targets/target',
+ outlet: 'header',
+ }
+ );
+
+ this.render('-empty', {
+ into: 'scopes/scope/targets/target',
+ outlet: 'navigation',
+ });
+
+ this.render('-empty', {
+ into: 'scopes/scope/targets/target',
+ outlet: 'actions',
+ });
+ }
+
+ // =actions
+
+ /**
+ * Add credential libraries to current target
+ * @param {TargetModel} target
+ * @param {[string]} credentialLibraryIDs
+ */
+ @action
+ @loading
+ @notifyError(({ message }) => message, { catch: true })
+ @notifySuccess('notifications.add-success')
+ async save(target, credentialLibraryIDs) {
+ await target.addCredentialLibraries(credentialLibraryIDs);
+ this.replaceWith('scopes.scope.targets.target.credential-libraries');
+ }
+
+ /**
+ * Redirect to target credential libraries as if nothing ever happened.
+ */
+ @action
+ cancel() {
+ this.replaceWith('scopes.scope.targets.target.credential-libraries');
+ }
+}
diff --git a/ui/admin/app/templates/scopes/scope/targets/target/-actions.hbs b/ui/admin/app/templates/scopes/scope/targets/target/-actions.hbs
index 9b6b207e3..9c95b8fa2 100644
--- a/ui/admin/app/templates/scopes/scope/targets/target/-actions.hbs
+++ b/ui/admin/app/templates/scopes/scope/targets/target/-actions.hbs
@@ -6,6 +6,13 @@
{{t 'resources.target.actions.add-host-sets'}}
+ {{#if (feature-flag 'credential-store')}}
+
+ {{t 'resources.target.actions.add-credential-libraries'}}
+
+ {{/if}}
+{{/if}}
\ No newline at end of file
diff --git a/ui/admin/app/templates/scopes/scope/targets/target/add-credential-libraries/-header.hbs b/ui/admin/app/templates/scopes/scope/targets/target/add-credential-libraries/-header.hbs
new file mode 100644
index 000000000..4ae5bad96
--- /dev/null
+++ b/ui/admin/app/templates/scopes/scope/targets/target/add-credential-libraries/-header.hbs
@@ -0,0 +1,4 @@
+
+ {{t 'resources.target.credential-library.messages.add.title'}}
+
+{{t 'resources.target.credential-library.messages.add.description'}}
\ No newline at end of file
diff --git a/ui/admin/tests/integration/components/form/target/add-credential-libraries/index-test.js b/ui/admin/tests/integration/components/form/target/add-credential-libraries/index-test.js
new file mode 100644
index 000000000..867478cc8
--- /dev/null
+++ b/ui/admin/tests/integration/components/form/target/add-credential-libraries/index-test.js
@@ -0,0 +1,29 @@
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+import { render } from '@ember/test-helpers';
+import { hbs } from 'ember-cli-htmlbars';
+
+module(
+ 'Integration | Component | form/target/add-credential-libraries/index',
+ function (hooks) {
+ setupRenderingTest(hooks);
+
+ test('it renders', async function (assert) {
+ // Set any properties with this.set('myProperty', 'value');
+ // Handle any actions with this.set('myAction', function(val) { ... });
+
+ await render(hbs``);
+
+ assert.equal(this.element.textContent.trim(), '');
+
+ // Template block usage:
+ await render(hbs`
+
+ template block text
+
+ `);
+
+ assert.equal(this.element.textContent.trim(), 'template block text');
+ });
+ }
+);
diff --git a/ui/admin/tests/unit/routes/scopes/scope/targets/target/add-credential-libraries-test.js b/ui/admin/tests/unit/routes/scopes/scope/targets/target/add-credential-libraries-test.js
new file mode 100644
index 000000000..e6279d5b3
--- /dev/null
+++ b/ui/admin/tests/unit/routes/scopes/scope/targets/target/add-credential-libraries-test.js
@@ -0,0 +1,16 @@
+import { module, test } from 'qunit';
+import { setupTest } from 'ember-qunit';
+
+module(
+ 'Unit | Route | scopes/scope/targets/target/add-credential-libraries',
+ function (hooks) {
+ setupTest(hooks);
+
+ test('it exists', function (assert) {
+ let route = this.owner.lookup(
+ 'route:scopes/scope/targets/target/add-credential-libraries'
+ );
+ assert.ok(route);
+ });
+ }
+);