UI - raft config and snapshotting (#7410)
* add storage route * template out the routes and new raft storage overview * fetch raft config and add new server model * pngcrush the favicon * add view components and binary-file component * add form-save-buttons component * adjust rawRequest so that it can send a request body and returns the response on errors * hook up restore * rename binary-file to file-to-array-buffer * add ember-service-worker * use forked version of ember-service-worker for now * scope the service worker to a single endpoint * show both download buttons for now * add service worker download with a fallback to JS in-mem download * add remove peer functionality * lint go file * add storage-type to the cluster and node models * update edit for to take a cancel action * separate out a css table styles to be used by http-requests-table and on the raft-overview component * add raft-join adapter, model, component and use on the init page * fix styling and gate the menu item on the cluster using raft storage * style tweaks to the raft-join component * fix linting * add form-save-buttons component to storybook * add cancel functionality for backup uploads, and add a success message for successful uploads * add component tests * add filesize.js * add filesize and modified date to file-to-array-buffer * fix linting * fix server section showing in the cluster nav * don't use babel transforms in service worker lib because we don't want 2 copies of babel polyfill * add file-to-array-buffer to storybook * add comments and use removeObjectURL to raft-storage-overview * update alert-banner markdown * messaging change for upload alert banner * Update ui/app/templates/components/raft-storage-restore.hbs Co-Authored-By: Joshua Ogle <joshua@joshuaogle.com> * more comments * actually render the label if passed and update stories with knobs
This commit is contained in:
parent
e8432f1ebe
commit
87d4e6e068
|
@ -101,11 +101,13 @@ export default DS.RESTAdapter.extend(AdapterFetch, {
|
|||
return fetch(url, {
|
||||
method: type || 'GET',
|
||||
headers: opts.headers || {},
|
||||
body: opts.body,
|
||||
signal: opts.signal,
|
||||
}).then(response => {
|
||||
if (response.status >= 200 && response.status < 300) {
|
||||
return RSVP.resolve(response);
|
||||
} else {
|
||||
return RSVP.reject();
|
||||
return RSVP.reject(response);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
import ApplicationAdapter from './application';
|
||||
|
||||
export default ApplicationAdapter.extend({
|
||||
urlForCreateRecord() {
|
||||
return '/v1/sys/storage/raft/join';
|
||||
},
|
||||
});
|
|
@ -0,0 +1,15 @@
|
|||
import ApplicationAdapter from './application';
|
||||
|
||||
export default ApplicationAdapter.extend({
|
||||
urlForFindAll() {
|
||||
return '/v1/sys/storage/raft/configuration';
|
||||
},
|
||||
urlForDeleteRecord() {
|
||||
return '/v1/sys/storage/raft/remove-peer';
|
||||
},
|
||||
deleteRecord(store, type, snapshot) {
|
||||
let server_id = snapshot.attr('nodeId');
|
||||
let url = '/v1/sys/storage/raft/remove-peer';
|
||||
return this.ajax(url, 'POST', { data: { server_id } });
|
||||
},
|
||||
});
|
|
@ -0,0 +1,59 @@
|
|||
import Component from '@ember/component';
|
||||
import filesize from 'filesize';
|
||||
|
||||
/**
|
||||
* @module FileToArrayBuffer
|
||||
* `FileToArrayBuffer` is a component that will allow you to pick a file from the local file system. Once
|
||||
* loaded, this file will be emitted as a JS ArrayBuffer to the passed `onChange` callback.
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* <FileToArrayBuffer @onChange={{action (mut file)}} />
|
||||
* ```
|
||||
* @param onChange=null {Function} - The function to call when the file read is complete. This function
|
||||
* recieves the file as a JS ArrayBuffer
|
||||
* @param [label=null {String}] - Text to use as the label for the file input
|
||||
* @param [fileHelpText=null {String} - Text to use as help under the file input
|
||||
*
|
||||
*/
|
||||
export default Component.extend({
|
||||
classNames: ['box', 'is-fullwidth', 'is-marginless', 'is-shadowless'],
|
||||
onChange: () => {},
|
||||
label: null,
|
||||
fileHelpText: null,
|
||||
|
||||
file: null,
|
||||
fileName: null,
|
||||
fileSize: null,
|
||||
fileLastModified: null,
|
||||
|
||||
readFile(file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => this.send('onChange', reader.result, file);
|
||||
reader.readAsArrayBuffer(file);
|
||||
},
|
||||
|
||||
actions: {
|
||||
pickedFile(e) {
|
||||
let { files } = e.target;
|
||||
if (!files.length) {
|
||||
return;
|
||||
}
|
||||
for (let i = 0, len = files.length; i < len; i++) {
|
||||
this.readFile(files[i]);
|
||||
}
|
||||
},
|
||||
clearFile() {
|
||||
this.send('onChange');
|
||||
},
|
||||
onChange(fileAsBytes, fileMeta) {
|
||||
let { name, size, lastModifiedDate } = fileMeta || {};
|
||||
let fileSize = size ? filesize(size) : null;
|
||||
this.set('file', fileAsBytes);
|
||||
this.set('fileName', name);
|
||||
this.set('fileSize', fileSize);
|
||||
this.set('fileLastModified', lastModifiedDate);
|
||||
this.onChange(fileAsBytes, name);
|
||||
},
|
||||
},
|
||||
});
|
|
@ -0,0 +1,38 @@
|
|||
import { inject as service } from '@ember/service';
|
||||
|
||||
/**
|
||||
* @module RaftJoin
|
||||
* RaftJoin component presents the user with a choice to join an existing raft cluster when a new Vault
|
||||
* server is brought up
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* <RaftJoin @onDismiss={{action (mut attr)}} />
|
||||
* ```
|
||||
* @param {function} onDismiss - This function will be called if the user decides not to join an existing
|
||||
* raft cluster
|
||||
*
|
||||
*/
|
||||
|
||||
import Component from '@ember/component';
|
||||
|
||||
export default Component.extend({
|
||||
classNames: 'raft-join',
|
||||
store: service(),
|
||||
onDismiss() {},
|
||||
preference: 'join',
|
||||
showJoinForm: false,
|
||||
actions: {
|
||||
advanceFirstScreen() {
|
||||
if (this.preference !== 'join') {
|
||||
this.onDismiss();
|
||||
return;
|
||||
}
|
||||
this.set('showJoinForm', true);
|
||||
},
|
||||
newModel() {
|
||||
return this.store.createRecord('raft-join');
|
||||
},
|
||||
},
|
||||
});
|
|
@ -0,0 +1,81 @@
|
|||
import Component from '@ember/component';
|
||||
import { getOwner } from '@ember/application';
|
||||
import config from '../config/environment';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default Component.extend({
|
||||
flashMessages: service(),
|
||||
useServiceWorker: null,
|
||||
|
||||
async init() {
|
||||
this._super(...arguments);
|
||||
if (this.useServiceWorker === false) {
|
||||
return;
|
||||
}
|
||||
// check to see if we support ServiceWorker
|
||||
if ('serviceWorker' in navigator) {
|
||||
// this checks to see if there's an active service worker - if it failed to register
|
||||
// for any reason, then this would be null
|
||||
let worker = await navigator.serviceWorker.getRegistration(config.serviceWorkerScope);
|
||||
if (worker) {
|
||||
this.set('useServiceWorker', true);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
async removePeer(model) {
|
||||
let { nodeId } = model;
|
||||
try {
|
||||
await model.destroyRecord();
|
||||
} catch (e) {
|
||||
let errString = e.errors ? e.errors.join(' ') : e.message || e;
|
||||
this.flashMessages.danger(`There was an issue removing the peer ${nodeId}: ${errString}`);
|
||||
return;
|
||||
}
|
||||
this.flashMessages.success(`Successfully removed the peer: ${nodeId}.`);
|
||||
},
|
||||
|
||||
downloadViaServiceWorker() {
|
||||
// the actual download happens when the user clicks the anchor link, and then the ServiceWorker
|
||||
// intercepts the request and adds auth headers.
|
||||
// Here we just want to notify users that something is happening before the browser starts the download
|
||||
this.flashMessages.success('The snapshot download will begin shortly.');
|
||||
},
|
||||
|
||||
async downloadSnapshot() {
|
||||
// this entire method is the fallback behavior in case the browser either doesn't support ServiceWorker
|
||||
// or the UI is not being run on https.
|
||||
// here we're downloading the entire snapshot in memory, creating a dataurl with createObjectURL, and
|
||||
// then forcing a download by clicking a link that has a download attribute
|
||||
//
|
||||
// this is not the default because
|
||||
let adapter = getOwner(this).lookup('adapter:application');
|
||||
|
||||
this.flashMessages.success('The snapshot download has begun.');
|
||||
let resp, blob;
|
||||
try {
|
||||
resp = await adapter.rawRequest('/v1/sys/storage/raft/snapshot', 'GET');
|
||||
blob = await resp.blob();
|
||||
} catch (e) {
|
||||
let errString = e.errors ? e.errors.join(' ') : e.message || e;
|
||||
this.flashMessages.danger(`There was an error trying to download the snapshot: ${errString}`);
|
||||
}
|
||||
let filename = 'snapshot.gz';
|
||||
let file = new Blob([blob], { type: 'application/x-gzip' });
|
||||
file.name = filename;
|
||||
if ('msSaveOrOpenBlob' in navigator) {
|
||||
navigator.msSaveOrOpenBlob(file, filename);
|
||||
return;
|
||||
}
|
||||
let a = document.createElement('a');
|
||||
let objectURL = window.URL.createObjectURL(file);
|
||||
a.href = objectURL;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
window.URL.revokeObjectURL(objectURL);
|
||||
},
|
||||
},
|
||||
});
|
|
@ -0,0 +1,45 @@
|
|||
import Component from '@ember/component';
|
||||
import { task } from 'ember-concurrency';
|
||||
import { getOwner } from '@ember/application';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { alias } from '@ember/object/computed';
|
||||
import { AbortController } from 'fetch';
|
||||
|
||||
export default Component.extend({
|
||||
file: null,
|
||||
errors: null,
|
||||
forceRestore: false,
|
||||
flashMessages: service(),
|
||||
isUploading: alias('restore.isRunning'),
|
||||
abortController: null,
|
||||
restore: task(function*() {
|
||||
this.set('errors', null);
|
||||
let adapter = getOwner(this).lookup('adapter:application');
|
||||
try {
|
||||
let url = '/v1/sys/storage/raft/snapshot';
|
||||
if (this.forceRestore) {
|
||||
url = `${url}-force`;
|
||||
}
|
||||
let file = new Blob([this.file], { type: 'application/gzip' });
|
||||
let controller = new AbortController();
|
||||
this.set('abortController', controller);
|
||||
yield adapter.rawRequest(url, 'POST', { body: file, signal: controller.signal });
|
||||
this.flashMessages.success('The snapshot was successfully uploaded!');
|
||||
} catch (e) {
|
||||
if (e.name === 'AbortError') {
|
||||
return;
|
||||
}
|
||||
let resp;
|
||||
if (e.json) {
|
||||
resp = yield e.json();
|
||||
}
|
||||
let err = resp ? resp.errors : [e];
|
||||
this.set('errors', err);
|
||||
}
|
||||
}),
|
||||
actions: {
|
||||
cancelUpload() {
|
||||
this.abortController.abort();
|
||||
},
|
||||
},
|
||||
});
|
|
@ -1,5 +1,5 @@
|
|||
import { inject as service } from '@ember/service';
|
||||
import { not, gte, alias, and, or } from '@ember/object/computed';
|
||||
import { alias, and, equal, gte, not, or } from '@ember/object/computed';
|
||||
import { get, computed } from '@ember/object';
|
||||
import DS from 'ember-data';
|
||||
import { fragment } from 'ember-data-model-fragments/attributes';
|
||||
|
@ -38,7 +38,9 @@ export default DS.Model.extend({
|
|||
sealThreshold: alias('leaderNode.sealThreshold'),
|
||||
sealProgress: alias('leaderNode.progress'),
|
||||
sealType: alias('leaderNode.type'),
|
||||
storageType: alias('leaderNode.storageType'),
|
||||
hasProgress: gte('sealProgress', 1),
|
||||
usingRaft: equal('storageType', 'raft'),
|
||||
|
||||
//replication mode - will only ever be 'unsupported'
|
||||
//otherwise the particular mode will have the relevant mode attr through replication-attributes
|
||||
|
|
|
@ -24,6 +24,7 @@ export default DS.Model.extend({
|
|||
sealNumShares: alias('n'),
|
||||
version: attr('string'),
|
||||
type: attr('string'),
|
||||
storageType: attr('string'),
|
||||
|
||||
//https://www.vaultproject.io/docs/http/sys-leader.html
|
||||
haEnabled: attr('boolean'),
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
import DS from 'ember-data';
|
||||
import { expandAttributeMeta } from 'vault/utils/field-to-attrs';
|
||||
import { computed } from '@ember/object';
|
||||
const { attr } = DS;
|
||||
|
||||
//leader_api_addr (string: <required>) – Address of the leader node in the Raft cluster to which this node is trying to join.
|
||||
|
||||
//retry (bool: false) - Retry joining the Raft cluster in case of failures.
|
||||
|
||||
//leader_ca_cert (string: "") - CA certificate used to communicate with Raft's leader node.
|
||||
|
||||
//leader_client_cert (string: "") - Client certificate used to communicate with Raft's leader node.
|
||||
|
||||
//leader_client_key (string: "") - Client key used to communicate with Raft's leader node.
|
||||
|
||||
export default DS.Model.extend({
|
||||
leaderApiAddr: attr('string', {
|
||||
label: 'Leader API Address',
|
||||
}),
|
||||
retry: attr('boolean', {
|
||||
label: 'Keep retrying to join in case of failures',
|
||||
}),
|
||||
leaderCaCert: attr('string', {
|
||||
label: 'Leader CA Certificate',
|
||||
editType: 'file',
|
||||
}),
|
||||
leaderClientCert: attr('string', {
|
||||
label: 'Leader Client Certificate',
|
||||
editType: 'file',
|
||||
}),
|
||||
leaderClientKey: attr('string', {
|
||||
label: 'Leader Client Key',
|
||||
editType: 'file',
|
||||
}),
|
||||
fields: computed(function() {
|
||||
return expandAttributeMeta(this, [
|
||||
'leaderApiAddr',
|
||||
'leaderCaCert',
|
||||
'leaderClientCert',
|
||||
'leaderClientKey',
|
||||
'retry',
|
||||
]);
|
||||
}),
|
||||
});
|
|
@ -0,0 +1,11 @@
|
|||
import DS from 'ember-data';
|
||||
const { attr } = DS;
|
||||
|
||||
//{"node_id":"1249bfbc-b234-96f3-0c66-07078ac3e16e","address":"127.0.0.1:8201","leader":true,"protocol_version":"3","voter":true}
|
||||
export default DS.Model.extend({
|
||||
address: attr('string'),
|
||||
nodeId: attr('string'),
|
||||
protocolVersion: attr('string'),
|
||||
voter: attr('boolean'),
|
||||
leader: attr('boolean'),
|
||||
});
|
|
@ -16,6 +16,8 @@ Router.map(function() {
|
|||
this.mount('open-api-explorer', { path: '/api-explorer' });
|
||||
this.route('license');
|
||||
this.route('requests', { path: '/metrics/requests' });
|
||||
this.route('storage', { path: '/storage/raft' });
|
||||
this.route('storage-restore', { path: '/storage/raft/restore' });
|
||||
this.route('settings', function() {
|
||||
this.route('index', { path: '/' });
|
||||
this.route('seal');
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
import Route from '@ember/routing/route';
|
||||
import ClusterRoute from 'vault/mixins/cluster-route';
|
||||
|
||||
export default Route.extend(ClusterRoute, {
|
||||
model() {
|
||||
return this.store.findAll('server');
|
||||
},
|
||||
|
||||
actions: {
|
||||
doRefresh() {
|
||||
this.refresh();
|
||||
},
|
||||
},
|
||||
});
|
|
@ -0,0 +1,13 @@
|
|||
import ApplicationSerializer from './application';
|
||||
|
||||
export default ApplicationSerializer.extend({
|
||||
primaryKey: 'node_id',
|
||||
normalizeItems(payload) {
|
||||
if (payload.data && payload.data.config) {
|
||||
// rewrite the payload from data.config.servers to data.keys so we can use the application serializer
|
||||
// on it
|
||||
return payload.data.config.servers.slice(0);
|
||||
}
|
||||
return this._super(payload);
|
||||
},
|
||||
});
|
|
@ -27,6 +27,7 @@ const API_PATHS = {
|
|||
replication: 'sys/replication',
|
||||
license: 'sys/license',
|
||||
seal: 'sys/seal',
|
||||
raft: 'sys/storage/raft/configuration',
|
||||
},
|
||||
metrics: {
|
||||
requests: 'sys/internal/counters/requests',
|
||||
|
|
|
@ -1,31 +1,4 @@
|
|||
.http-requests-table {
|
||||
& .is-collapsed {
|
||||
visibility: collapse;
|
||||
}
|
||||
|
||||
& th,
|
||||
td {
|
||||
padding: $spacing-s;
|
||||
}
|
||||
|
||||
& th {
|
||||
color: $grey-dark;
|
||||
font-weight: 500;
|
||||
font-size: $size-8;
|
||||
}
|
||||
|
||||
& tbody th {
|
||||
font-size: $size-7;
|
||||
}
|
||||
|
||||
& tr {
|
||||
border-bottom: 1px solid $grey-light;
|
||||
}
|
||||
|
||||
& td {
|
||||
color: $grey-darkest;
|
||||
}
|
||||
|
||||
& .percent-change {
|
||||
font-weight: 500;
|
||||
font-size: $size-7;
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
.raft-join .field {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.raft-join .box.is-fullwidth {
|
||||
padding-top: $spacing-s;
|
||||
padding-bottom: $spacing-s;
|
||||
}
|
||||
.raft-join-unseal {
|
||||
color: $orange;
|
||||
font-size: $size-6;
|
||||
display: inline-block;
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
.vlt-table {
|
||||
.is-collapsed {
|
||||
visibility: collapse;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: $spacing-s;
|
||||
}
|
||||
|
||||
th {
|
||||
color: $grey-dark;
|
||||
font-weight: 500;
|
||||
font-size: $size-8;
|
||||
}
|
||||
|
||||
tbody th {
|
||||
font-size: $size-7;
|
||||
}
|
||||
|
||||
tr {
|
||||
border-bottom: 1px solid $grey-light;
|
||||
}
|
||||
|
||||
td {
|
||||
color: $grey-darkest;
|
||||
}
|
||||
|
||||
td.middle {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
td.no-padding {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
|
@ -55,8 +55,8 @@
|
|||
@import './components/form-section';
|
||||
@import './components/global-flash';
|
||||
@import './components/hover-copy-button';
|
||||
@import './components/http-requests-table';
|
||||
@import './components/http-requests-bar-chart';
|
||||
@import './components/http-requests-table';
|
||||
@import './components/init-illustration';
|
||||
@import './components/info-table-row';
|
||||
@import './components/input-hint';
|
||||
|
@ -73,6 +73,7 @@
|
|||
@import './components/page-header';
|
||||
@import './components/popup-menu';
|
||||
@import './components/radial-progress';
|
||||
@import './components/raft-join';
|
||||
@import './components/role-item';
|
||||
@import './components/search-select';
|
||||
@import './components/shamir-progress';
|
||||
|
@ -87,6 +88,7 @@
|
|||
@import './components/ui-wizard';
|
||||
@import './components/vault-loading';
|
||||
@import './components/vlt-radio';
|
||||
@import './components/vlt-table';
|
||||
|
||||
// bulma-free-zone
|
||||
@import './components/hs-icon';
|
||||
|
|
|
@ -17,3 +17,9 @@
|
|||
position: absolute;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.checkbox-help-text {
|
||||
font-size: $size-7;
|
||||
color: $ui-gray-700;
|
||||
padding-left: 28px;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
<div class="field">
|
||||
<div class="control is-expanded">
|
||||
<label class="is-label">
|
||||
{{#if label}}
|
||||
{{label}}
|
||||
{{/if}}
|
||||
</label>
|
||||
<div class="file has-name is-fullwidth">
|
||||
<label class="file-label">
|
||||
<input class="file-input" type="file" onchange={{action "pickedFile"}} data-test-file-input>
|
||||
<span class="file-cta button">
|
||||
<Icon @glyph="upload" class="has-light-grey-text" />
|
||||
Choose a file…
|
||||
</span>
|
||||
<span class="file-name has-text-grey-dark" data-test-text-file-input-label=true>
|
||||
{{or this.fileName "No file chosen"}}
|
||||
</span>
|
||||
{{#if this.fileName}}
|
||||
<button type="button" class="file-delete-button" {{action 'clearFile'}} data-test-text-clear>
|
||||
<Icon @glyph="cancel-circle-outline" />
|
||||
</button>
|
||||
{{/if}}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{{#if this.fileName}}
|
||||
<p class="help has-text-grey">
|
||||
This file is {{this.fileSize}} and was created on {{date-format this.fileLastModified 'MMM DD, YYYY hh:mm:ss A'}}.
|
||||
</p>
|
||||
{{/if}}
|
||||
{{#if @fileHelpText}}
|
||||
<p class="help has-text-grey">
|
||||
{{@fileHelpText}}
|
||||
</p>
|
||||
{{/if}}
|
||||
</div>
|
|
@ -1,4 +1,4 @@
|
|||
<div class="http-requests-table">
|
||||
<div class="http-requests-table vlt-table">
|
||||
<table class="is-fullwidth">
|
||||
<caption class="is-collapsed">HTTP Request Volume</caption>
|
||||
<thead class="has-text-weight-semibold">
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
<AlertBanner
|
||||
@type="warning"
|
||||
@message="Vault is sealed"
|
||||
@yieldWithoutColumn={{true}}
|
||||
class="is-marginless"
|
||||
>
|
||||
<div class="is-flex is-flex-v-centered">
|
||||
<div>
|
||||
<span class="message-title">Warning</span> Vault is sealed
|
||||
</div>
|
||||
</div>
|
||||
</AlertBanner>
|
||||
{{#if this.showJoinForm}}
|
||||
<div class="box is-marginless is-shadowless">
|
||||
<h2 class="title is-5" data-test-join-header>
|
||||
Join an existing Raft cluster
|
||||
</h2>
|
||||
<EditForm
|
||||
@model={{compute (action "newModel")}}
|
||||
@saveButtonText="Join"
|
||||
@cancelButtonText="Back"
|
||||
@onCancel={{action (mut this.showJoinForm) false}}
|
||||
@onSave={{transition-to "vault.cluster.unseal"}}
|
||||
@flashEnabled={{false}}
|
||||
@includeBox={{false}}
|
||||
/>
|
||||
</div>
|
||||
{{else}}
|
||||
<form onsubmit={{action "advanceFirstScreen" }} data-test-join-choice>
|
||||
<div class="box is-marginless is-shadowless">
|
||||
<h2 class="title is-6">
|
||||
This server is configured to use Raft Storage.
|
||||
<br />
|
||||
<br />
|
||||
How do you want to get started?
|
||||
</h2>
|
||||
<RadioButton
|
||||
@value="join"
|
||||
@groupValue={{this.preference}}
|
||||
@changed={{action (mut this.preference) }}
|
||||
@name="setup-pref"
|
||||
@radioId="join"
|
||||
@classNames="vlt-radio is-block"
|
||||
>
|
||||
<label for="join" />
|
||||
Join an existing Raft cluster
|
||||
</RadioButton>
|
||||
<RadioButton
|
||||
@value="init"
|
||||
@groupValue={{this.preference}}
|
||||
@changed={{action (mut this.preference) }}
|
||||
@name="setup-pref"
|
||||
@radioId="init"
|
||||
@classNames="vlt-radio is-block"
|
||||
>
|
||||
<label for="init" data-test-join-init />
|
||||
Create a new Raft cluster
|
||||
</RadioButton>
|
||||
</div>
|
||||
<div class="box is-marginless is-shadowless">
|
||||
<button type="submit" class="button is-primary" data-test-next>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{{/if}}
|
|
@ -0,0 +1,109 @@
|
|||
<PageHeader as |p|>
|
||||
<p.levelLeft>
|
||||
<h1 class="title is-3">
|
||||
Raft Storage
|
||||
</h1>
|
||||
</p.levelLeft>
|
||||
</PageHeader>
|
||||
<Toolbar>
|
||||
<ToolbarActions>
|
||||
<BasicDropdown
|
||||
@class="popup-menu"
|
||||
@horizontalPosition="auto-right"
|
||||
@verticalPosition="below"
|
||||
as |D|
|
||||
>
|
||||
<D.trigger
|
||||
data-test-popup-menu-trigger="true"
|
||||
@class={{concat "toolbar-link" (if D.isOpen " is-active")}}
|
||||
@tagName="button"
|
||||
>
|
||||
Snapshots
|
||||
<Chevron @direction="down" @isButton={{true}} />
|
||||
</D.trigger>
|
||||
<D.content @class="popup-menu-content">
|
||||
<nav class="box menu">
|
||||
<ul class="menu-list">
|
||||
<li class="action">
|
||||
{{#if this.useServiceWorker}}
|
||||
<a href="/v1/sys/storage/raft/snapshot" onclick={{queue (action "downloadViaServiceWorker") (action D.actions.close)}}>
|
||||
Download
|
||||
</a>
|
||||
{{else}}
|
||||
<button type="button" class="link is-ghost" onclick={{queue (action "downloadSnapshot") (action D.actions.close)}}>
|
||||
Download
|
||||
</button>
|
||||
{{/if}}
|
||||
</li>
|
||||
<li class="action">
|
||||
{{#link-to "vault.cluster.storage-restore"}}
|
||||
Restore
|
||||
{{/link-to}}
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</D.content>
|
||||
</BasicDropdown>
|
||||
</ToolbarActions>
|
||||
</Toolbar>
|
||||
|
||||
<table class="vlt-table is-fullwidth">
|
||||
<caption class="is-collapsed">Raft servers</caption>
|
||||
<thead class="has-text-weight-semibold">
|
||||
<tr>
|
||||
<th scope="col">Address</th>
|
||||
<th scope="col">Voter</th>
|
||||
<th scope="col"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each @model as |server|}}
|
||||
<tr data-raft-row>
|
||||
<td>
|
||||
{{server.address}}
|
||||
{{#if server.leader}}
|
||||
<span class="tag">Leader</span>
|
||||
{{/if}}
|
||||
</td>
|
||||
<td>
|
||||
|
||||
{{#if server.voter}}
|
||||
<Icon
|
||||
aria-label="Yes"
|
||||
class="icon-true has-text-success"
|
||||
@size="l"
|
||||
@glyph="check-circle-outline"
|
||||
/>
|
||||
{{else}}
|
||||
<Icon
|
||||
aria-label="No"
|
||||
class="icon-false"
|
||||
@size="l"
|
||||
@glyph="cancel-square-outline"
|
||||
/>
|
||||
{{/if}}
|
||||
</td>
|
||||
<td class="middle no-padding has-text-right">
|
||||
<PopupMenu>
|
||||
<Confirm as |c|>
|
||||
<nav>
|
||||
<ul>
|
||||
<li class="action">
|
||||
<c.Message
|
||||
@id={{server.nodeId}}
|
||||
@onConfirm={{action "removePeer" server}}
|
||||
@triggerText="Remove Peer"
|
||||
@confirmButtonText="Remove"
|
||||
@title={{concat "Remove " server.nodeId "?"}}
|
||||
@message="This will remove the server from the raft cluster."
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</Confirm>
|
||||
</PopupMenu>
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
|
@ -0,0 +1,70 @@
|
|||
<PageHeader as |p|>
|
||||
<p.top>
|
||||
<nav class="key-value-header breadcrumb">
|
||||
<ul>
|
||||
<li>
|
||||
<span class="sep">/</span>
|
||||
{{#link-to "vault.cluster.storage"}}
|
||||
Raft Storage
|
||||
{{/link-to}}
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</p.top>
|
||||
<p.levelLeft>
|
||||
<h1 class="title is-3">
|
||||
Restore Snapshot
|
||||
</h1>
|
||||
</p.levelLeft>
|
||||
</PageHeader>
|
||||
|
||||
<form {{action (perform this.restore this.file) on="submit"}}>
|
||||
<MessageError @errors={{this.errors}} />
|
||||
|
||||
{{#if this.isUploading}}
|
||||
<div class="box is-sideless is-fullwidth is-marginless">
|
||||
<AlertBanner
|
||||
@type="warning"
|
||||
@title="Uploading your file..."
|
||||
@message="Raft snapshots can be very large files. Uploading the snapshot may take some time."
|
||||
/>
|
||||
</div>
|
||||
<div class="box is-fullwidth is-shadowless">
|
||||
<button type="button" class="button" onclick={{action "cancelUpload"}}>
|
||||
Cancel upload
|
||||
</button>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="box is-sideless is-fullwidth is-marginless">
|
||||
<AlertBanner
|
||||
@type="warning"
|
||||
@title="This might take a while"
|
||||
@message="Raft snapshots can be very large files. Uploading the snapshot may take some time."
|
||||
/>
|
||||
<FileToArrayBuffer @onChange={{action (mut file)}} />
|
||||
<div class="b-checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="force-restore"
|
||||
class="styled"
|
||||
checked={{this.forceRestore}}
|
||||
onchange={{action
|
||||
(mut this.forceRestore)
|
||||
value="target.checked"
|
||||
}}
|
||||
/>
|
||||
<label for="force-restore" class="is-label">
|
||||
Force restore
|
||||
</label>
|
||||
<p class="checkbox-help-text">
|
||||
Bypass checks to ensure the AutoUnseal or Shamir keys are consistent with the snapshot data.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<FormSaveButtons
|
||||
@saveButtonText="Restore"
|
||||
@isSaving={{this.restore.isRunning}}
|
||||
@cancelLinkParams={{array "vault.cluster.storage"}}
|
||||
/>
|
||||
{{/if}}
|
||||
</form>
|
|
@ -62,25 +62,41 @@
|
|||
<hr/>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{#if (has-permission 'status' routeParams='license')}}
|
||||
{{/unless}}
|
||||
{{#if (or
|
||||
(and version.features (has-permission 'status' routeParams='license'))
|
||||
(and cluster.usingRaft (has-permission 'status' routeParams='raft'))
|
||||
)
|
||||
}}
|
||||
<nav class="menu">
|
||||
<div class="menu-label">
|
||||
License
|
||||
Server
|
||||
</div>
|
||||
<ul class="menu-list">
|
||||
<li class="action">
|
||||
{{#link-to "vault.cluster.license" activeCluster.name invokeAction=onLinkClick}}
|
||||
<div class="level is-mobile">
|
||||
<span class="level-left">See details</span>
|
||||
<Chevron class="has-text-grey-light level-right" />
|
||||
</div>
|
||||
{{/link-to}}
|
||||
</li>
|
||||
{{#if (and version.features (has-permission 'status' routeParams='license'))}}
|
||||
<li class="action">
|
||||
{{#link-to "vault.cluster.license" activeCluster.name invokeAction=onLinkClick}}
|
||||
<div class="level is-mobile">
|
||||
<span class="level-left">License</span>
|
||||
<Chevron class="has-text-grey-light level-right" />
|
||||
</div>
|
||||
{{/link-to}}
|
||||
</li>
|
||||
{{/if}}
|
||||
{{#if (and cluster.usingRaft (has-permission 'status' routeParams='raft'))}}
|
||||
<li class="action">
|
||||
{{#link-to "vault.cluster.storage" activeCluster.name invokeAction=onLinkClick}}
|
||||
<div class="level is-mobile">
|
||||
<span class="level-left">Raft Storage</span>
|
||||
<Chevron class="has-text-grey-light level-right" />
|
||||
</div>
|
||||
{{/link-to}}
|
||||
</li>
|
||||
{{/if}}
|
||||
</ul>
|
||||
</nav>
|
||||
<hr/>
|
||||
{{/if}}
|
||||
{{/unless}}
|
||||
<nav class="menu">
|
||||
<div class="menu-label">
|
||||
Seal status
|
||||
|
|
|
@ -1,5 +1,14 @@
|
|||
<SplashPage as |Page|>
|
||||
{{#if keyData}}
|
||||
{{#if (and this.model.usingRaft (not this.prefersInit))}}
|
||||
<Page.header>
|
||||
<h1 class="title is-4">
|
||||
Raft Storage
|
||||
</h1>
|
||||
</Page.header>
|
||||
<Page.content>
|
||||
<RaftJoin @onDismiss={{action (mut prefersInit) true}} />
|
||||
</Page.content>
|
||||
{{else if keyData}}
|
||||
<Page.header>
|
||||
{{#let (or keyData.recovery_keys keyData.keys) as |keyArray|}}
|
||||
<h1 class="title is-4">
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
<RaftStorageRestore />
|
|
@ -0,0 +1 @@
|
|||
<RaftStorageOverview @model={{this.model}} />
|
|
@ -6,6 +6,7 @@ module.exports = function(environment) {
|
|||
modulePrefix: 'vault',
|
||||
environment: environment,
|
||||
rootURL: '/ui/',
|
||||
serviceWorkerScope: '/v1/sys/storage/raft/snapshot',
|
||||
locationType: 'auto',
|
||||
EmberENV: {
|
||||
FEATURES: {
|
||||
|
|
|
@ -11,6 +11,10 @@ const isCI = !!process.env.CI;
|
|||
|
||||
module.exports = function(defaults) {
|
||||
var app = new EmberApp(defaults, {
|
||||
'ember-service-worker': {
|
||||
serviceWorkerScope: config.serviceWorkerScope,
|
||||
skipWaitingOnMessage: true,
|
||||
},
|
||||
svgJar: {
|
||||
//optimize: false,
|
||||
//paths: [],
|
||||
|
|
|
@ -14,6 +14,7 @@ import layout from '../templates/components/alert-banner';
|
|||
*
|
||||
* @param type=null {String} - The banner type. This comes from the message-types helper.
|
||||
* @param [message=null {String}] - The message to display within the banner.
|
||||
* @param [title=null {String}] - A title to show above the message. If this is not provided, there are default values for each type of alert.
|
||||
*
|
||||
*/
|
||||
|
||||
|
@ -21,6 +22,7 @@ export default Component.extend({
|
|||
layout,
|
||||
type: null,
|
||||
message: null,
|
||||
title: null,
|
||||
yieldWithoutColumn: false,
|
||||
classNameBindings: ['containerClass'],
|
||||
|
||||
|
|
|
@ -15,7 +15,10 @@ export default Component.extend({
|
|||
deleteSuccessMessage: 'Deleted!',
|
||||
deleteButtonText: 'Delete',
|
||||
saveButtonText: 'Save',
|
||||
cancelButtonText: 'Cancel',
|
||||
cancelLink: null,
|
||||
flashEnabled: true,
|
||||
includeBox: true,
|
||||
|
||||
/*
|
||||
* @param Function
|
||||
|
@ -42,7 +45,9 @@ export default Component.extend({
|
|||
}
|
||||
return;
|
||||
}
|
||||
this.flashMessages.success(this.get(messageKey));
|
||||
if (this.flashEnabled) {
|
||||
this.flashMessages.success(this.get(messageKey));
|
||||
}
|
||||
if (this.callOnSaveAfterRender) {
|
||||
next(() => {
|
||||
this.onSave({ saveType: method, model });
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
import Component from '@ember/component';
|
||||
import layout from '../templates/components/form-save-buttons';
|
||||
|
||||
/**
|
||||
* @module FormSaveButtons
|
||||
* `FormSaveButtons` displays a button save and a cancel button at the bottom of a form.
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* <FormSaveButtons @saveButtonText="Save" @isSaving={{isSaving}} @cancelLinkParams={{array
|
||||
* "foo.route"}} />
|
||||
* ```
|
||||
*
|
||||
* @param [saveButtonText="Save" {String}] - The text that will be rendered on the Save button.
|
||||
* @param [isSaving=false {Boolean}] - If the form is saving, this should be true. This will disable the save button and render a spinner on it;
|
||||
* @param [cancelLinkParams=[] {Array}] - An array of arguments used to construct a link to navigate back to when the Cancel button is clicked.
|
||||
* @param [onCancel=null {Fuction}] - If the form should call an action on cancel instead of route somewhere, the fucntion can be passed using onCancel instead of passing an array to cancelLinkParams.
|
||||
* @param [includeBox=true {Boolean}] - By default we include padding around the form with underlines. Passing this value as false will remove that padding.
|
||||
*
|
||||
*/
|
||||
|
||||
export default Component.extend({
|
||||
layout,
|
||||
tagName: '',
|
||||
});
|
|
@ -1,6 +1,6 @@
|
|||
<form {{action (perform save model) on="submit"}}>
|
||||
<MessageError @model={{model}} data-test-edit-form-error />
|
||||
<div class="box is-sideless is-fullwidth is-marginless">
|
||||
<div class="{{if this.includeBox 'box is-sideless is-fullwidth is-marginless'}}">
|
||||
<NamespaceReminder @mode="save" />
|
||||
{{#if (or model.fields model.attrs)}}
|
||||
{{#each (or model.fields model.attrs) as |attr|}}
|
||||
|
@ -10,21 +10,11 @@
|
|||
<FormFieldGroups @model={{model}} @mode={{@mode}} />
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="field is-grouped is-grouped-split is-fullwidth box is-bottomless">
|
||||
<div class="field is-grouped">
|
||||
<div class="control">
|
||||
<button type="submit" data-test-edit-form-submit class="button is-primary {{if save.isRunning 'loading'}}"
|
||||
disabled={{save.isRunning}}>
|
||||
{{saveButtonText}}
|
||||
</button>
|
||||
</div>
|
||||
{{#if cancelLinkParams}}
|
||||
<div class="control">
|
||||
{{#link-to params=cancelLinkParams class="button"}}
|
||||
Cancel
|
||||
{{/link-to}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
<FormSaveButtons
|
||||
@isSaving={{this.save.isRunning}}
|
||||
@saveButtonText={{this.saveButtonText}}
|
||||
@canceLinkParams={{@canceLinkParams}}
|
||||
@includeBox={{this.includeBox}}
|
||||
@onCancel={{@onCancel}}
|
||||
/>
|
||||
</form>
|
|
@ -0,0 +1,24 @@
|
|||
<div class="field is-grouped is-grouped-split is-fullwidth box is-bottomless {{if (eq @includeBox false) 'is-shadowless'}}">
|
||||
<div class="field is-grouped">
|
||||
<div class="control">
|
||||
<button type="submit" data-test-edit-form-submit class="button is-primary {{if @isSaving 'loading'}}"
|
||||
disabled={{@isSaving}}>
|
||||
{{or @saveButtonText "Save"}}
|
||||
</button>
|
||||
</div>
|
||||
{{#if @cancelLinkParams}}
|
||||
<div class="control">
|
||||
{{#link-to params=@cancelLinkParams class="button"}}
|
||||
{{or @cancelButtonText "Cancel"}}
|
||||
{{/link-to}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if @onCancel}}
|
||||
<div class="control">
|
||||
<button type="button" class="button" onclick={{action onCancel}} data-test-cancel-button>
|
||||
{{or @cancelButtonText "Cancel"}}
|
||||
</button>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1 @@
|
|||
export { default } from 'core/components/form-save-buttons';
|
|
@ -1,13 +1,15 @@
|
|||
<!--THIS FILE IS AUTO GENERATED. This file is generated from JSDoc comments in app/components/alert-banner.js. To make changes, first edit that file and run "yarn gen-story-md alert-banner" to re-generate the content.-->
|
||||
<!--THIS FILE IS AUTO GENERATED. This file is generated from JSDoc comments in lib/core/addon/components/alert-banner.js. To make changes, first edit that file and run "yarn gen-story-md alert-banner" to re-generate the content.-->
|
||||
|
||||
## AlertBanner
|
||||
`AlertBanner` components are used to inform users of important messages.
|
||||
|
||||
**Params**
|
||||
|
||||
| Param | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| type | <code>String</code> | <code></code> | The banner type. This comes from the message-types helper. |
|
||||
| [message] | <code>String</code> | <code></code> | The message to display within the banner. |
|
||||
| [title] | <code>String</code> | <code></code> | A title to show above the message. If this is not provided, there are default values for each type of alert. |
|
||||
|
||||
**Example**
|
||||
|
||||
|
@ -17,7 +19,7 @@
|
|||
|
||||
**See**
|
||||
|
||||
- [Uses of AlertBanner](https://github.com/hashicorp/vault/search?l=Handlebars&q=AlertBanner)
|
||||
- [AlertBanner Source Code](https://github.com/hashicorp/vault/blob/master/ui/app/components/alert-banner.js)
|
||||
- [Uses of AlertBanner](https://github.com/hashicorp/vault/search?l=Handlebars&q=AlertBanner+OR+alert-banner)
|
||||
- [AlertBanner Source Code](https://github.com/hashicorp/vault/blob/master/ui/lib/core/addon/components/alert-banner.js)
|
||||
|
||||
---
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
<!--THIS FILE IS AUTO GENERATED. This file is generated from JSDoc comments in lib/core/addon/components/form-save-buttons.js. To make changes, first edit that file and run "yarn gen-story-md form-save-buttons" to re-generate the content.-->
|
||||
|
||||
## FormSaveButtons
|
||||
`FormSaveButtons` displays a button save and a cancel button at the bottom of a form.
|
||||
|
||||
**Params**
|
||||
|
||||
| Param | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| [saveButtonText] | <code>String</code> | <code>"Save"</code> | The text that will be rendered on the Save button. |
|
||||
| [isSaving] | <code>Boolean</code> | <code>false</code> | If the form is saving, this should be true. This will disable the save button and render a spinner on it; |
|
||||
| [cancelLinkParams] | <code>Array</code> | <code>[]</code> | An array of arguments used to construct a link to navigate back to when the Cancel button is clicked. |
|
||||
| [onCancel] | <code>Fuction</code> | <code></code> | If the form should call an action on cancel instead of route somewhere, the fucntion can be passed using onCancel instead of passing an array to cancelLinkParams. |
|
||||
| [includeBox] | <code>Boolean</code> | <code>true</code> | By default we include padding around the form with underlines. Passing this value as false will remove that padding. |
|
||||
|
||||
**Example**
|
||||
|
||||
```js
|
||||
<FormSaveButtons @saveButtonText="Save" @isSaving={{isSaving}} @cancelLinkParams={{array
|
||||
"foo.route"}} />
|
||||
```
|
||||
|
||||
**See**
|
||||
|
||||
- [Uses of FormSaveButtons](https://github.com/hashicorp/vault/search?l=Handlebars&q=FormSaveButtons+OR+form-save-buttons)
|
||||
- [FormSaveButtons Source Code](https://github.com/hashicorp/vault/blob/master/ui/lib/core/addon/components/form-save-buttons.js)
|
||||
|
||||
---
|
|
@ -0,0 +1,38 @@
|
|||
import hbs from 'htmlbars-inline-precompile';
|
||||
import { storiesOf } from '@storybook/ember';
|
||||
import { withKnobs, text, boolean } from '@storybook/addon-knobs';
|
||||
import notes from './form-save-buttons.md';
|
||||
|
||||
storiesOf('FormSaveButtons/', module)
|
||||
.addParameters({ options: { showPanel: true } })
|
||||
.addDecorator(
|
||||
withKnobs({
|
||||
escapeHTML: false,
|
||||
})
|
||||
)
|
||||
.add(
|
||||
`FormSaveButtons`,
|
||||
() => ({
|
||||
template: hbs`
|
||||
<h5 class="title is-5">Form save buttons</h5>
|
||||
<FormSaveButtons
|
||||
@isSaving={{this.save}}
|
||||
@saveButtonText={{this.saveButtonText}}
|
||||
@cancelButtonText={{this.cancelButtonText}}
|
||||
@includeBox={{this.includeBox}}
|
||||
@onCancel={{this.onCancel}}
|
||||
/>
|
||||
`,
|
||||
|
||||
context: {
|
||||
save: boolean('saving?', false),
|
||||
includeBox: boolean('include box?', true),
|
||||
saveButtonText: text('save button text', 'Save'),
|
||||
cancelButtonText: text('cancel button text', 'Cancel'),
|
||||
onCancel: () => {
|
||||
console.log('Canceled!');
|
||||
},
|
||||
},
|
||||
}),
|
||||
{ notes }
|
||||
);
|
|
@ -0,0 +1,16 @@
|
|||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
name: require('./package').name,
|
||||
|
||||
isDevelopingAddon() {
|
||||
return true;
|
||||
},
|
||||
|
||||
serverMiddleware({ app }) {
|
||||
app.use((req, res, next) => {
|
||||
res.setHeader('Service-Worker-Allowed', '/');
|
||||
next();
|
||||
});
|
||||
},
|
||||
};
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"name": "service-worker-authenticated-download",
|
||||
"keywords": [
|
||||
"ember-addon",
|
||||
"ember-service-worker-plugin"
|
||||
],
|
||||
"ember-addon": {
|
||||
"before": [
|
||||
"serve-files-middleware",
|
||||
"broccoli-serve-files",
|
||||
"history-support-middleware",
|
||||
"proxy-server-middleware"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"ember-cli-babel": "*",
|
||||
"ember-auto-import": "*",
|
||||
"ember-source": "*",
|
||||
"ember-service-worker": "*"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
import { addSuccessHandler } from 'ember-service-worker/service-worker-registration';
|
||||
import Namespace from '@ember/application/namespace';
|
||||
|
||||
function getToken() {
|
||||
// fix this later by allowing registration somewhere in the app lifecycle were we can have access to
|
||||
// services, etc.
|
||||
return Namespace.NAMESPACES_BY_ID['vault'].__container__.lookup('service:auth').currentToken;
|
||||
}
|
||||
|
||||
addSuccessHandler(function(registration) {
|
||||
// attach the handler for the message event so we can send over the auth token
|
||||
navigator.serviceWorker.addEventListener('message', event => {
|
||||
let { action } = event.data;
|
||||
let port = event.ports[0];
|
||||
|
||||
if (action === 'getToken') {
|
||||
let token = getToken();
|
||||
if (!token) {
|
||||
console.error('Unable to retrieve Vault tokent');
|
||||
}
|
||||
port.postMessage({ token: token });
|
||||
} else {
|
||||
console.error('Unknown event', event);
|
||||
port.postMessage({
|
||||
error: 'Unknown request',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// attempt to unregister the service worker on unload because we're not doing any sort of caching
|
||||
window.addEventListener('unload', function() {
|
||||
registration.unregister();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,51 @@
|
|||
import { createUrlRegEx, urlMatchesAnyPattern } from 'ember-service-worker/service-worker/url-utils';
|
||||
|
||||
var patterns = ['/v1/sys/storage/raft/snapshot'];
|
||||
var REGEXES = patterns.map(createUrlRegEx);
|
||||
|
||||
function sendMessage(message) {
|
||||
return self.clients.matchAll({ includeUncontrolled: true, type: 'window' }).then(function(results) {
|
||||
var client = results[0];
|
||||
return new Promise(function(resolve, reject) {
|
||||
var messageChannel = new MessageChannel();
|
||||
messageChannel.port2.onmessage = function(event) {
|
||||
if (event.data.error) {
|
||||
reject(event.data.error);
|
||||
} else {
|
||||
resolve(event.data.token);
|
||||
}
|
||||
};
|
||||
|
||||
client.postMessage(message, [messageChannel.port1]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function authenticateRequest(request) {
|
||||
// copy the reaquest headers so we can mutate them
|
||||
let headers = new Headers(request.headers);
|
||||
|
||||
// get and set vault token so the request is authenticated
|
||||
return sendMessage({ action: 'getToken' }).then(function(token) {
|
||||
headers.set('X-Vault-Token', token);
|
||||
|
||||
// continue the fetch with the new request
|
||||
// that has the auth header
|
||||
return fetch(
|
||||
new Request(request.url, {
|
||||
method: request.method,
|
||||
headers,
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
self.addEventListener('fetch', function(fetchEvent) {
|
||||
const request = fetchEvent.request;
|
||||
|
||||
if (urlMatchesAnyPattern(request.url, REGEXES) && request.method === 'GET') {
|
||||
return fetchEvent.respondWith(authenticateRequest(request));
|
||||
} else {
|
||||
return fetchEvent.respondWith(fetch(request));
|
||||
}
|
||||
});
|
|
@ -107,6 +107,7 @@
|
|||
"ember-resolver": "^5.0.1",
|
||||
"ember-responsive": "^3.0.0-beta.3",
|
||||
"ember-router-helpers": "^0.2.0",
|
||||
"ember-service-worker": "meirish/ember-service-worker#configurable-scope",
|
||||
"ember-sinon": "^4.0.0",
|
||||
"ember-source": "~3.8.0",
|
||||
"ember-svg-jar": "^2.1.0",
|
||||
|
@ -117,6 +118,7 @@
|
|||
"eslint-config-prettier": "^6.0.0",
|
||||
"eslint-plugin-ember": "^6.7.0",
|
||||
"eslint-plugin-prettier": "^3.1.0",
|
||||
"filesize": "^4.2.1",
|
||||
"flat": "^4.1.0",
|
||||
"ivy-codemirror": "IvyApp/ivy-codemirror#fb09333c5144da47e14a9e6260f80577d5408374",
|
||||
"jsonlint": "^1.6.3",
|
||||
|
@ -163,7 +165,8 @@
|
|||
"lib/css",
|
||||
"lib/kmip",
|
||||
"lib/open-api-explorer",
|
||||
"lib/replication"
|
||||
"lib/replication",
|
||||
"lib/service-worker-authenticated-download"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
BIN
ui/public/favicon.png (Stored with Git LFS)
BIN
ui/public/favicon.png (Stored with Git LFS)
Binary file not shown.
|
@ -0,0 +1,26 @@
|
|||
<!--THIS FILE IS AUTO GENERATED. This file is generated from JSDoc comments in app/components/file-to-array-buffer.js. To make changes, first edit that file and run "yarn gen-story-md file-to-array-buffer" to re-generate the content.-->
|
||||
|
||||
## FileToArrayBuffer
|
||||
`FileToArrayBuffer` is a component that will allow you to pick a file from the local file system. Once
|
||||
loaded, this file will be emitted as a JS ArrayBuffer to the passed `onChange` callback.
|
||||
|
||||
**Params**
|
||||
|
||||
| Param | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| onChange | <code>function</code> | <code></code> | The function to call when the file read is complete. This function recieves the file as a JS ArrayBuffer |
|
||||
| [label] | <code>String</code> | <code></code> | Text to use as the label for the file input |
|
||||
| fileHelpText | <code>String</code> | <code></code> | Text to use as help under the file input |
|
||||
|
||||
**Example**
|
||||
|
||||
```js
|
||||
<FileToArrayBuffer @onChange={{action (mut file)}} />
|
||||
```
|
||||
|
||||
**See**
|
||||
|
||||
- [Uses of FileToArrayBuffer](https://github.com/hashicorp/vault/search?l=Handlebars&q=FileToArrayBuffer+OR+file-to-array-buffer)
|
||||
- [FileToArrayBuffer Source Code](https://github.com/hashicorp/vault/blob/master/ui/app/components/file-to-array-buffer.js)
|
||||
|
||||
---
|
|
@ -0,0 +1,31 @@
|
|||
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
import { storiesOf } from '@storybook/ember';
|
||||
import { withKnobs, text } from '@storybook/addon-knobs';
|
||||
|
||||
import notes from './file-to-array-buffer.md';
|
||||
|
||||
storiesOf('FileToArrayBuffer/', module)
|
||||
.addParameters({ options: { showPanel: true } })
|
||||
.addDecorator(
|
||||
withKnobs()
|
||||
)
|
||||
.add(`FileToArrayBuffer`, () => ({
|
||||
template: hbs`
|
||||
<h5 class="title is-5">File To Array Buffer</h5>
|
||||
<FileToArrayBuffer @onChange={{this.onChange}} @label={{this.label}}
|
||||
@fileHelpText={{this.fileHelpText}} />
|
||||
{{#if this.fileName}}
|
||||
{{this.fileName}} as bytes: {{this.fileBytes}}
|
||||
{{/if}}
|
||||
`,
|
||||
context: {
|
||||
onChange(file, name) {
|
||||
console.log(`${name} contents as an ArrayBuffer:`, file);
|
||||
},
|
||||
label: text('Label'),
|
||||
fileHelpText: text('Help text'),
|
||||
},
|
||||
}),
|
||||
{notes}
|
||||
);
|
|
@ -0,0 +1,37 @@
|
|||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { click, render } from '@ember/test-helpers';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
import sinon from 'sinon';
|
||||
|
||||
module('Integration | Component | raft-join', function(hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
test('it renders', async function(assert) {
|
||||
await render(hbs`<RaftJoin />`);
|
||||
assert.dom('[data-test-join-choice]').exists();
|
||||
});
|
||||
|
||||
test('it shows the join form when clicking next', async function(assert) {
|
||||
await render(hbs`<RaftJoin />`);
|
||||
await click('[data-test-next]');
|
||||
assert.dom('[data-test-join-header]').exists();
|
||||
});
|
||||
test('it returns to the first screen when clicking back', async function(assert) {
|
||||
await render(hbs`<RaftJoin />`);
|
||||
await click('[data-test-next]');
|
||||
assert.dom('[data-test-join-header]').exists();
|
||||
await click('[data-test-cancel-button]');
|
||||
assert.dom('[data-test-join-choice]').exists();
|
||||
});
|
||||
|
||||
test('it calls onDismiss when a user chooses to init', async function(assert) {
|
||||
let spy = sinon.spy();
|
||||
this.set('onDismiss', spy);
|
||||
await render(hbs`<RaftJoin @onDismiss={{onDismiss}} />`);
|
||||
|
||||
await click('[data-test-join-init]');
|
||||
await click('[data-test-next]');
|
||||
assert.ok(spy.calledOnce, 'it calls the passed onDismiss');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,18 @@
|
|||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { render } from '@ember/test-helpers';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
|
||||
module('Integration | Component | raft-storage-overview', function(hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
test('it renders', async function(assert) {
|
||||
let model = [
|
||||
{ address: '127.0.0.1:8200', voter: true },
|
||||
{ address: '127.0.0.1:8200', voter: true, leader: true },
|
||||
];
|
||||
this.set('model', model);
|
||||
await render(hbs`<RaftStorageOverview @model={{this.model}} />`);
|
||||
assert.dom('[data-raft-row]').exists({ count: 2 });
|
||||
});
|
||||
});
|
61
ui/yarn.lock
61
ui/yarn.lock
|
@ -4338,6 +4338,23 @@ broccoli-test-helper@^2.0.0:
|
|||
tmp "^0.0.33"
|
||||
walk-sync "^0.3.3"
|
||||
|
||||
broccoli-uglify-sourcemap@^2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/broccoli-uglify-sourcemap/-/broccoli-uglify-sourcemap-2.2.0.tgz#2ff49389bdf342a550c3596750ba2dde95a8f7d4"
|
||||
integrity sha1-L/STib3zQqVQw1lnULot3pWo99Q=
|
||||
dependencies:
|
||||
async-promise-queue "^1.0.4"
|
||||
broccoli-plugin "^1.2.1"
|
||||
debug "^3.1.0"
|
||||
lodash.defaultsdeep "^4.6.0"
|
||||
matcher-collection "^1.0.5"
|
||||
mkdirp "^0.5.0"
|
||||
source-map-url "^0.4.0"
|
||||
symlink-or-copy "^1.0.1"
|
||||
terser "^3.7.5"
|
||||
walk-sync "^0.3.2"
|
||||
workerpool "^2.3.0"
|
||||
|
||||
broccoli-uglify-sourcemap@^3.1.0:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/broccoli-uglify-sourcemap/-/broccoli-uglify-sourcemap-3.1.1.tgz#c99342fe1da09ff79653b6184ef8efe0b9bac793"
|
||||
|
@ -7419,6 +7436,22 @@ ember-runtime-enumerable-includes-polyfill@^2.0.0:
|
|||
ember-cli-babel "^6.9.0"
|
||||
ember-cli-version-checker "^2.1.0"
|
||||
|
||||
ember-service-worker@meirish/ember-service-worker#configurable-scope:
|
||||
version "0.8.0"
|
||||
resolved "https://codeload.github.com/meirish/ember-service-worker/tar.gz/86c8dcf5cfc42e1e721b8c7d68afb513a2c288c7"
|
||||
dependencies:
|
||||
broccoli-caching-writer "^3.0.3"
|
||||
broccoli-file-creator "^2.1.1"
|
||||
broccoli-funnel "^2.0.1"
|
||||
broccoli-merge-trees "^3.0.1"
|
||||
broccoli-rollup "^2.1.1"
|
||||
broccoli-uglify-sourcemap "^2.2.0"
|
||||
clone "^2.1.2"
|
||||
ember-cli-babel "^6.16.0"
|
||||
glob "^7.1.3"
|
||||
hash-for-dep "^1.2.3"
|
||||
rollup-plugin-replace "^2.1.0"
|
||||
|
||||
ember-sinon@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ember-sinon/-/ember-sinon-4.0.0.tgz#bb9bc43b68cc4500457261606a47c7b6ef8c30a3"
|
||||
|
@ -8436,6 +8469,11 @@ filesize@^4.1.2:
|
|||
resolved "https://registry.yarnpkg.com/filesize/-/filesize-4.1.2.tgz#fcd570af1353cea97897be64f56183adb995994b"
|
||||
integrity sha512-iSWteWtfNcrWQTkQw8ble2bnonSl7YJImsn9OZKpE2E4IHhXI78eASpDYUljXZZdYj36QsEKjOs/CsiDqmKMJw==
|
||||
|
||||
filesize@^4.2.1:
|
||||
version "4.2.1"
|
||||
resolved "https://registry.yarnpkg.com/filesize/-/filesize-4.2.1.tgz#ab1cb2069db5d415911c1a13e144c0e743bc89bc"
|
||||
integrity sha512-bP82Hi8VRZX/TUBKfE24iiUGsB/sfm2WUrwTQyAzQrhO3V9IhcBBNBXMyzLY5orACxRyYJ3d2HeRVX+eFv4lmA==
|
||||
|
||||
fill-range@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7"
|
||||
|
@ -11278,6 +11316,13 @@ magic-string@^0.24.0:
|
|||
dependencies:
|
||||
sourcemap-codec "^1.4.1"
|
||||
|
||||
magic-string@^0.25.2:
|
||||
version "0.25.3"
|
||||
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.3.tgz#34b8d2a2c7fec9d9bdf9929a3fd81d271ef35be9"
|
||||
integrity sha512-6QK0OpF/phMz0Q2AxILkX2mFhi7m+WMwTRg0LQKq/WBB0cDP4rYH3Wp4/d3OTXlrPLVJT/RFqj8tFeAR4nk8AA==
|
||||
dependencies:
|
||||
sourcemap-codec "^1.4.4"
|
||||
|
||||
make-dir@^1.0.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c"
|
||||
|
@ -11401,7 +11446,7 @@ marked@^0.7.0:
|
|||
resolved "https://registry.yarnpkg.com/marked/-/marked-0.7.0.tgz#b64201f051d271b1edc10a04d1ae9b74bb8e5c0e"
|
||||
integrity sha512-c+yYdCZJQrsRjTPhUx7VKkApw9bwDkNbHUKo1ovgcfDjb2kc8rLuRbIFyXL5WOEUwzSSKo3IXpph2K6DqB/KZg==
|
||||
|
||||
matcher-collection@^1.0.0, matcher-collection@^1.1.1:
|
||||
matcher-collection@^1.0.0, matcher-collection@^1.0.5, matcher-collection@^1.1.1:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/matcher-collection/-/matcher-collection-1.1.2.tgz#1076f506f10ca85897b53d14ef54f90a5c426838"
|
||||
integrity sha512-YQ/teqaOIIfUHedRam08PB3NK7Mjct6BvzRnJmpGDm8uFXpNr1sbY4yuflI5JcEs6COpYA0FpRQhSDBf1tT95g==
|
||||
|
@ -14300,7 +14345,15 @@ ripemd160@^2.0.0, ripemd160@^2.0.1:
|
|||
hash-base "^3.0.0"
|
||||
inherits "^2.0.1"
|
||||
|
||||
rollup-pluginutils@^2.0.1:
|
||||
rollup-plugin-replace@^2.1.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/rollup-plugin-replace/-/rollup-plugin-replace-2.2.0.tgz#f41ae5372e11e7a217cde349c8b5d5fd115e70e3"
|
||||
integrity sha512-/5bxtUPkDHyBJAKketb4NfaeZjL5yLZdeUihSfbF2PQMz+rSTEb8ARKoOl3UBT4m7/X+QOXJo3sLTcq+yMMYTA==
|
||||
dependencies:
|
||||
magic-string "^0.25.2"
|
||||
rollup-pluginutils "^2.6.0"
|
||||
|
||||
rollup-pluginutils@^2.0.1, rollup-pluginutils@^2.6.0:
|
||||
version "2.8.1"
|
||||
resolved "https://registry.yarnpkg.com/rollup-pluginutils/-/rollup-pluginutils-2.8.1.tgz#8fa6dd0697344938ef26c2c09d2488ce9e33ce97"
|
||||
integrity sha512-J5oAoysWar6GuZo0s+3bZ6sVZAC0pfqKz68De7ZgDi5z63jOVZn1uJL/+z1jeKHNbGII8kAyHF5q8LnxSX5lQg==
|
||||
|
@ -15027,7 +15080,7 @@ source-map@~0.1.x:
|
|||
dependencies:
|
||||
amdefine ">=0.0.4"
|
||||
|
||||
sourcemap-codec@^1.4.1:
|
||||
sourcemap-codec@^1.4.1, sourcemap-codec@^1.4.4:
|
||||
version "1.4.6"
|
||||
resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.6.tgz#e30a74f0402bad09807640d39e971090a08ce1e9"
|
||||
integrity sha512-1ZooVLYFxC448piVLBbtOxFcXwnymH9oUF8nRd3CuYDVvkRBxRl6pB4Mtas5a4drtL+E8LDgFkQNcgIw6tc8Hg==
|
||||
|
@ -15688,7 +15741,7 @@ terser@^3.16.1:
|
|||
source-map "~0.6.1"
|
||||
source-map-support "~0.5.9"
|
||||
|
||||
terser@^3.17.0:
|
||||
terser@^3.17.0, terser@^3.7.5:
|
||||
version "3.17.0"
|
||||
resolved "https://registry.yarnpkg.com/terser/-/terser-3.17.0.tgz#f88ffbeda0deb5637f9d24b0da66f4e15ab10cb2"
|
||||
integrity sha512-/FQzzPJmCpjAH9Xvk2paiWrFq+5M6aVOf+2KRbwhByISDX/EujxsK+BAvrhb6H+2rtrLCHK9N01wO014vrIwVQ==
|
||||
|
|
|
@ -33,6 +33,7 @@ type UIConfig struct {
|
|||
func NewUIConfig(enabled bool, physicalStorage physical.Backend, barrierStorage logical.Storage) *UIConfig {
|
||||
defaultHeaders := http.Header{}
|
||||
defaultHeaders.Set("Content-Security-Policy", "default-src 'none'; connect-src 'self'; img-src 'self' data:; script-src 'self'; style-src 'unsafe-inline' 'self'; form-action 'none'; frame-ancestors 'none'")
|
||||
defaultHeaders.Set("Service-Worker-Allowed", "/")
|
||||
|
||||
return &UIConfig{
|
||||
physicalStorage: physicalStorage,
|
||||
|
|
Loading…
Reference in New Issue