mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-10-29 17:52:32 +00:00
Secrets Sync UI: Bug fixes part 3 (#24644)
* update header to refer to destination name * teeny design improvements VAULT-22943 * update azure model attrs * remove padding, add destination type to description VAULT-22930 VAULT-22943 * fix overview popupmenu nav to sync secrets VAULT-22944 * update sync banner, hyperlink secret * redirect when all destinations are deleted VAULT-22945 * add keyVaultUri to credentials for editing * fix extra space and test for sync banner * use localName to get dynamic route section to fix pagination transition error * add copy header remove duplicate app type * add cloud param to azure mirage destination * add comments * enter line * conditionally render view synced secrets button * revert pagination route change * combine buttons and add logic for args * rename to route * remove model arg
This commit is contained in:
@@ -8,8 +8,8 @@ import { attr } from '@ember-data/model';
|
||||
import { withFormFields } from 'vault/decorators/model-form-fields';
|
||||
const displayFields = ['name', 'keyVaultUri', 'tenantId', 'cloud', 'clientId', 'clientSecret'];
|
||||
const formFieldGroups = [
|
||||
{ default: ['name', 'keyVaultUri', 'tenantId', 'cloud', 'clientId'] },
|
||||
{ Credentials: ['clientSecret'] },
|
||||
{ default: ['name', 'tenantId', 'cloud', 'clientId'] },
|
||||
{ Credentials: ['keyVaultUri', 'clientSecret'] },
|
||||
];
|
||||
@withFormFields(displayFields, formFieldGroups)
|
||||
export default class SyncDestinationsAzureKeyVaultModel extends SyncDestinationModel {
|
||||
@@ -19,7 +19,7 @@ export default class SyncDestinationsAzureKeyVaultModel extends SyncDestinationM
|
||||
'URI of an existing Azure Key Vault instance. If empty, Vault will use the KEY_VAULT_URI environment variable if configured.',
|
||||
editDisabled: true,
|
||||
})
|
||||
keyVaultUri;
|
||||
keyVaultUri; // obfuscated, never returned by API
|
||||
|
||||
@attr('string', {
|
||||
label: 'Client ID',
|
||||
@@ -44,7 +44,6 @@ export default class SyncDestinationsAzureKeyVaultModel extends SyncDestinationM
|
||||
|
||||
@attr('string', {
|
||||
subText: 'Specifies a cloud for the client. The default is Azure Public Cloud.',
|
||||
defaultValue: 'cloud',
|
||||
editDisabled: true,
|
||||
})
|
||||
cloud;
|
||||
|
||||
@@ -26,7 +26,7 @@ const SYNC_DESTINATIONS: Array<SyncDestination> = [
|
||||
type: 'azure-kv',
|
||||
icon: 'azure-color',
|
||||
category: 'cloud',
|
||||
maskedParams: ['clientSecret'],
|
||||
maskedParams: ['clientSecret', 'keyVaultUri'],
|
||||
},
|
||||
{
|
||||
name: 'Google Secret Manager',
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
~}}
|
||||
|
||||
<SyncHeader
|
||||
@title="Sync Secrets to {{@destination.typeDisplayName}}"
|
||||
@title="Sync Secrets to {{@destination.name}}"
|
||||
@icon={{@destination.icon}}
|
||||
@breadcrumbs={{array
|
||||
(hash label="Secrets Sync" route="secrets.overview")
|
||||
(hash label="Destinations" route="secrets.destinations")
|
||||
@@ -13,15 +14,19 @@
|
||||
}}
|
||||
/>
|
||||
|
||||
<form {{on "submit" (perform this.setAssociation)}} class={{unless (or this.error this.syncedSecret) "has-top-margin-m"}}>
|
||||
<form {{on "submit" (perform this.setAssociation)}}>
|
||||
<MessageError @errorMessage={{this.error}} />
|
||||
|
||||
{{#if this.syncedSecret}}
|
||||
<Hds::Alert @type="inline" @color="success" as |A|>
|
||||
<A.Title>Successfully synced a secret</A.Title>
|
||||
<Hds::Alert @type="inline" @color="success" @icon="sync" as |A|>
|
||||
<A.Title>Sync initiated</A.Title>
|
||||
<A.Description data-test-sync-success-message>
|
||||
Sync operation successfully initiated for "{{this.syncedSecret}}". You can continue on this page to sync more
|
||||
secrets.
|
||||
Sync operation successfully initiated for
|
||||
<Hds::Link::Inline
|
||||
@isRouteExternal={{true}}
|
||||
@route="kvSecretDetails"
|
||||
@models={{array this.mountPath this.syncedSecret}}
|
||||
>{{this.syncedSecret}}</Hds::Link::Inline>. You can continue on this page to sync more secrets.
|
||||
</A.Description>
|
||||
</Hds::Alert>
|
||||
{{/if}}
|
||||
@@ -30,7 +35,9 @@
|
||||
|
||||
<p class="is-label">Which secrets would you like us to sync?</p>
|
||||
<p class="sub-text">
|
||||
Select a KV engine mount and path to sync a secret to the destination.
|
||||
Select a KV engine mount and path to sync a secret to the
|
||||
{{@destination.typeDisplayName}}
|
||||
destination.
|
||||
</p>
|
||||
|
||||
<div class="has-top-margin-l">
|
||||
@@ -72,7 +79,14 @@
|
||||
disabled={{this.isSubmitDisabled}}
|
||||
data-test-sync-submit
|
||||
/>
|
||||
<Hds::Button @text="Back" @color="secondary" {{on "click" this.back}} data-test-sync-cancel />
|
||||
<Hds::Button
|
||||
@text={{if this.syncedSecret "View synced secrets" "Back"}}
|
||||
@icon={{if this.syncedSecret "chevron-right"}}
|
||||
@iconPosition="trailing"
|
||||
@color={{if this.syncedSecret "tertiary" "secondary"}}
|
||||
@route="secrets.destinations.destination.secrets"
|
||||
data-test-sync-cancel
|
||||
/>
|
||||
</Hds::ButtonSet>
|
||||
{{#if this.isSecretDirectory}}
|
||||
<AlertInline
|
||||
|
||||
@@ -66,11 +66,6 @@ export default class DestinationSyncPageComponent extends Component<Args> {
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
back() {
|
||||
this.router.transitionTo('vault.cluster.sync.secrets.destinations.destination.secrets');
|
||||
}
|
||||
|
||||
@action
|
||||
setMount(selected: Array<string>) {
|
||||
this.mountPath = selected[0] || '';
|
||||
|
||||
@@ -97,7 +97,7 @@
|
||||
/>
|
||||
<dd.Interactive
|
||||
@route="secrets.destinations.destination.sync"
|
||||
@model={{data}}
|
||||
@models={{array data.type data.name}}
|
||||
@text="Sync secrets"
|
||||
data-test-overview-table-action="sync"
|
||||
/>
|
||||
|
||||
@@ -8,6 +8,8 @@ import { inject as service } from '@ember/service';
|
||||
import { hash } from 'rsvp';
|
||||
|
||||
import type StoreService from 'vault/services/store';
|
||||
import type RouterService from '@ember/routing/router-service';
|
||||
import type { ModelFrom } from 'vault/vault/route';
|
||||
import type SyncDestinationModel from 'vault/vault/models/sync/destination';
|
||||
|
||||
interface SyncSecretsDestinationsIndexRouteParams {
|
||||
@@ -18,6 +20,7 @@ interface SyncSecretsDestinationsIndexRouteParams {
|
||||
|
||||
export default class SyncSecretsDestinationsIndexRoute extends Route {
|
||||
@service declare readonly store: StoreService;
|
||||
@service declare readonly router: RouterService;
|
||||
|
||||
queryParams = {
|
||||
page: {
|
||||
@@ -31,6 +34,12 @@ export default class SyncSecretsDestinationsIndexRoute extends Route {
|
||||
},
|
||||
};
|
||||
|
||||
redirect(model: ModelFrom<SyncSecretsDestinationsIndexRoute>) {
|
||||
if (model.destinations.length === 0) {
|
||||
this.router.transitionTo('vault.cluster.sync.secrets.overview');
|
||||
}
|
||||
}
|
||||
|
||||
filterData(dataset: Array<SyncDestinationModel>, name: string, type: string): Array<SyncDestinationModel> {
|
||||
let filteredDataset = dataset;
|
||||
const filter = (key: keyof SyncDestinationModel, value: string) => {
|
||||
|
||||
@@ -21,6 +21,7 @@ export default Factory.extend({
|
||||
tenant_id: 'tenant-id',
|
||||
client_id: 'azure-client-id',
|
||||
client_secret: '*****',
|
||||
cloud: 'Azure Public Cloud',
|
||||
}),
|
||||
['gcp-sm']: trait({
|
||||
type: 'gcp-sm',
|
||||
|
||||
@@ -70,7 +70,7 @@ module('Integration | Component | sync | Secrets::Page::Destinations::Destinatio
|
||||
});
|
||||
|
||||
test('it should sync secret', async function (assert) {
|
||||
assert.expect(4);
|
||||
assert.expect(6);
|
||||
|
||||
const { type, name } = this.destination;
|
||||
this.server.post(`/sys/sync/destinations/${type}/${name}/associations/set`, (schema, req) => {
|
||||
@@ -81,14 +81,16 @@ module('Integration | Component | sync | Secrets::Page::Destinations::Destinatio
|
||||
});
|
||||
|
||||
assert.dom(submit).isDisabled('Submit button is disabled when mount is not selected');
|
||||
assert.dom(cancel).hasText('Back', 'back button renders');
|
||||
await selectChoose(mountSelect, '.ember-power-select-option', 1);
|
||||
assert.dom(submit).isDisabled('Submit button is disabled when secret is not selected');
|
||||
await click(kvSuggestion.input);
|
||||
await click(searchSelect.option(1));
|
||||
await click(submit);
|
||||
assert.dom(cancel).hasText('View synced secrets', 'view secrets tertiary renders');
|
||||
assert
|
||||
.dom(successMessage)
|
||||
.includesText('Sync operation successfully initiated for "my-secret".', 'Success banner renders');
|
||||
.includesText('Sync operation successfully initiated for my-secret.', 'Success banner renders');
|
||||
});
|
||||
|
||||
test('it should allow manual mount path input if kv mounts are not returned', async function (assert) {
|
||||
@@ -116,16 +118,6 @@ module('Integration | Component | sync | Secrets::Page::Destinations::Destinatio
|
||||
await click(submit);
|
||||
});
|
||||
|
||||
test('it should transition to destination secrets route on cancel', async function (assert) {
|
||||
const transitionStub = sinon.stub(this.owner.lookup('service:router'), 'transitionTo');
|
||||
await click(cancel);
|
||||
assert.propEqual(
|
||||
transitionStub.lastCall.args,
|
||||
['vault.cluster.sync.secrets.destinations.destination.secrets'],
|
||||
'Transitions to destination secrets route on cancel'
|
||||
);
|
||||
});
|
||||
|
||||
test('it should render alert banner on sync error', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
|
||||
@@ -83,22 +83,6 @@ export interface EngineOwner extends Owner {
|
||||
mountPoint: string;
|
||||
}
|
||||
|
||||
export type SyncDestinationType = 'aws-sm' | 'azure-kv' | 'gcp-sm' | 'gh' | 'vercel-project';
|
||||
export type SyncDestinationName =
|
||||
| 'AWS Secrets Manager'
|
||||
| 'Azure Key Vault'
|
||||
| 'Google Secret Manager'
|
||||
| 'Github Actions'
|
||||
| 'Vercel Project';
|
||||
|
||||
export interface SyncDestination {
|
||||
name: SyncDestinationName;
|
||||
type: SyncDestinationType;
|
||||
icon: 'aws-color' | 'azure-color' | 'gcp-color' | 'github-color' | 'vercel-color';
|
||||
category: 'cloud' | 'dev-tools';
|
||||
maskedParams: Array<string>;
|
||||
}
|
||||
|
||||
export interface SearchSelectOption {
|
||||
name: string;
|
||||
id: string;
|
||||
|
||||
25
ui/types/vault/route.d.ts
vendored
Normal file
25
ui/types/vault/route.d.ts
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
import Route from '@ember/routing/route';
|
||||
|
||||
/*
|
||||
Get the resolved type of an item.
|
||||
https://docs.ember-cli-typescript.com/cookbook/working-with-route-models
|
||||
|
||||
- If the item is a promise, the result will be the resolved value type
|
||||
- If the item is not a promise, the result will just be the type of the item
|
||||
*/
|
||||
export type Resolved<P> = P extends Promise<infer T> ? T : P;
|
||||
|
||||
/*
|
||||
Get the resolved model value from a route.
|
||||
Example use:
|
||||
|
||||
import type { ModelFrom } from 'vault/vault/router';
|
||||
export default class MyRoute extends Route {
|
||||
redirect(model: ModelFrom<MyRoute>) {}
|
||||
}
|
||||
*/
|
||||
export type ModelFrom<R extends Route> = Resolved<ReturnType<R['model']>>;
|
||||
Reference in New Issue
Block a user