Skip to content

Commit

Permalink
UI/kv codemirror diff (#13000)
Browse files Browse the repository at this point in the history
* staying with jsondiff

* routing setup

* send compare against data to component after using new adapater method to return the version data.

* functionality

* fix issue on route transition not calling model hook

* formatting

* update version

* changelog

* glimmerize the json-editor component

* catch up

* passing tracked property from child to parent

* pull out of jsonEditor

* fix issue with message

* icon

* fix some issues with right selection

* changes and convert to component

* integration test

* tests

* fixes

* cleanup

* cleanup 2

* fixes

* fix test by spread attributes

* remove log

* remove
  • Loading branch information
Monkeychip committed Dec 1, 2021
1 parent 4339cfa commit 720db8e
Show file tree
Hide file tree
Showing 19 changed files with 495 additions and 60 deletions.
3 changes: 3 additions & 0 deletions changelog/13000.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:improvement
ui: Add version diff view for KV V2
```
10 changes: 10 additions & 0 deletions ui/app/adapters/secret-v2-version.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,16 @@ export default ApplicationAdapter.extend({
});
},

querySecretDataByVersion(id) {
return this.ajax(this.urlForQueryRecord(id), 'GET')
.then(resp => {
return resp.data;
})
.catch(error => {
return error.data;
});
},

urlForCreateRecord(modelName, snapshot) {
let backend = snapshot.belongsTo('secret').belongsTo('engine').id;
let path = snapshot.attr('path');
Expand Down
83 changes: 83 additions & 0 deletions ui/app/components/diff-version-selector.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/* eslint-disable no-undef */
import Component from '@glimmer/component';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';

/**
* @module DiffVersionSelector
* DiffVersionSelector component includes a toolbar and diff view between KV 2 versions. It uses the library jsondiffpatch.
*
* @example
* ```js
* <DiffVersionSelector @model={model}/>
* ```
* @param {object} model - model that comes from secret-v2-version
*/

export default class DiffVersionSelector extends Component {
@tracked leftSideVersionDataSelected = null;
@tracked leftSideVersionSelected = null;
@tracked rightSideVersionDataSelected = null;
@tracked rightSideVersionSelected = null;
@tracked statesMatch = false;
@tracked visualDiff = null;
@service store;

adapter = this.store.adapterFor('secret-v2-version');

constructor() {
super(...arguments);
this.createVisualDiff();
}

get leftSideDataInit() {
let string = `["${this.args.model.engineId}", "${this.args.model.id}", "${this.args.model.currentVersion}"]`;
return this.adapter
.querySecretDataByVersion(string)
.then(response => response.data)
.catch(() => null);
}
get rightSideDataInit() {
let string = `["${this.args.model.engineId}", "${this.args.model.id}", "${this.rightSideVersionInit}"]`;
return this.adapter
.querySecretDataByVersion(string)
.then(response => response.data)
.catch(() => null);
}
get rightSideVersionInit() {
// initial value of right side version is one less than the current version
return this.args.model.currentVersion === 1 ? 0 : this.args.model.currentVersion - 1;
}

async createVisualDiff() {
let diffpatcher = jsondiffpatch.create({});
let leftSideVersionData = this.leftSideVersionDataSelected || (await this.leftSideDataInit);
let rightSideVersionData = this.rightSideVersionDataSelected || (await this.rightSideDataInit);
let delta = diffpatcher.diff(rightSideVersionData, leftSideVersionData);
if (delta === undefined) {
this.statesMatch = true;
this.visualDiff = JSON.stringify(leftSideVersionData, undefined, 2); // params: value, replacer (all properties included), space (white space and indentation, line break, etc.)
} else {
this.statesMatch = false;
this.visualDiff = jsondiffpatch.formatters.html.format(delta, rightSideVersionData);
}
}

@action
async selectVersion(selectedVersion, actions, side) {
let string = `["${this.args.model.engineId}", "${this.args.model.id}", "${selectedVersion}"]`;
let secretData = await this.adapter.querySecretDataByVersion(string);
if (side === 'left') {
this.leftSideVersionDataSelected = secretData.data;
this.leftSideVersionSelected = selectedVersion;
}
if (side === 'right') {
this.rightSideVersionDataSelected = secretData.data;
this.rightSideVersionSelected = selectedVersion;
}
await this.createVisualDiff();
// close dropdown menu.
actions.close();
}
}
74 changes: 46 additions & 28 deletions ui/app/components/json-editor.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,21 @@
import Component from '@ember/component';
import Component from '@glimmer/component';
import { action } from '@ember/object';

/**
* @module JsonEditor
*
* @example
* ```js
* <JsonEditor @title="Policy" @value={{codemirror.string}} @valueUpdated={{ action "codemirrorUpdate"}} />
* ```
*
* @param {string} [title] - Name above codemirror view
* @param {string} value - a specific string the comes from codemirror. It's the value inside the codemirror display
* @param {Function} [valueUpdated] - action to preform when you edit the codemirror value.
* @param {Function} [onFocusOut] - action to preform when you focus out of codemirror.
* @param {string} [helpText] - helper text.
* @param {object} [options] - option object that overrides codemirror default options such as the styling.
*/

const JSON_EDITOR_DEFAULTS = {
// IMPORTANT: `gutters` must come before `lint` since the presence of
Expand All @@ -13,20 +30,16 @@ const JSON_EDITOR_DEFAULTS = {
showCursorWhenSelecting: true,
};

export default Component.extend({
showToolbar: true,
title: null,
subTitle: null,
helpText: null,
value: null,
options: null,
valueUpdated: null,
onFocusOut: null,
readOnly: false,
export default class JsonEditorComponent extends Component {
value = null;
valueUpdated = null;
onFocusOut = null;
readOnly = false;
options = null;

init() {
this._super(...arguments);
this.options = { ...JSON_EDITOR_DEFAULTS, ...this.options };
constructor() {
super(...arguments);
this.options = { ...JSON_EDITOR_DEFAULTS, ...this.args.options };
if (this.options.autoHeight) {
this.options.viewportMargin = Infinity;
delete this.options.autoHeight;
Expand All @@ -36,18 +49,23 @@ export default Component.extend({
this.options.lineNumbers = false;
delete this.options.gutters;
}
},
}

get getShowToolbar() {
return this.args.showToolbar === false ? false : true;
}

@action
updateValue(...args) {
if (this.args.valueUpdated) {
this.args.valueUpdated(...args);
}
}

actions: {
updateValue(...args) {
if (this.valueUpdated) {
this.valueUpdated(...args);
}
},
onFocus(...args) {
if (this.onFocusOut) {
this.onFocusOut(...args);
}
},
},
});
@action
onFocus(...args) {
if (this.args.onFocusOut) {
this.args.onFocusOut(...args);
}
}
}
4 changes: 4 additions & 0 deletions ui/app/controllers/vault/cluster/secrets/backend/diff.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import Controller from '@ember/controller';
import BackendCrumbMixin from 'vault/mixins/backend-crumb';

export default class DiffController extends Controller.extend(BackendCrumbMixin) {}
1 change: 1 addition & 0 deletions ui/app/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ Router.map(function() {

this.route('list', { path: '/list/*secret' });
this.route('show', { path: '/show/*secret' });
this.route('diff', { path: '/diff/*id' });
this.route('metadata', { path: '/metadata/*secret' });
this.route('edit-metadata', { path: '/edit-metadata/*secret' });
this.route('create', { path: '/create/*secret' });
Expand Down
25 changes: 25 additions & 0 deletions ui/app/routes/vault/cluster/secrets/backend/diff.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';

export default class diff extends Route {
@service store;

beforeModel() {
let { backend } = this.paramsFor('vault.cluster.secrets.backend');
this.backend = backend;
}

model(params) {
let { id } = params;
return this.store.queryRecord('secret-v2', {
backend: this.backend,
id,
});
}

setupController(controller, model) {
controller.set('backend', this.backend); // for backendCrumb
controller.set('id', model.id); // for navigation on tabs
controller.set('model', model);
}
}
24 changes: 24 additions & 0 deletions ui/app/styles/components/diff-version-selector.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
.visual-diff {
background-color: black;

pre {
color: $ui-gray-010;
}
}

.jsondiffpatch-deleted .jsondiffpatch-property-name,
.jsondiffpatch-deleted pre,
.jsondiffpatch-modified .jsondiffpatch-left-value pre,
.jsondiffpatch-textdiff-deleted {
background: $red-500;
}
.jsondiffpatch-added .jsondiffpatch-property-name,
.jsondiffpatch-added .jsondiffpatch-value pre,
.jsondiffpatch-modified .jsondiffpatch-right-value pre,
.jsondiffpatch-textdiff-added {
background: $green-500;
}

.jsondiffpatch-property-name {
color: $ui-gray-300;
}
11 changes: 11 additions & 0 deletions ui/app/styles/components/toolbar.scss
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,14 @@
margin: 0 $spacing-xs;
width: 0;
}

.version-diff-toolbar {
display: flex;
align-items: baseline;
gap: $spacing-s;

.diff-status {
display: flex;
direction: rtl;
}
}
1 change: 1 addition & 0 deletions ui/app/styles/core.scss
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
@import './components/confirm';
@import './components/console-ui-panel';
@import './components/control-group';
@import './components/diff-version-selector';
@import './components/doc-link';
@import './components/empty-state';
@import './components/env-banner';
Expand Down
94 changes: 94 additions & 0 deletions ui/app/templates/components/diff-version-selector.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<Toolbar>
<div class="version-diff-toolbar" data-test-version-diff-toolbar>
{{!-- Left side version --}}
<BasicDropdown
@class="popup-menu"
@horizontalPosition="auto-right"
@verticalPosition="below"
as |D|
>
<D.trigger
data-test-popup-menu-trigger="left-version"
@class={{concat "toolbar-link" (if D.isOpen " is-active")}}
@tagName="button"
>
Version {{or this.leftSideVersionSelected this.args.model.currentVersion}}
<Chevron @direction="down" @isButton={{true}} />
</D.trigger>
<D.content @class="popup-menu-content">
<nav class="box menu">
<ul class="menu-list">
{{#each (reverse this.args.model.versions) as |leftSideSecretVersion|}}
<li class="action" data-test-leftSide-version={{leftSideSecretVersion.version}}>
<button
class="link"
{{on "click" (fn this.selectVersion leftSideSecretVersion.version D.actions "left")}}
>
Version {{leftSideSecretVersion.version}}
{{#if (and (eq leftSideSecretVersion.version (or this.leftSideVersionSelected this.args.model.currentVersion)) (not leftSideSecretVersion.destroyed) (not leftSideSecretVersion.deleted))}}
<Icon @glyph="check-circle-outline" class="has-text-success is-pulled-right" />
{{else if leftSideSecretVersion.destroyed}}
<Icon @glyph="cancel-square-fill" class="has-text-danger is-pulled-right" />
{{else if leftSideSecretVersion.deleted}}
<Icon @glyph="cancel-square-fill" class="has-text-grey is-pulled-right" />
{{/if}}
</button>
</li>
{{/each}}
</ul>
</nav>
</D.content>
</BasicDropdown>
{{!-- Right side version --}}
<BasicDropdown
@class="popup-menu"
@horizontalPosition="right"
@verticalPosition="below"
as |D|
>
<D.trigger
@class={{concat "toolbar-link" (if D.isOpen " is-active")}}
@tagName="button"
data-test-popup-menu-trigger="right-version"
>
Version {{or this.rightSideVersionSelected this.rightSideVersionInit}}
<Chevron @direction="down" @isButton={{true}} />
</D.trigger>
<D.content @class="popup-menu-content">
<nav class="box menu">
<ul class="menu-list">
{{#each (reverse this.args.model.versions) as |rightSideSecretVersion|}}
<li class="action">
<button
class="link"
{{on "click" (fn this.selectVersion rightSideSecretVersion.version D.actions "right")}}
data-test-rightSide-version={{rightSideSecretVersion.version}}
>
Version {{rightSideSecretVersion.version}}
{{#if (and (eq rightSideSecretVersion.version (or this.rightSideVersionSelected this.rightSideVersionInit)) (not rightSideSecretVersion.destroyed) (not rightSideSecretVersion.deleted))}}
<Icon @glyph="check-circle-outline" class="has-text-success is-pulled-right" />
{{else if rightSideSecretVersion.destroyed}}
<Icon @glyph="cancel-square-fill" class="has-text-danger is-pulled-right" />
{{else if rightSideSecretVersion.deleted}}
<Icon @glyph="cancel-square-fill" class="has-text-grey is-pulled-right" />
{{/if}}
</button>
</li>
{{/each}}
</ul>
</nav>
</D.content>
</BasicDropdown>
{{!-- Status --}}
{{#if this.statesMatch}}
<div class="diff-status">
<span>States match</span>
<Icon @glyph="check-circle-fill" class="has-text-success" />
</div>
{{/if}}
</div>
</Toolbar>

<div class="form-section visual-diff">
<pre>{{{this.visualDiff}}}</pre>
</div>
Loading

0 comments on commit 720db8e

Please sign in to comment.