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:
Matthew Irish 2019-10-14 13:23:29 -05:00 committed by GitHub
parent e8432f1ebe
commit 87d4e6e068
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
52 changed files with 1156 additions and 72 deletions

View File

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

View File

@ -0,0 +1,7 @@
import ApplicationAdapter from './application';
export default ApplicationAdapter.extend({
urlForCreateRecord() {
return '/v1/sys/storage/raft/join';
},
});

15
ui/app/adapters/server.js Normal file
View File

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

View File

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

View File

@ -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');
},
},
});

View File

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

View File

@ -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();
},
},
});

View File

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

View File

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

View File

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

11
ui/app/models/server.js Normal file
View File

@ -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'),
});

View File

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

View File

@ -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();
},
},
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -17,3 +17,9 @@
position: absolute;
top: 0;
}
.checkbox-help-text {
font-size: $size-7;
color: $ui-gray-700;
padding-left: 28px;
}

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,70 @@
<PageHeader as |p|>
<p.top>
<nav class="key-value-header breadcrumb">
<ul>
<li>
<span class="sep">&#x0002f;</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>

View File

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

View File

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

View File

@ -0,0 +1 @@
<RaftStorageRestore />

View File

@ -0,0 +1 @@
<RaftStorageOverview @model={{this.model}} />

View File

@ -6,6 +6,7 @@ module.exports = function(environment) {
modulePrefix: 'vault',
environment: environment,
rootURL: '/ui/',
serviceWorkerScope: '/v1/sys/storage/raft/snapshot',
locationType: 'auto',
EmberENV: {
FEATURES: {

View File

@ -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: [],

View File

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

View File

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

View File

@ -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: '',
});

View File

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

View File

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

View File

@ -0,0 +1 @@
export { default } from 'core/components/form-save-buttons';

View File

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

View File

@ -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>&quot;Save&quot;</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)
---

View File

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

View File

@ -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();
});
},
};

View File

@ -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": "*"
}
}

View File

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

View File

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

View File

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

Binary file not shown.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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