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:
claire bontempo
2024-01-04 12:02:12 -08:00
committed by GitHub
parent 52917e0908
commit 36fc2c1a73
10 changed files with 66 additions and 47 deletions

View File

@@ -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;

View File

@@ -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',

View File

@@ -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

View File

@@ -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] || '';

View File

@@ -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"
/>

View File

@@ -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) => {

View File

@@ -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',

View File

@@ -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);

View File

@@ -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
View 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']>>;