Skip to content

Commit

Permalink
ui: Add version diff comparison to KV v2 (#23200)
Browse files Browse the repository at this point in the history
* add diff route

* add version diff toolbar link

* finish functionality of version diff comparison

* add tests

* update empty state message

* update selectors

* wip tests

* finish test

* add empty state test

* switch dropdowns

* add changelog

* add comment
  • Loading branch information
hellobontempo authored Sep 21, 2023
1 parent 758de87 commit 8375149
Show file tree
Hide file tree
Showing 11 changed files with 342 additions and 23 deletions.
3 changes: 3 additions & 0 deletions changelog/23200.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:improvement
ui: Move access to KV V2 version diff view to toolbar in Version History
```
41 changes: 30 additions & 11 deletions ui/lib/kv/addon/components/kv-version-dropdown.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,36 @@
<ul class="menu-list">
{{#each @metadata.sortedVersions as |versionData|}}
<li data-test-version={{versionData.version}} class="action">
<LinkTo @query={{hash version=versionData.version}} {{on "click" (fn @onClose D)}}>
Version
{{versionData.version}}
{{#if versionData.destroyed}}
<Icon @name="x-square-fill" class="has-text-danger is-pulled-right" />
{{else if versionData.isSecretDeleted}}
<Icon @name="x-square-fill" class="has-text-grey is-pulled-right" />
{{else if (loose-equal versionData.version @metadata.currentVersion)}}
<Icon @name="check-circle" class="has-text-success is-pulled-right" />
{{/if}}
</LinkTo>
{{#if @onSelect}}
<button
disabled={{or versionData.destroyed versionData.isSecretDeleted}}
{{on "click" (fn @onSelect versionData.version D.actions)}}
type="button"
class="link {{if (loose-equal versionData.version @displayVersion) 'is-active'}}"
>
Version
{{versionData.version}}
{{#if versionData.destroyed}}
<Icon @name="x-square-fill" class="has-text-danger is-pulled-right" />
{{else if versionData.isSecretDeleted}}
<Icon @name="x-square-fill" class="has-text-grey is-pulled-right" />
{{else if (loose-equal versionData.version @metadata.currentVersion)}}
<Icon @name="check-circle" class="has-text-success is-pulled-right" />
{{/if}}
</button>
{{else}}
<LinkTo @query={{hash version=versionData.version}} {{on "click" (fn @onClose D)}}>
Version
{{versionData.version}}
{{#if versionData.destroyed}}
<Icon @name="x-square-fill" class="has-text-danger is-pulled-right" />
{{else if versionData.isSecretDeleted}}
<Icon @name="x-square-fill" class="has-text-grey is-pulled-right" />
{{else if (loose-equal versionData.version @metadata.currentVersion)}}
<Icon @name="check-circle" class="has-text-success is-pulled-right" />
{{/if}}
</LinkTo>
{{/if}}
</li>
{{/each}}
</ul>
Expand Down
33 changes: 33 additions & 0 deletions ui/lib/kv/addon/components/page/secret/metadata/version-diff.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<KvPageHeader @breadcrumbs={{@breadcrumbs}} @pageTitle="Version Diff">
<:toolbarFilters>
<span class="has-text-grey has-text-weight-semibold is-size-8">FROM:</span>
<KvVersionDropdown
@displayVersion={{this.leftVersion}}
@metadata={{@metadata}}
@onSelect={{fn this.handleSelect "leftVersion"}}
/>
<span class="has-text-grey has-text-weight-semibold is-size-8">TO:</span>
<KvVersionDropdown
@displayVersion={{this.rightVersion}}
@metadata={{@metadata}}
@onSelect={{fn this.handleSelect "rightVersion"}}
/>
{{#if this.statesMatch}}
<div class="has-left-padding-s">
<Icon @name="check-circle-fill" class="has-text-success" />
<span>States match</span>
</div>
{{/if}}
</:toolbarFilters>
</KvPageHeader>

{{#if this.deactivatedState}}
<EmptyState
@title="Version {{this.rightVersion}} has been {{this.deactivatedState}}"
@message="The current version of this secret has been {{this.deactivatedState}}. Select another version to compare."
/>
{{else}}
<div class="form-section visual-diff text-grey-lightest background-color-black has-top-margin-s">
<pre data-test-visual-diff>{{sanitized-html this.visualDiff}}</pre>
</div>
{{/if}}
82 changes: 82 additions & 0 deletions ui/lib/kv/addon/components/page/secret/metadata/version-diff.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/

import Component from '@glimmer/component';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';
import { tracked } from '@glimmer/tracking';
import { kvDataPath } from 'vault/utils/kv-path';

/**
* @module KvSecretMetadataVersionDiff renders the version diff comparison
* <Page::Secret::Metadata::VersionDiff
* @metadata={{this.model.metadata}}
* @path={{this.model.path}}
* @backend={{this.model.backend}}
* @breadcrumbs={{this.breadcrumbs}}
* />
*
* @param {model} metadata - Ember data model: 'kv/metadata'
* @param {string} path - path to request secret data for selected version
* @param {string} backend - kv secret mount to make network request
* @param {array} breadcrumbs - Array to generate breadcrumbs, passed to the page header component
*/

/* eslint-disable no-undef */
export default class KvSecretMetadataVersionDiff extends Component {
@service store;
@tracked leftVersion;
@tracked rightVersion;
@tracked visualDiff;
@tracked statesMatch = false;

constructor() {
super(...arguments);

// initialize with most recently (before current), active version on left
const olderVersions = this.args.metadata.sortedVersions.slice(1);
const recentlyActive = olderVersions.find((v) => !v.destroyed && !v.isSecretDeleted);
this.leftVersion = Number(recentlyActive?.version);
this.rightVersion = this.args.metadata.currentVersion;

// this diff is from older to newer (current) secret data
this.createVisualDiff();
}

// this can only be true on initialization if the current version is inactive
// selecting a deleted/destroyed version is otherwise disabled
get deactivatedState() {
const { currentVersion, currentSecret } = this.args.metadata;
return this.rightVersion === currentVersion && currentSecret.isDeactivated ? currentSecret.state : '';
}

@action
handleSelect(side, version, actions) {
this[side] = Number(version);
actions.close();
this.createVisualDiff();
}

async createVisualDiff() {
const leftSecretData = await this.fetchSecretData(this.leftVersion);
const rightSecretData = await this.fetchSecretData(this.rightVersion);
const diffpatcher = jsondiffpatch.create({});
const delta = diffpatcher.diff(leftSecretData, rightSecretData);

this.statesMatch = !delta;
this.visualDiff = delta
? jsondiffpatch.formatters.html.format(delta, leftSecretData)
: JSON.stringify(rightSecretData, undefined, 2);
}

async fetchSecretData(version) {
const { backend, path } = this.args;
// check the store first, avoiding an extra network request if possible.
const storeData = await this.store.peekRecord('kv/data', kvDataPath(backend, path, version));
const data = storeData ? storeData : await this.store.queryRecord('kv/data', { backend, path, version });

return data?.secretData;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,14 @@
<LinkTo @route="secret.paths" data-test-secrets-tab="Paths">Paths</LinkTo>
<LinkTo @route="secret.metadata.versions" data-test-secrets-tab="Version History">Version History</LinkTo>
</:tabLinks>

<:toolbarActions>
{{#if @metadata.canReadMetadata}}
<ToolbarLink @route="secret.metadata.diff">Version diff</ToolbarLink>
{{/if}}
</:toolbarActions>
</KvPageHeader>

<Toolbar />
{{#if @metadata.canReadMetadata}}
<div class="sub-text has-text-weight-semibold is-flex-end has-short-padding">
<KvTooltipTimestamp @text="Secret last updated" @timestamp={{@metadata.updatedTime}} />
Expand Down
1 change: 1 addition & 0 deletions ui/lib/kv/addon/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export default buildRoutes(function () {
this.route('metadata', function () {
this.route('edit');
this.route('versions');
this.route('diff');
});
});
this.route('configuration');
Expand Down
23 changes: 23 additions & 0 deletions ui/lib/kv/addon/routes/secret/metadata/diff.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/

import Route from '@ember/routing/route';
import { breadcrumbsForSecret } from 'kv/utils/kv-breadcrumbs';

export default class KvSecretMetadataDiffRoute extends Route {
// model passed from parent secret route, if we need to access or intercept
// it can retrieved via `this.modelFor('secret'), which includes the metadata model.
setupController(controller, resolvedModel) {
super.setupController(controller, resolvedModel);
const breadcrumbsArray = [
{ label: 'secrets', route: 'secrets', linkExternal: true },
{ label: resolvedModel.backend, route: 'list' },
...breadcrumbsForSecret(resolvedModel.path),
{ label: 'version history', route: 'secret.metadata.versions' },
{ label: 'diff' },
];
controller.set('breadcrumbs', breadcrumbsArray);
}
}
6 changes: 6 additions & 0 deletions ui/lib/kv/addon/templates/secret/metadata/diff.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<Page::Secret::Metadata::VersionDiff
@metadata={{this.model.metadata}}
@path={{this.model.path}}
@backend={{this.model.backend}}
@breadcrumbs={{this.breadcrumbs}}
/>
9 changes: 5 additions & 4 deletions ui/tests/helpers/kv/kv-selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,6 @@ export const PAGE = {
edit: {
toggleDiff: '[data-test-toggle-input="Show diff"',
toggleDiffDescription: '[data-test-diff-description]',
visualDiff: '[data-test-visual-diff]',
added: `.jsondiffpatch-added`,
deleted: `.jsondiffpatch-deleted`,
},
list: {
createSecret: '[data-test-toolbar-create-secret]',
Expand All @@ -73,10 +70,14 @@ export const PAGE = {
icon: (version) => `[data-test-icon-holder="${version}"]`,
linkedBlock: (version) =>
version ? `[data-test-version-linked-block="${version}"]` : '[data-test-version-linked-block]',
button: (version) => `[data-test-version-button="${version}"]`,
versionMenu: (version) => `[data-test-version-linked-block="${version}"] [data-test-popup-menu-trigger]`,
createFromVersion: (version) => `[data-test-create-new-version-from="${version}"]`,
},
diff: {
visualDiff: '[data-test-visual-diff]',
added: `.jsondiffpatch-added`,
deleted: `.jsondiffpatch-deleted`,
},
create: {
metadataSection: '[data-test-metadata-section]',
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,24 +108,24 @@ module('Integration | Component | kv-v2 | Page::Secret::Edit', function (hooks)

assert.dom(PAGE.edit.toggleDiff).isDisabled('Diff toggle is disabled');
assert.dom(PAGE.edit.toggleDiffDescription).hasText('No changes to show. Update secret to view diff');
assert.dom(PAGE.edit.visualDiff).doesNotExist('Does not show visual diff');
assert.dom(PAGE.diff.visualDiff).doesNotExist('Does not show visual diff');

await fillIn(FORM.keyInput(1), 'foo2');
await fillIn(FORM.maskedValueInput(1), 'bar2');

assert.dom(PAGE.edit.toggleDiff).isNotDisabled('Diff toggle is not disabled');
assert.dom(PAGE.edit.toggleDiffDescription).hasText('Showing the diff will reveal secret values');
assert.dom(PAGE.edit.visualDiff).doesNotExist('Does not show visual diff');
assert.dom(PAGE.diff.visualDiff).doesNotExist('Does not show visual diff');
await click(PAGE.edit.toggleDiff);
assert.dom(PAGE.edit.visualDiff).exists('Shows visual diff');
assert.dom(PAGE.edit.added).hasText(`foo2"bar2"`);
assert.dom(PAGE.diff.visualDiff).exists('Shows visual diff');
assert.dom(PAGE.diff.added).hasText(`foo2"bar2"`);

await click(FORM.toggleJson);
codemirror().setValue('{ "foo3": "bar3" }');

assert.dom(PAGE.edit.visualDiff).exists('Visual diff updates');
assert.dom(PAGE.edit.deleted).hasText(`foo"bar"`);
assert.dom(PAGE.edit.added).hasText(`foo3"bar3"`);
assert.dom(PAGE.diff.visualDiff).exists('Visual diff updates');
assert.dom(PAGE.diff.deleted).hasText(`foo"bar"`);
assert.dom(PAGE.diff.added).hasText(`foo3"bar3"`);
});

test('it saves nested secrets', async function (assert) {
Expand Down
Loading

0 comments on commit 8375149

Please sign in to comment.