mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-10-30 18:17:55 +00:00
Ui/replication status discoverability (#8705)
* Sidebranch: add new route on DR secondary (#8640) * setup, not complete * update routing * clean up * add test * add link from status menu * clean up * fixes per pr comments * revert back to two if statements due to refresh bug * Sidebranch: Setup Replication Page as component to be consumed in all pages for project (#8661) * setup, not complete * update routing * clean up * add test * add link from status menu * clean up * fixes per pr comments * setup dashboard with contextual components, and toggle * setup option to show tabs or not * handle conditional nav menu * pass in whole model object * rename to replication-page * clean up * clean up based on pr feedback * fix linting error * Sidebranch: setup replication dashboard with scss and initial card components (#8670) * setup replication dashboard with scss and initial card components * sync with Noelles changes and clean up the inner grid container inside the selectable card * set up nested contextual components for selectable-cards * setup component for table row * address walk through notes * remove name selectable from card component as it is not selectable * add missing space * Ui/dr primary/initial page setup (#8671) * add helperText param to InfoTableRow * initial page setup * format card with padding and correct number of rows * style card titles with margin * move styles inside replication class; add todos * move replication-summary styles into core app so hot reloading works * prevent known secondaries card from being cut off on the right hand side * make cards have the correct column span * make code elements inside tables black * WIP - start VltTable component * simplify css * renamed VltTable to ReplicationTable and use divs instead of table elements * fix position of known secondaries * use table element for secondaries card * add todo * move replication components to replication engine * Revert "move replication components to replication engine" This reverts commit 2228b8392199a1376815dc8b7642de438aad68b5. * move ReplicationPrimaryCards to components * remove hover box shadow since cards are not selectable yet * only apply padding to replication selectable-cards * specify replication vlt-table in classname * move replication toggle and toggle into core addon * remove extra toolbar border * remove duplicate css * move ReplicationTableRows to core addon and use them on DR primary page * clean up todos * add jsdoc comments * rename ReplicationTable to KnownSecondaries * update replicaiton table api to accept flexible data * rename replicationAttrs to data * move replication components to core addon * Ui/dr primary components (#8711) * populate table with actual secondary ids * add todo * make KnownSecondariesCard component * move KnownSecondariesCard styles to own file * add EmptyState when there are no known secondaries * fix known secondaries manage link * fix Add Secondary link; bring in capabilities model to repliation index route so we can check for adding secondaries * fix JSDOC comments and updata data to replicationAttrs * Sidebranch: DR Secondary Dashboard - pr4 (#8706) * setup styling for delta * clean up * replication table remove and rename model to data * remove old replication header component now that it is in addons * move replication secondary card component * calc delta * clean up * remove unused components that are now in addon * address pr comments * remove test * fix failing test * address pr comments * attempting to fix test * move to computed components * fix test error * fix state of null error * Sidebranch: DR Secondary Dashboard state message handlers (#8741) * setup styling for delta * clean up * replication table remove and rename model to data * remove old replication header component now that it is in addons * move replication secondary card component * calc delta * clean up * remove unused components that are now in addon * address pr comments * remove test * fix failing test * address pr comments * attempting to fix test * initial setup before merge updates * move to computed components * fix test error * fix state of null error * clean up * setup alert banner and documentation link * setup alert banner with second icon * remove underscore to dash * add in missing error messages * add connection-state shutdown * add storybook update to alert banner * pr comments * move css class to helper class * address pr comments * add in connection states correct endpoint * Ui/dr dashboard tests (#8732) * initial test setup * use margin when there are no knownsecondaries * set up replication tests in ember engine * set knownSecondaries * move tests to host app and set resolver * finish known-secondaries-card-test * make knownSecondaries array match the API response * add known secondaries table test * oops, remove stories directory * wip - replication table rows test * remove extra code * finish replication table rows tests * add | Enterprise | to test module * remove unncessary assertion: * show dynamic state glyph (#8747) * show dynamic state glyph * show state glyph after state * move LearnLink into core addon * make ReplicationDocLink component * prevent double lines at the bottom of the dashboard * Sidebranch: dr-secondary-dashboard-pr7 (#8792) * move dr to higher level component and setup isDisabled for error state when dr mode is disabled. * add in error messages final * refactor transistion on submit handler focused on dr secondary * handle transition * update empty state component to include icon and add empty state to details page * fix spelling * address pr comments * merge with replication branch * remove component structure for replication-secondary-card * compute title and error message * make specific empty state messages * fix test * address pr comments * regenerate the storyboook for empty state * Replication Primary Dashboard: handle errors (#8845) * use h3 instead of code elements * use correct property names for StateDisplay * WIP * remove todo * move cluster states into a map; make status menu icon match cluster state * show error in state card using the same state map in the cluster model * whitespace * move cluster-states into a helper and update usage * use circle success icon for stream-wals because that is the ideal state * more refactoring of cluster state display * use new cluster-states helper * whitespace * use clusterStates helper in replication secondary card * remove extra import * add default values for when state isn't recognized * make sure that state exists before getting state details from clusterStates helper * be more strict when state cannot be found * use brace expansion to fix linting error * add tests for error states * fix text wrapping issue on secondary cards; make titles match mocks * use unknown if metric isn't foudn * remove extra border on selectable card when there is an error * use outline square in status menu for error * Ui/replication/refactor dashboard components (#8878) * use ReplicationDashboard component * move syncing and alert banners into dashboard component * only show primary cluster addr if dashboard is for a secondary * use ReplicationPage and Dashboard * move isSecondary to page component * remove duplication * remove dead code * refactored table rows * make sure dashboards update data when we are switching between mclusterModes and replicationTypes * clarified replicationMode and clusterMode * remove extra margin * get rid of data * remove syncProgress * remove Enterprise filter from tests so component tests are run * Ui/replication/primary reindexing (#8906) * fix typo * fetch replication/mode/status and pass to dashboard component * add reindexing stage to AlertBanner; use real value for isReindexing * remove dr since we don't need it anymore * add indentation * remove TODO * capitalize reindexing_stage and make progress 0 by default * remove Toggle since we don't need it anymore * get allllll the variables at once * only run secondary details test on enterprise * Sidebranch: component and acceptance tests (#8903) * address secondary card overflow issue * setup replicaiton header test * address secondary card overflow issue * setup replication secondary card test * setup replicaiton header test * setup replicaiton page test * setup replication secondary card test * setup replication dashboard test * setup replicaiton page test * remove unused code * fix overflow * finish test for rep dashboard * update rep secondary card test * finish rep header test * fix rep table rows and header test * fix header test * fix missing data-test-primary-cluster * add to secondary test * remove pauseTest * add to enterprise replication test * add mode to dr secondary test * remove pauseTest * add enterprise to test * amend per pr commments * re organize rep secondary card test * adjust error heights with design input * move const around in rep secondary card test * move const around and message for rep dashboard test * amend per pr review comments * remove styling from grid-item-left * remove dup hasErrorClass key * quick fix * test failure fix * fix test due to merge * remove hasErrorClass * modify test message * Sidebranch: remove delta, toggle, and make auto-refresh (#8945) * change styling * remove replication toggle * modifications for auto refresh and final removal of delta and last wal * fix refresh issue by removing replicationMode on this.reset which conflicts with the same property being set on the cluster model * remove comments * add unknown placeholder * add auto refresh to other components and remove mention of toggle * remove meep and primary cluster heading area * ensure status menu displays replication state, not just one (#8959) * Add Replication Reindexing Progress Bar (#8975) * whitespace * rename consts * rename variables * test that dashboard shows a reindexing alert banner * standardize shamir and ui wizard progress bar * make new progressbar component * just kidding, we can use the html5 progress bar * make top margins consistent across primary and secondary dashboards * clean up AlertBanner JSDocs and markdown * show a progress bar inside an AlertBanner if cluster is reindexing * add example AlertBanner with Progress Bar * add reindexing tests * add a tiny left margin to progress bars inside alert banners * keep old class names in wizard to prevent bug, but keep consistent progress background color * use spacing variables * remove extra border when secondary card has an error * make card header sizes and weight consistent * Sidebranch: Performance Secondary Dashboard (#8956) * setup rep dashboard to dynamically take in the component to render and dynamically setup the css based on mode of cluster * conditional pass in the correct props to the Dashboard.card component and add margin to reindexing alertBanner * update replication dashboard test * add performance secondary test and clean up replication-secondary-card test * fix message * replace cluster-id with secondaryId * remove reindexing test as its a duplicate of the branch noelle is working on * cleanup * address pr comments * small test fixes * add secondaryId to header test * fix tests description * Ui/replication/test update (#8995) * make sure progress bar updates and animates * ensure dashboard updates when replication mode has changed * make sure we update isSyncing when state has changed * wip - console log statements to see if components are getting new attrs * Revert "wip - console log statements to see if components are getting new attrs" This reverts commit d05219ba6c14c64a9f2e867892476faf7dad4659. * style progress bar in mozilla; allow testing the progress bar in storybook * test that primary and secondary card container don't display at the same time * prepare KnownSecondariesTable for backend compatibility (#9029) * Ui/replication mgmt action block (#9053) This does some low-impact work to prepare for the refactor of replication-actions. Includes: - Move modal to addon in lib/core - Update modal to take a "type" param which changes the header color + icon - Add tests for modal changes - Add action-block style only component - Add styles-only replication-action grid that the action-blocks will live inside of * Sidebranch: address transition issues on replication engine and actions (#9010) * small formatting changes * change findRecord to peekRecord so it keeps track of the changing data. * add styling such that when page is loading it does not spread across the whole page * help with reload and styling on replication route * initial setup for new flow that handles adding a perf secondary, and also some on a dr secondary * clean up * add loader on rep page for situations when data is still loading, and add loading mode in header, seperate from the modeForUrl used in other places to help transistion * fix transitionTo when coming from different replication.mode vs replication.index route * set default of mode for radio checkboxes after removing from DEFAULTS var * reset and cont using onEnable because TransitionTo is not working inside of component * remove console * the reason we were getting transition errors :( * remove modeObjecT * fix error by removing peek record from application and moving it lower down in a property replicationAttrs * Readd back space * this one really does fix the issue * add back peek record and add conditional to isLoadingData * figure out cluster id from service instead of hardcoded * fix capabilities-self error by adding a 1 sceond delay for when transition from replication.index to replication.mode.index on enable performance secondary * remove attempt to circumvent the peekRecord in application * add to replication page tests and clarify replicationMode to formattedReplicationMode, it's super confusing when seeing replicationMode being duplicated throughout the computed components. this clarifies its computed only for formatting * fix repetive conditional * capture the state when either dr.mode or performance.mode are undefined, which happens during a transition. If this is the case add a loader on the replicationindex page. * address some pr comments * small change * add bootstrapping mode to test * add Replication Learn Links to wizard (#9106) * Ui/summary dashboard (#9079) * move key value to lib/core/addon so I can use inside replication engine * setup summary dasbhoard on replication summary component * set title for summary dashboard * do not show replication table rows on summary dashboard * show that last_wal updates every 10 seconds * show replication table rows on individual dashboards, but not summary * remove extra bottom border on replication-dashboard * add replicationDetailsSummary object and replication-summary-card * setup structure and data calcs of replication summary card * fix links and styling on summary card * breadcrumbs * match state title on summary dashboard to individual dashboards * add margin below replication header * update breadcrumbs to show replication mode * align details link right * add margin below tabs in replication header * user helper-text to make card text styling consistent across dashboards * remove unneeded code * add bottom border to summary state * add bottom margin to summary dashboard * add negative margins to bring values closer to related cell * fix failing test due to data-test attribute change and make storybook component for replication-summary-card * setup replication summary card test. I suspect we'll move the hasError test to the dashboard where the error will show around the state display * add to replication acceptance test for new summary dashboard * remove pauseTest * add is-active to li element * clean up * dashboard test and clean up * addressing pr comments * fix replication/null/status error * add JSDocs for rep page and rep dash * more pr cleanup * remove conditional and fix styling blue link * fix conditional on when loading summary dashboard to check for primary on both. wrap code in div so it lands on another line. Co-authored-by: Noelle Daley <adriannenoelle@gmail.com> * change message with bold 'not' if primary (#9112) * Add JSDocs to components (#9125) * jsdocs * remove todo that is no longer relevant * clean up wording * wordsmithing * fix spelling * example for clusterMode * Replication Management Sidebranch: Replication Action Disable (#9061) Set up dr-secondary management page with new action flow * Create confirmation-modal component * Refactor replication-dr-secondary splash page to replication manage page * Refactor replication-action-disable component to use confirmation modal * Add details/manage tab to replication-dr-secondary section * Refactor Replication Action: Promote to use modal flow (#9122) * Ui/replication mgmt/reindex action (#9126) * Replication Management Sidebranch: Replication Action Disable (#9061) * Ui/replication mgmt/recover action (#9127) * Replication Management Sidebranch: Replication Action Recover (#9061) * Close link-to tag in header (#9139) Fixes bad merge conflict * UI: Fix replication management tests (#9136) * do not show replication mode or id when replication isn't enabled * fix broken tag * fill in confirmation text when disabling replication in tests * fix typo * fix demote primary test selector * add test selectors and update tests to match new format * fill in Performance when disabling performance secondary * Ui/replication mgmt/update primary action (#9149) * Update Primary replication action uses modal flow * Update modal max-height to accommodate for the navbar * Ui/secondary token flow dr (#9150) * setup token modal flow * calc expirationDate * fix date-format test after moving it in addon * fix icon conditional in modal title * decode token to get epoch expiration date and convert * handle clicking outside of modal * remove extra copy button * add modal check in rep acceptance test * look only at day and month and remove console * fix spelling * cleanup * replace dr with variable * make string check longer in test * fix test variables * refactor enterprise test for secondary token flow * make cluster model property replicationModeForDisplay to handle all cases where we were either conditionally displaying the DR, Disaster Recovery, etc. or where we were hardcoding it into the hbs. For situations where it was DR before, I am now keeping it more consistent and using Disaster Recovery as on the manage page we do not show the Diaster Recovery (DR) anywhere. * set initial value for ttl picker to fix issue where itwas setting seconds to minutes * clean up * add comment about ttl picker * Add known primaries info table (#9152) * replace primaryClusterAddr with knownPrimaryClusterAddrs * rename state to Status; fix css layout * add InfoTable component * only show label column if there is a label * add grid-item-middle class * whitespace * fix grid layout * die tagName, die * set table max-height * prep InfoTable for Storybook * ensure cards always have the same height * remove duplicate max height since vlt-table already has max-height * add InfoTable tests * add InfoTable to Storybook * organize grid item css; rename for consistency * add sticky header to table * add sticky-header class to keep table styles in scope * whoops, do not use fake data * Ui/rep design updates (#9169) * show secondaryId in table rows * show primary_cluster_addr in table rows * remove cluster Ids from replication headers * Ui/fix enable overflow (#9173) * only show primary_cluster_addr for primary * fix overflow on replication index * remove display from cluster-states because it is not used anywhere * fix missing replication mode from description * add comments * use helper to consolidate replication descriptions * fix text wrapping on medium screen sizes * Ui/replication mgmt/demote action (#9168) * Replication demote action uses modal flow Co-authored-by: Noelle Daley <adriannenoelle@gmail.com> Co-authored-by: Angel Garbarino <argarbarino@gmail.com> * Ui/replication merge cleanup 2 (#9212) * replace with replicationModeForDisplay that is defined on the cluster * fix spelling on replication and confirmed with design for placeholder when Not defined * remove extra div with box class * change manage link to take you to the secondaries manage as it's within the known secondaries card * fix scroll always showing by adding auto, and decreasing the height. WIP * add empty state to known_primary_cluster_addrs * address pr comments * Add real connected state and API address (#9219) * fix title of secondary card * show connected status * fix tests * fix enterprise test (#9229) * fix enterprise test * add n * add another n * Ui/replication mgmt/generate token action (#9187) Generate operation token flow from replication DR Secondary. Clicking 'Cancel' on the modal after the operation has started results in cancelling generate operation and restarting the process. * use none set instead of not defined Co-authored-by: Noelle Daley <noelledaley@users.noreply.github.com> Co-authored-by: Chelsea Shaw <chelshaw.dev@gmail.com> Co-authored-by: Noelle Daley <adriannenoelle@gmail.com>
This commit is contained in:
@@ -17,6 +17,7 @@
|
||||
- [Writing Stories](#writing-stories)
|
||||
- [Adding a new story](#adding-a-new-story)
|
||||
- [Code Generators](#code-generators-1)
|
||||
- [Storybook Deployment](#storybook-deployment)
|
||||
- [Further Reading / Useful Links](#further-reading--useful-links)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
@@ -67,6 +68,11 @@ long-form version of the npm script:
|
||||
|
||||
Make use of the many generators for code, try `ember help generate` for more details. If you're using a component that can be widely-used, consider making it an `addon` component instead (see [this PR](https://github.com/hashicorp/vault/pull/6629) for more details)
|
||||
|
||||
eg. a reusable component named foo that you'd like in the core engine
|
||||
|
||||
- `ember g component foo --in lib/core`
|
||||
- `echo "export { default } from 'core/components/foo';" > lib/core/app/components/foo.js`
|
||||
|
||||
### Running Tests
|
||||
|
||||
Running tests will spin up a Vault dev server on port 9200 via a
|
||||
@@ -158,7 +164,7 @@ Note that placing a param inside brackets (e.g. `[closedLabel=More options]` ind
|
||||
|
||||
2. Generate a new story with `ember generate story [name-of-component]`
|
||||
3. Inside the newly generated `stories` file, add at least one example of the component. If the component should be interactive, enable the [Storybook Knobs addon](https://github.com/storybooks/storybook/tree/master/addons/knobs).
|
||||
4. Generate the `notes` file for the component with `yarn gen-story-md [name-of-component]` (e.g. `yarn gen-md alert-banner`). This will generate markdown documentation of the component and place it at `vault/ui/stories/[name-of-component].md`. If your component is a template-only component, you will need to manually create the markdown file.
|
||||
4. Generate the `notes` file for the component with `yarn gen-story-md [name-of-component] [name-of-engine-or-addon]` (e.g. `yarn gen-md alert-banner core`). This will generate markdown documentation of the component and place it at `vault/ui/stories/[name-of-component].md`. If your component is a template-only component, you will need to manually create the markdown file.
|
||||
|
||||
See the [Storybook Docs](https://storybook.js.org/docs/basics/introduction/) for more information on writing stories.
|
||||
|
||||
|
||||
@@ -183,7 +183,10 @@ export default ApplicationAdapter.extend({
|
||||
},
|
||||
|
||||
generateDrOperationToken(data, options) {
|
||||
const verb = options && options.checkStatus ? 'GET' : 'PUT';
|
||||
let verb = options && options.checkStatus ? 'GET' : 'PUT';
|
||||
if (options.cancel) {
|
||||
verb = 'DELETE';
|
||||
}
|
||||
let url = `${this.buildURL()}/replication/dr/secondary/generate-operation-token/`;
|
||||
if (!data || data.pgp_key || data.attempt) {
|
||||
// start the generation
|
||||
|
||||
14
ui/app/adapters/replication-mode.js
Normal file
14
ui/app/adapters/replication-mode.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import ApplicationAdapter from './application';
|
||||
|
||||
export default ApplicationAdapter.extend({
|
||||
getStatusUrl(mode) {
|
||||
return this.buildURL() + `/replication/${mode}/status`;
|
||||
},
|
||||
|
||||
fetchStatus(mode) {
|
||||
let url = this.getStatusUrl(mode);
|
||||
return this.ajax(url, 'GET', { unauthenticated: true }).then(resp => {
|
||||
return resp.data;
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -31,6 +31,9 @@ App = Application.extend({
|
||||
'version',
|
||||
'wizard',
|
||||
],
|
||||
externalRoutes: {
|
||||
replication: 'vault.cluster.replication.index',
|
||||
},
|
||||
},
|
||||
},
|
||||
kmip: {
|
||||
|
||||
39
ui/app/components/shamir-modal-flow.js
Normal file
39
ui/app/components/shamir-modal-flow.js
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* @module ShamirModalFlow
|
||||
* ShamirModalFlow is an extension of the ShamirFlow component that does the Generate Action Token workflow inside of a Modal.
|
||||
* Please note, this is not an extensive list of the required parameters -- please see ShamirFlow for others
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* <ShamirModalFlow @onClose={action 'onClose'}>This copy is the main paragraph when the token flow has not started</ShamirModalFlow>
|
||||
* ```
|
||||
* @param {function} onClose - This function will be triggered when the modal intends to be closed
|
||||
*/
|
||||
import { inject as service } from '@ember/service';
|
||||
import ShamirFlow from './shamir-flow';
|
||||
import layout from '../templates/components/shamir-modal-flow';
|
||||
|
||||
export default ShamirFlow.extend({
|
||||
layout,
|
||||
store: service(),
|
||||
onClose: () => {},
|
||||
actions: {
|
||||
onCancelClose() {
|
||||
if (this.encoded_token) {
|
||||
this.send('reset');
|
||||
} else if (this.generateAction && !this.started) {
|
||||
if (this.generateStep !== 'chooseMethod') {
|
||||
this.send('reset');
|
||||
}
|
||||
} else {
|
||||
const adapter = this.get('store').adapterFor('cluster');
|
||||
adapter.generateDrOperationToken(this.model, { cancel: true });
|
||||
this.send('reset');
|
||||
}
|
||||
this.onClose();
|
||||
},
|
||||
onClose() {
|
||||
this.onClose();
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -3,4 +3,9 @@ import Controller from '@ember/controller';
|
||||
export default Controller.extend({
|
||||
queryParams: ['action'],
|
||||
action: '',
|
||||
actions: {
|
||||
onPromote() {
|
||||
this.transitionToRoute('vault.cluster.replication.mode.index', 'dr');
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -9,6 +9,7 @@ const CLUSTER = 'vault.cluster';
|
||||
const CLUSTER_INDEX = 'vault.cluster.index';
|
||||
const OIDC_CALLBACK = 'vault.cluster.oidc-callback';
|
||||
const DR_REPLICATION_SECONDARY = 'vault.cluster.replication-dr-promote';
|
||||
const DR_REPLICATION_SECONDARY_DETAILS = 'vault.cluster.replication-dr-promote.details';
|
||||
const EXCLUDED_REDIRECT_URLS = ['/vault/logout'];
|
||||
|
||||
export { INIT, UNSEAL, AUTH, CLUSTER, CLUSTER_INDEX, DR_REPLICATION_SECONDARY };
|
||||
@@ -70,6 +71,13 @@ export default Mixin.create({
|
||||
return UNSEAL;
|
||||
}
|
||||
if (get(cluster, 'dr.isSecondary')) {
|
||||
if (transition && transition.targetName === DR_REPLICATION_SECONDARY_DETAILS) {
|
||||
return DR_REPLICATION_SECONDARY_DETAILS;
|
||||
}
|
||||
if (this.router.currentRouteName === DR_REPLICATION_SECONDARY_DETAILS) {
|
||||
return DR_REPLICATION_SECONDARY_DETAILS;
|
||||
}
|
||||
|
||||
return DR_REPLICATION_SECONDARY;
|
||||
}
|
||||
if (!isAuthed) {
|
||||
|
||||
@@ -46,57 +46,22 @@ export default DS.Model.extend({
|
||||
//otherwise the particular mode will have the relevant mode attr through replication-attributes
|
||||
mode: attr('string'),
|
||||
allReplicationDisabled: and('{dr,performance}.replicationDisabled'),
|
||||
|
||||
anyReplicationEnabled: or('{dr,performance}.replicationEnabled'),
|
||||
|
||||
stateDisplay(state) {
|
||||
if (!state) {
|
||||
return null;
|
||||
}
|
||||
const defaultDisp = 'Synced';
|
||||
const displays = {
|
||||
'stream-wals': 'Streaming',
|
||||
'merkle-diff': 'Determining sync status',
|
||||
'merkle-sync': 'Syncing',
|
||||
};
|
||||
|
||||
return displays[state] || defaultDisp;
|
||||
},
|
||||
|
||||
drStateDisplay: computed('dr.state', function() {
|
||||
return this.stateDisplay(this.get('dr.state'));
|
||||
}),
|
||||
|
||||
performanceStateDisplay: computed('performance.state', function() {
|
||||
return this.stateDisplay(this.get('performance.state'));
|
||||
}),
|
||||
|
||||
stateGlyph(state) {
|
||||
const glyph = 'check-circle-outline';
|
||||
|
||||
const glyphs = {
|
||||
'stream-wals': 'android-sync',
|
||||
'merkle-diff': 'android-sync',
|
||||
'merkle-sync': null,
|
||||
};
|
||||
|
||||
return glyphs[state] || glyph;
|
||||
},
|
||||
|
||||
drStateGlyph: computed('dr.state', function() {
|
||||
return this.stateGlyph(this.get('dr.state'));
|
||||
}),
|
||||
|
||||
performanceStateGlyph: computed('performance.state', function() {
|
||||
return this.stateGlyph(this.get('performance.state'));
|
||||
}),
|
||||
|
||||
dr: fragment('replication-attributes'),
|
||||
performance: fragment('replication-attributes'),
|
||||
// this service exposes what mode the UI is currently viewing
|
||||
// replicationAttrs will then return the relevant `replication-attributes` fragment
|
||||
rm: service('replication-mode'),
|
||||
replicationMode: alias('rm.mode'),
|
||||
replicationModeForDisplay: computed('replicationMode', function() {
|
||||
return this.replicationMode === 'dr' ? 'Disaster Recovery' : 'Performance';
|
||||
}),
|
||||
replicationIsInitializing: computed('dr.mode', 'performance.mode', function() {
|
||||
// a mode of null only happens when a cluster is being initialized
|
||||
// otherwise the mode will be 'disabled', 'primary', 'secondary'
|
||||
return !this.dr.mode || !this.performance.mode;
|
||||
}),
|
||||
replicationAttrs: computed('dr.mode', 'performance.mode', 'replicationMode', function() {
|
||||
const replicationMode = this.get('replicationMode');
|
||||
return replicationMode ? get(this, replicationMode) : null;
|
||||
|
||||
@@ -18,16 +18,25 @@ export default Fragment.extend({
|
||||
isPrimary: match('mode', /primary/),
|
||||
|
||||
knownSecondaries: attr('array'),
|
||||
secondaries: attr('array'),
|
||||
|
||||
// secondary attrs
|
||||
isSecondary: match('mode', /secondary/),
|
||||
|
||||
connection_state: attr('string'),
|
||||
modeForUrl: computed('mode', function() {
|
||||
const mode = this.get('mode');
|
||||
return mode === 'bootstrapping'
|
||||
? 'bootstrapping'
|
||||
: (this.get('isSecondary') && 'secondary') || (this.get('isPrimary') && 'primary');
|
||||
}),
|
||||
modeForHeader: computed('mode', function() {
|
||||
const mode = this.mode;
|
||||
if (!mode) {
|
||||
// mode will be false or undefined if it calls the status endpoint while still setting up the cluster
|
||||
return 'loading';
|
||||
}
|
||||
return mode;
|
||||
}),
|
||||
secondaryId: attr('string'),
|
||||
primaryClusterAddr: attr('string'),
|
||||
knownPrimaryClusterAddrs: attr('array'),
|
||||
|
||||
38
ui/app/models/replication-mode.js
Normal file
38
ui/app/models/replication-mode.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import DS from 'ember-data';
|
||||
const { attr } = DS;
|
||||
|
||||
/* sample response
|
||||
|
||||
{
|
||||
"request_id": "d81bba81-e8a1-0ee9-240e-a77d36e3e08f",
|
||||
"lease_id": "",
|
||||
"renewable": false,
|
||||
"lease_duration": 0,
|
||||
"data": {
|
||||
"cluster_id": "ab7d4191-d1a3-b4d6-6297-5a41af6154ae",
|
||||
"known_secondaries": [
|
||||
"test"
|
||||
],
|
||||
"last_performance_wal": 72,
|
||||
"last_reindex_epoch": "1588281113",
|
||||
"last_wal": 73,
|
||||
"merkle_root": "c8d258d376f01d98156f74e8d8f82ea2aca8dc4a",
|
||||
"mode": "primary",
|
||||
"primary_cluster_addr": "",
|
||||
"reindex_building_progress": 26838,
|
||||
"reindex_building_total": 305443,
|
||||
"reindex_in_progress": true,
|
||||
"reindex_stage": "building",
|
||||
"state": "running"
|
||||
},
|
||||
"wrap_info": null,
|
||||
"warnings": null,
|
||||
"auth": null
|
||||
}
|
||||
|
||||
|
||||
*/
|
||||
|
||||
export default DS.Model.extend({
|
||||
status: attr('object'),
|
||||
});
|
||||
@@ -127,7 +127,9 @@ Router.map(function() {
|
||||
this.route('show', { path: '/:policy_name' });
|
||||
this.route('edit', { path: '/:policy_name/edit' });
|
||||
});
|
||||
this.route('replication-dr-promote');
|
||||
this.route('replication-dr-promote', function() {
|
||||
this.route('details');
|
||||
});
|
||||
if (config.addRootMounts) {
|
||||
config.addRootMounts.call(this);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { inject as service } from '@ember/service';
|
||||
import Base from './cluster-route-base';
|
||||
import Base from '../cluster-route-base';
|
||||
|
||||
export default Base.extend({
|
||||
replicationMode: service(),
|
||||
10
ui/app/routes/vault/cluster/replication-dr-promote/index.js
Normal file
10
ui/app/routes/vault/cluster/replication-dr-promote/index.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import { inject as service } from '@ember/service';
|
||||
import Base from '../cluster-route-base';
|
||||
|
||||
export default Base.extend({
|
||||
replicationMode: service(),
|
||||
beforeModel() {
|
||||
this._super(...arguments);
|
||||
this.get('replicationMode').setMode('dr');
|
||||
},
|
||||
});
|
||||
12
ui/app/serializers/replication-mode.js
Normal file
12
ui/app/serializers/replication-mode.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import ApplicationSerializer from './application';
|
||||
|
||||
export default ApplicationSerializer.extend({
|
||||
normalizeResponse(store, primaryModelClass, payload, id, requestType) {
|
||||
const normalizedPayload = {
|
||||
id: payload.id,
|
||||
status: payload.data,
|
||||
};
|
||||
|
||||
return this._super(store, primaryModelClass, normalizedPayload, id, requestType);
|
||||
},
|
||||
});
|
||||
67
ui/app/styles/components/action-block.scss
Normal file
67
ui/app/styles/components/action-block.scss
Normal file
@@ -0,0 +1,67 @@
|
||||
@mixin stacked-grid {
|
||||
grid-template-columns: 1fr;
|
||||
grid-row: 1/1;
|
||||
}
|
||||
@mixin stacked-content {
|
||||
margin-bottom: $spacing-l;
|
||||
}
|
||||
|
||||
.action-block {
|
||||
@extend .selectable-card;
|
||||
grid-template-columns: 2fr 1fr;
|
||||
display: grid;
|
||||
padding: $spacing-m $spacing-l;
|
||||
line-height: inherit;
|
||||
grid-gap: $spacing-m;
|
||||
|
||||
@include until($mobile) {
|
||||
@include stacked-grid();
|
||||
}
|
||||
}
|
||||
|
||||
.action-block-info {
|
||||
@include until($mobile) {
|
||||
@include stacked-content();
|
||||
}
|
||||
}
|
||||
|
||||
.action-block.stacked {
|
||||
@include stacked-grid();
|
||||
}
|
||||
.stacked > .action-block-info {
|
||||
@include stacked-content();
|
||||
}
|
||||
|
||||
.action-block-title {
|
||||
font-size: $size-5;
|
||||
font-weight: $font-weight-bold;
|
||||
}
|
||||
.action-block-action {
|
||||
text-align: right;
|
||||
@include until($mobile) {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
/* Action Block Grid */
|
||||
.replication-actions-grid-layout {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: $spacing-m 0;
|
||||
@include until($tablet) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.replication-actions-grid-item {
|
||||
flex-basis: 50%;
|
||||
padding: $spacing-s;
|
||||
}
|
||||
|
||||
.replication-actions-grid-item .action-block {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
@include until($tablet) {
|
||||
height: inherit;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
.empty-state {
|
||||
align-items: center;
|
||||
color: $grey;
|
||||
display: flex;
|
||||
background: $ui-gray-010;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: $spacing-xxl $spacing-s;
|
||||
box-shadow: 0 -2px 0 -1px $ui-gray-300;
|
||||
@@ -36,3 +36,8 @@
|
||||
margin-left: $spacing-s;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state-icon > .hs-icon {
|
||||
float: left;
|
||||
margin-right: $spacing-xs;
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
}
|
||||
|
||||
.column {
|
||||
align-self: center;
|
||||
|
||||
&.info-table-row-edit {
|
||||
padding-bottom: 0.3rem;
|
||||
padding-top: 0.3rem;
|
||||
@@ -25,6 +27,10 @@
|
||||
textarea {
|
||||
min-height: 35px;
|
||||
}
|
||||
|
||||
.helper-text {
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.hs-icon {
|
||||
|
||||
9
ui/app/styles/components/info-table.scss
Normal file
9
ui/app/styles/components/info-table.scss
Normal file
@@ -0,0 +1,9 @@
|
||||
.info-table {
|
||||
&.vlt-table td {
|
||||
padding-top: 0px;
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
.info-table-row {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
18
ui/app/styles/components/known-secondaries-card.scss
Normal file
18
ui/app/styles/components/known-secondaries-card.scss
Normal file
@@ -0,0 +1,18 @@
|
||||
.selectable-card.secondaries {
|
||||
grid-column: 2/3;
|
||||
grid-row: 1/3;
|
||||
|
||||
@include until($mobile) {
|
||||
grid-column: 1/1;
|
||||
grid-row: 1/1;
|
||||
}
|
||||
|
||||
.secondaries-table {
|
||||
margin-bottom: $spacing-s;
|
||||
}
|
||||
|
||||
.link {
|
||||
font-size: $size-7;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,8 @@
|
||||
.modal-card {
|
||||
box-shadow: $box-shadow-highest;
|
||||
border: 1px solid $grey-light;
|
||||
max-height: calc(100vh - 70px);
|
||||
margin-top: 60px;
|
||||
|
||||
&-head {
|
||||
border-radius: 0;
|
||||
@@ -39,6 +41,38 @@
|
||||
}
|
||||
}
|
||||
|
||||
.modal-card-title.title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
pre {
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
.is-highlight {
|
||||
.modal-card-head {
|
||||
background: $yellow-010;
|
||||
border: 1px solid $yellow-100;
|
||||
}
|
||||
.modal-card-title {
|
||||
color: $yellow-dark;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-confirm-section .is-help {
|
||||
color: $grey;
|
||||
margin: $spacing-xxs 0;
|
||||
strong {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-confirm-section {
|
||||
margin: $spacing-xl 0 $spacing-m;
|
||||
}
|
||||
|
||||
.modal-card-foot-outlined {
|
||||
background: #f7f8fa;
|
||||
border-top: 1px solid #bac1cc;
|
||||
}
|
||||
|
||||
131
ui/app/styles/components/replication-dashboard.scss
Normal file
131
ui/app/styles/components/replication-dashboard.scss
Normal file
@@ -0,0 +1,131 @@
|
||||
.replication-dashboard {
|
||||
box-shadow: none;
|
||||
|
||||
.selectable-card {
|
||||
line-height: normal;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0 0 1px rgba($grey-dark, 0.3);
|
||||
}
|
||||
|
||||
.toolbar-link {
|
||||
color: $blue-500;
|
||||
}
|
||||
}
|
||||
|
||||
.helper-text {
|
||||
font-weight: $font-weight-normal;
|
||||
}
|
||||
|
||||
.title.is-6 {
|
||||
margin-bottom: $spacing-xs;
|
||||
}
|
||||
|
||||
.reindexing-alert,
|
||||
.syncing-alert {
|
||||
margin-top: $spacing-xl;
|
||||
}
|
||||
|
||||
.selectable-card-container {
|
||||
margin-top: $spacing-xl;
|
||||
display: grid;
|
||||
|
||||
&.primary,
|
||||
.summary {
|
||||
margin: 2rem 0 2rem 0;
|
||||
grid-template-columns: 1fr 2fr;
|
||||
|
||||
@include until($mobile) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
&.secondary {
|
||||
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
||||
grid-gap: $spacing-xl;
|
||||
}
|
||||
|
||||
.card-container {
|
||||
display: grid;
|
||||
grid-gap: $spacing-s;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-rows: 0.2fr 0.2fr 0.2fr;
|
||||
padding: $spacing-l;
|
||||
line-height: 1.5;
|
||||
|
||||
&.summary {
|
||||
grid-template-rows: 0.2fr 1fr 0.2fr 1fr;
|
||||
}
|
||||
|
||||
&.has-border-danger:hover {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
@include until(1320px) {
|
||||
// prevent an issue with the card descriptions wrapping and expanding height
|
||||
min-height: 250px;
|
||||
}
|
||||
|
||||
.grid-item-top-left {
|
||||
grid-column: 1 / span 1;
|
||||
display: flex;
|
||||
}
|
||||
.grid-item-top-right {
|
||||
grid-column: 2 / span 1;
|
||||
justify-self: right;
|
||||
}
|
||||
.grid-item-left {
|
||||
grid-column: 1/1;
|
||||
grid-row: 2/2;
|
||||
}
|
||||
.grid-item-right {
|
||||
grid-column: 2/2;
|
||||
grid-row: 2/2;
|
||||
}
|
||||
|
||||
.grid-item-bottom-left {
|
||||
grid-column: 1/1;
|
||||
grid-row: 3/3;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.grid-item-bottom-right {
|
||||
grid-column: 2/2;
|
||||
grid-row: 3/3;
|
||||
}
|
||||
|
||||
.grid-item-second-row {
|
||||
grid-column: 1 / span 2;
|
||||
grid-row: 2/2;
|
||||
}
|
||||
|
||||
.grid-item-third-row {
|
||||
grid-column: 1 / span 2;
|
||||
grid-row: 3/4;
|
||||
|
||||
.empty-state {
|
||||
padding: 0px 12px;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
.grid-item-bottom-row {
|
||||
grid-column: 1 / span 2;
|
||||
grid-row: 4/4;
|
||||
}
|
||||
}
|
||||
|
||||
&.summary {
|
||||
margin-bottom: $spacing-xl;
|
||||
}
|
||||
}
|
||||
.summary-state {
|
||||
padding-bottom: $spacing-xl;
|
||||
border-bottom: 1px solid rgba($grey-dark, 0.3);
|
||||
}
|
||||
|
||||
// prevent double lines at the bottom of the dashboard
|
||||
&.box {
|
||||
padding-bottom: 0;
|
||||
padding-top: 1px; // at least 1px so border still shows
|
||||
}
|
||||
}
|
||||
8
ui/app/styles/components/replication-doc-link.scss
Normal file
8
ui/app/styles/components/replication-doc-link.scss
Normal file
@@ -0,0 +1,8 @@
|
||||
.documentation-link {
|
||||
margin: $spacing-s 0 $spacing-l 0;
|
||||
float: right;
|
||||
|
||||
.doc-link {
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
5
ui/app/styles/components/replication-header.scss
Normal file
5
ui/app/styles/components/replication-header.scss
Normal file
@@ -0,0 +1,5 @@
|
||||
.replication-header {
|
||||
.tabs-container {
|
||||
margin-bottom: $spacing-l;
|
||||
}
|
||||
}
|
||||
11
ui/app/styles/components/replication-mode-summary.scss
Normal file
11
ui/app/styles/components/replication-mode-summary.scss
Normal file
@@ -0,0 +1,11 @@
|
||||
.replication-description {
|
||||
flex-shrink: 1;
|
||||
|
||||
.title {
|
||||
margin-bottom: $spacing-xs;
|
||||
}
|
||||
|
||||
.detail-tags {
|
||||
margin-bottom: $spacing-m;
|
||||
}
|
||||
}
|
||||
10
ui/app/styles/components/replication-page.scss
Normal file
10
ui/app/styles/components/replication-page.scss
Normal file
@@ -0,0 +1,10 @@
|
||||
.replication-page {
|
||||
.empty-state {
|
||||
background: none;
|
||||
|
||||
.empty-state-message {
|
||||
padding-bottom: $spacing-s;
|
||||
border-bottom: 1px solid $grey-light;
|
||||
}
|
||||
}
|
||||
}
|
||||
15
ui/app/styles/components/replication-primary-card.scss
Normal file
15
ui/app/styles/components/replication-primary-card.scss
Normal file
@@ -0,0 +1,15 @@
|
||||
.replication {
|
||||
.selectable-card {
|
||||
display: initial;
|
||||
line-height: normal;
|
||||
padding: $spacing-l;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0 0 1px rgba($grey-dark, 0.3);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
9
ui/app/styles/components/replication-summary.scss
Normal file
9
ui/app/styles/components/replication-summary.scss
Normal file
@@ -0,0 +1,9 @@
|
||||
.replication {
|
||||
.toolbar {
|
||||
border-top: 0px;
|
||||
}
|
||||
|
||||
.helper-text {
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
@@ -67,12 +67,21 @@
|
||||
font-weight: 500;
|
||||
line-height: 1.33;
|
||||
}
|
||||
|
||||
.vlt-table {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.selectable-card.is-rounded {
|
||||
border-radius: $radius;
|
||||
}
|
||||
|
||||
.selectable-card.has-border-danger {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.change-metric-icon.is-decrease {
|
||||
transform: rotate(135deg);
|
||||
}
|
||||
|
||||
@@ -191,7 +191,7 @@
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
background: $ui-gray-100;
|
||||
background: $progress-bar-background-color;
|
||||
box-shadow: inset 0 0 0 1px $ui-gray-200;
|
||||
display: flex;
|
||||
height: $wizard-progress-bar-height;
|
||||
|
||||
@@ -4,6 +4,15 @@
|
||||
height: 0;
|
||||
}
|
||||
|
||||
&.sticky-header {
|
||||
thead th {
|
||||
position: sticky;
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 0px 0px rgba($grey-dark, 0.3);
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: $spacing-s;
|
||||
@@ -34,4 +43,9 @@
|
||||
td.no-padding {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: $size-7;
|
||||
color: $black;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,9 +60,11 @@
|
||||
@import './components/http-requests-bar-chart';
|
||||
@import './components/http-requests-table';
|
||||
@import './components/init-illustration';
|
||||
@import './components/info-table';
|
||||
@import './components/info-table-row';
|
||||
@import './components/input-hint';
|
||||
@import './components/kmip-role-edit';
|
||||
@import './components/known-secondaries-card.scss';
|
||||
@import './components/linked-block';
|
||||
@import './components/list-item-row';
|
||||
@import './components/list-pagination';
|
||||
@@ -78,10 +80,19 @@
|
||||
@import './components/radio-card';
|
||||
@import './components/radial-progress';
|
||||
@import './components/raft-join';
|
||||
@import './components/replication-dashboard';
|
||||
@import './components/replication-doc-link';
|
||||
@import './components/replication-header';
|
||||
@import './components/replication-mode-summary';
|
||||
@import './components/replication-page';
|
||||
@import './components/replication-primary-card';
|
||||
@import './components/replication-summary';
|
||||
@import './components/role-item';
|
||||
@import './components/search-select';
|
||||
@import './components/selectable-card';
|
||||
@import './components/selectable-card-container.scss';
|
||||
// action-block extends selectable-card
|
||||
@import './components/action-block.scss';
|
||||
@import './components/shamir-progress';
|
||||
@import './components/sidebar';
|
||||
@import './components/splash-page';
|
||||
|
||||
@@ -39,6 +39,10 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.is-flex-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
.is-flex-v-centered {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -143,6 +147,30 @@
|
||||
font-size: $size-8;
|
||||
text-transform: lowercase;
|
||||
}
|
||||
.has-bottom-margin {
|
||||
.has-bottom-margin-xs {
|
||||
margin-bottom: $spacing-xs;
|
||||
}
|
||||
.has-bottom-margin-s {
|
||||
margin-bottom: $spacing-s;
|
||||
}
|
||||
.has-bottom-margin-m {
|
||||
margin-bottom: $spacing-m;
|
||||
}
|
||||
.has-bottom-margin-l {
|
||||
margin-bottom: $spacing-l;
|
||||
}
|
||||
.has-top-margin-xl {
|
||||
margin-top: $spacing-xl;
|
||||
}
|
||||
.has-border-danger {
|
||||
border: 1px solid $danger;
|
||||
}
|
||||
|
||||
ul.bullet {
|
||||
list-style: disc;
|
||||
padding-left: $spacing-m;
|
||||
}
|
||||
|
||||
.has-text-semibold {
|
||||
font-weight: $font-weight-semibold;
|
||||
}
|
||||
|
||||
@@ -25,6 +25,10 @@
|
||||
font-size: 16px;
|
||||
font-weight: $font-weight-bold;
|
||||
line-height: 1.25;
|
||||
|
||||
.progress {
|
||||
margin-left: $spacing-xs;
|
||||
}
|
||||
}
|
||||
|
||||
.close-button + .message-title {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
.progress[value]::-webkit-progress-bar,
|
||||
.progress[value]::-webkit-progress-value {
|
||||
border-radius: 2px;
|
||||
}
|
||||
.progress {
|
||||
border-radius: 0;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
background: $progress-bar-background-color;
|
||||
box-shadow: inset 0 0 0 1px $ui-gray-200;
|
||||
border-radius: $radius;
|
||||
margin-bottom: 0;
|
||||
&.is-small {
|
||||
height: 0.5rem;
|
||||
@@ -11,10 +11,24 @@
|
||||
&.is-narrow {
|
||||
width: 30px;
|
||||
}
|
||||
&.is-medium {
|
||||
width: 120px;
|
||||
}
|
||||
}
|
||||
.progress.is-rounded {
|
||||
border-radius: 2px;
|
||||
|
||||
// style the container in chrome
|
||||
.progress[value]::-webkit-progress-bar {
|
||||
box-shadow: inset 0 0 0 1px $ui-gray-200;
|
||||
}
|
||||
.progress.is-bordered {
|
||||
box-shadow: 0 0 0 4px $progress-bar-background-color;
|
||||
|
||||
// style the bar in chrome
|
||||
.progress[value]::-webkit-progress-value {
|
||||
border-radius: $radius;
|
||||
transition: width 1s ease-out;
|
||||
}
|
||||
|
||||
// style the bar in firefox
|
||||
.progress[value]::-moz-progress-bar {
|
||||
border-radius: $radius;
|
||||
transition: width 1s ease-out;
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ $navbar-background-color: transparent;
|
||||
$menu-item-hover-background-color: $blue;
|
||||
$menu-item-hover-color: $white;
|
||||
|
||||
$progress-bar-background-color: lighten($grey-light, 15%);
|
||||
$progress-bar-background-color: $ui-gray-050;
|
||||
|
||||
$base-border: 1px solid $ui-gray-300;
|
||||
$light-border: 1px solid $ui-gray-200;
|
||||
|
||||
@@ -119,7 +119,7 @@
|
||||
</form>
|
||||
{{else}}
|
||||
<form {{action 'onSubmit' (hash key=key) on="submit"}} id="shamir">
|
||||
<div class="box is-marginless is-shadowless">
|
||||
<div class="box is-shadowless">
|
||||
{{#if errors}}
|
||||
<div class="box is-shadowless is-marginless no-padding-top is-fullwidth">
|
||||
{{message-error errors=errors}}
|
||||
|
||||
194
ui/app/templates/components/shamir-modal-flow.hbs
Normal file
194
ui/app/templates/components/shamir-modal-flow.hbs
Normal file
@@ -0,0 +1,194 @@
|
||||
<Modal
|
||||
@title="Generate operation token"
|
||||
@onClose={{action 'onClose'}}
|
||||
@isActive={{isActive}}
|
||||
@type="warning"
|
||||
@showCloseButton={{true}}
|
||||
>
|
||||
<section class="modal-card-body">
|
||||
{{#if encoded_token}}
|
||||
<p class="has-bottom-margin-l">
|
||||
{{#if otp}}
|
||||
There are three values below. The encoded operation token and the OTP are used in the <span class="has-text-semibold">DR operation token command</span>, which you will need to copy and paste into your CLI in order to generate the operation token. Save your encoded operation token and OTP.
|
||||
{{else}}
|
||||
The encoded operation token and the OTP are used in the <span class="has-text-semibold">DR operation token command</span>, which you will need to copy and paste into your CLI in order to generate the operation token. Save your encoded operation token and OTP.
|
||||
{{/if}}
|
||||
</p>
|
||||
<div class="has-bottom-margin-m">
|
||||
<h4 class="title is-7 has-bottom-padding-m is-fullwidth">
|
||||
Encoded operation token
|
||||
</h4>
|
||||
<div class="message is-list has-copy-button" tabindex="-1">
|
||||
<HoverCopyButton @copyValue={{encoded_token}} />
|
||||
<code class="is-word-break" data-test-shamir-encoded-token>{{encoded_token}}</code>
|
||||
</div>
|
||||
{{#if otp}}
|
||||
<h4 class="title is-7 has-bottom-padding-m is-fullwidth">
|
||||
One time password (OTP)
|
||||
</h4>
|
||||
<div class="message is-list has-copy-button" tabindex="-1">
|
||||
<HoverCopyButton @copyValue={{otp}} />
|
||||
<code class="is-word-break">{{otp}}</code>
|
||||
</div>
|
||||
{{/if}}
|
||||
<h4 class="title is-7 has-bottom-padding-m is-fullwidth">
|
||||
DR operation token command
|
||||
</h4>
|
||||
<div class="message is-list has-copy-button" tabindex="-1">
|
||||
{{#let (if otp
|
||||
(concat 'vault operator generate-root -otp="' otp '" -decode="' encoded_token '"') (concat 'vault operator generate-root -otp="<enter your otp here>" -decode="' encoded_token '"') ) as |cmd|}}
|
||||
<HoverCopyButton @copyValue={{cmd}} />
|
||||
<code class="is-word-break">{{cmd}}</code>
|
||||
{{/let}}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<button type="button" class="button is-primary" {{action 'onCancelClose'}}>
|
||||
Clear & Close
|
||||
</button>
|
||||
</div>
|
||||
{{else if (and generateAction (not started))}}
|
||||
<form {{action 'startGenerate' (hash pgp_key=pgp_key) on="submit"}} id="shamir">
|
||||
{{message-error errors=errors}}
|
||||
{{#if (eq generateStep 'chooseMethod')}}
|
||||
<div class="has-bottom-margin-m" data-test-shamir-modal-body>
|
||||
{{yield}}
|
||||
</div>
|
||||
<div class="field is-grouped">
|
||||
<div class="control is-flex-row">
|
||||
<button type="button" class="link" {{action (mut generateWithPGP) true}}>
|
||||
Provide PGP Key
|
||||
</button>
|
||||
</div>
|
||||
<div class="control">
|
||||
<span class="button auto-width is-white is-static">
|
||||
or
|
||||
</span>
|
||||
</div>
|
||||
<div class="control">
|
||||
<button type="submit" class="button is-primary" data-test-generate-token-cta>
|
||||
Generate operation token
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if (eq generateStep 'providePGPKey')}}
|
||||
<div class="has-bottom-margin-m">
|
||||
<p>
|
||||
Choose a PGP Key from your computer or paste the contents of one in the form below.
|
||||
This key will be used to Encrypt the generated operation token.
|
||||
</p>
|
||||
{{pgp-file index='' key=pgpKeyFile onChange=(action 'setKey')}}
|
||||
</div>
|
||||
<div class="field is-grouped">
|
||||
<div class="control">
|
||||
<button type="button" class="button" {{action "reset"}}>
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
<div class="control">
|
||||
<button type="button" disabled={{not pgp_key}} class="button is-primary" {{action "savePGPKey"}}>
|
||||
Use PGP Key
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if (eq generateStep 'beginGenerationWithPGP')}}
|
||||
<div>
|
||||
<p class="has-bottom-margin-m">
|
||||
Below is the base-64 encoded PGP Key that will be used to encrypt the generated operation token.
|
||||
Next we'll enter portions of the master key to generate an operation token. Click the "Generate operation token" button to proceed.
|
||||
</p>
|
||||
<h4 class="title is-7 has-bottom-padding-m is-fullwidth">
|
||||
PGP Key {{pgpKeyFile.fileName}}
|
||||
</h4>
|
||||
<div class="message is-list has-copy-button" tabindex="-1">
|
||||
<HoverCopyButton @copyValue={{pgp_key}} />
|
||||
<code class="is-word-break">{{pgp_key}}</code>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field is-grouped">
|
||||
<div class="control">
|
||||
<button type="button" class="button" {{action "reset"}}>
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
<div class="control">
|
||||
<button type="submit" disabled={{and (not pgp_key)}} class="button is-primary">
|
||||
Generate operation token
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</form>
|
||||
{{else}}
|
||||
<form {{action 'onSubmit' (hash key=key) on="submit"}} id="shamir">
|
||||
<div class="has-bottom-margin-m">
|
||||
{{#if errors}}
|
||||
<div class="box is-shadowless is-marginless no-padding-top is-fullwidth">
|
||||
{{message-error errors=errors}}
|
||||
</div>
|
||||
{{/if}}
|
||||
<div class="box is-shadowless is-marginless no-padding-top is-fullwidth no-padding-sides" data-test-form-text>
|
||||
{{#if otp}}
|
||||
<div>
|
||||
<h4 class="title is-7 has-bottom-padding-m is-fullwidth">
|
||||
One-time password (OTP)
|
||||
</h4>
|
||||
<div class="message is-list has-copy-button" tabindex="-1">
|
||||
<HoverCopyButton @copyValue={{otp}} />
|
||||
<code class="is-word-break">{{otp}}</code>
|
||||
</div>
|
||||
</div>
|
||||
<div class="has-text-grey is-size-8 has-bottom-margin-l">
|
||||
This OTP will be used to encode the generated operation token. <span class="has-text-semibold">Save this</span>, as you will need it later to decode the operation token.
|
||||
</div>
|
||||
{{/if}}
|
||||
<p>
|
||||
Generate an operation token by entering a portion of the master key. Once all portions are entered, the generated token may be used to manage your secondary Disaster Recovery cluster.
|
||||
</p>
|
||||
</div>
|
||||
<div class="field {{if paddingless 'has-margin-top'}}">
|
||||
<label for="key" class="is-label">
|
||||
Master Key Portion
|
||||
</label>
|
||||
<div class="control">
|
||||
{{input class="input"type="password" name="key" value=key data-test-shamir-input=true}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="has-bottom-margin-m">
|
||||
<div class="columns is-mobile">
|
||||
<div class="column is-narrow">
|
||||
<button
|
||||
type="submit"
|
||||
class="button is-primary"
|
||||
disabled={{loading}}
|
||||
data-test-shamir-submit=true
|
||||
>
|
||||
{{if generateAction "Generate Token" buttonText}}
|
||||
</button>
|
||||
</div>
|
||||
<div class="column is-flex-v-centered is-flex-end">
|
||||
{{#if (or started hasProgress)}}
|
||||
<ShamirProgress
|
||||
@threshold={{threshold}}
|
||||
@progress={{progress}}
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{{/if}}
|
||||
</section>
|
||||
<footer class="modal-card-foot modal-card-foot-outlined">
|
||||
<button
|
||||
class="button is-secondary"
|
||||
onclick={{action 'onCancelClose'}}
|
||||
data-test-shamir-modal-cancel-button
|
||||
>
|
||||
{{if encoded_token 'Close' 'Cancel'}}
|
||||
</button>
|
||||
</footer>
|
||||
</Modal>
|
||||
@@ -89,7 +89,7 @@
|
||||
<Icon @glyph="copy-action" aria-label="Copy" />
|
||||
</CopyButton>
|
||||
</div>
|
||||
<p class="help has-bottom-margin">Plaintext is base64 encoded</p>
|
||||
<p class="help has-bottom-margin-m">Plaintext is base64 encoded</p>
|
||||
<h2 class="title is-6">Ciphertext</h2>
|
||||
<div class="copy-text level">
|
||||
<code class="level-left">{{ciphertext}}</code>
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
<WizardContent @headerText="Replication" @glyph="tour">
|
||||
<WizardSection
|
||||
@headerText="Your cluster information"
|
||||
@docText="Docs: Replication"
|
||||
@docPath="/docs/internals/replication.html"
|
||||
>
|
||||
<p>
|
||||
Here you can see the details about your new replication cluster, manage or disable replication, and handle secondary clusters. You can also get a quick status by hovering over the "Replication" link at the top.
|
||||
</p>
|
||||
</WizardSection>
|
||||
<div class="wizard-details">
|
||||
<h3 class="title is-6">
|
||||
Ready to move on?
|
||||
</h3>
|
||||
<button type="button" class="button next-feature-step" {{action onAdvance}}>
|
||||
{{nextFeature}} <Chevron @isButton={{true}} />
|
||||
</button>
|
||||
</div>
|
||||
</WizardContent>
|
||||
@@ -8,6 +8,16 @@
|
||||
<p>
|
||||
Vault has two kinds of replication, each for a different purpose. Do you want to keep a backup of your data, or are you more interested in speed of access?
|
||||
</p>
|
||||
|
||||
<LearnLink @path="/vault/operations/ops-replication">
|
||||
<Icon @glyph="learn" aria-hidden="true" />Learn: Setting Up Performance Replication
|
||||
</LearnLink>
|
||||
|
||||
<LearnLink @path="/vault/operations/ops-disaster-recovery">
|
||||
<Icon @glyph="learn" aria-hidden="true" />
|
||||
Learn: Setting up Disaster Recovery
|
||||
</LearnLink>
|
||||
|
||||
</WizardSection>
|
||||
<WizardSection @headerText="Cluster mode" @class="wizard-details">
|
||||
<p>
|
||||
|
||||
@@ -62,6 +62,30 @@
|
||||
</nav>
|
||||
<hr/>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
{{#if (has-permission 'status' routeParams='replication')}}
|
||||
<nav class="menu">
|
||||
<p class="menu-label">Replication</p>
|
||||
<ul>
|
||||
{{#if cluster.anyReplicationEnabled}}
|
||||
<li>
|
||||
{{#link-to
|
||||
"vault.cluster.replication-dr-promote.details"
|
||||
disabled=(not currentToken)
|
||||
invokeAction=(action onLinkClick)
|
||||
}}
|
||||
{{replication-mode-summary
|
||||
mode="dr"
|
||||
display='menu'
|
||||
cluster=cluster
|
||||
}}
|
||||
{{/link-to}}
|
||||
</li>
|
||||
{{/if}}
|
||||
</ul>
|
||||
</nav>
|
||||
<hr/>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/unless}}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div class="splash-page-container section is-flex-v-centered-tablet is-flex-1 is-fullwidth">
|
||||
<div class="columns is-centered is-gapless is-fullwidth">
|
||||
<div class="column is-4-desktop is-6-tablet">
|
||||
<div class="has-text-grey has-bottom-margin has-current-color-fill">
|
||||
<div class="has-text-grey has-bottom-margin-m has-current-color-fill">
|
||||
<LogoEdition />
|
||||
</div>
|
||||
<AlertBanner
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
<SplashPage as |Page|>
|
||||
<Page.header>
|
||||
<h1 class="title is-4">
|
||||
Disaster Recovery secondary is enabled
|
||||
</h1>
|
||||
</Page.header>
|
||||
<Page.content>
|
||||
<nav class="tabs sub-nav is-marginless">
|
||||
<ul>
|
||||
<li class="{{if (eq action '') 'is-active'}}">
|
||||
{{#link-to 'vault.cluster.replication-dr-promote' (query-params action='')}}
|
||||
Operation token
|
||||
{{/link-to}}
|
||||
</li>
|
||||
<li class="{{if (eq action 'update') 'is-active'}}">
|
||||
{{#link-to 'vault.cluster.replication-dr-promote' (query-params action='update')}}
|
||||
Update primary
|
||||
{{/link-to}}
|
||||
</li>
|
||||
<li class="{{if (eq action 'promote') 'is-active'}}">
|
||||
{{#link-to 'vault.cluster.replication-dr-promote' (query-params action='promote')}}
|
||||
Promote
|
||||
{{/link-to}}
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
{{#if (eq action 'promote')}}
|
||||
<AlertBanner
|
||||
@type="warning"
|
||||
@title="Caution"
|
||||
@message="Vault Replication is not designed for active-active usage and enabling two performance primaries should never be done, as it can lead to data loss if they or their secondaries are ever reconnected."
|
||||
@class="unseal-warning"
|
||||
data-test-cluster-status
|
||||
/>
|
||||
<ReplicationActions
|
||||
@replicationMode="dr"
|
||||
@selectedAction="promote"
|
||||
@model={{model}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{#if (eq action 'update')}}
|
||||
<ReplicationActions
|
||||
@replicationMode="dr"
|
||||
@selectedAction="update-primary"
|
||||
@model={{model}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{#unless action}}
|
||||
<ShamirFlow
|
||||
@action="generate-dr-operation-token"
|
||||
@buttonText="Promote cluster"
|
||||
@fetchOnInit=true
|
||||
@generateAction=true
|
||||
>
|
||||
<p>
|
||||
Generate an Operation Token by entering a portion of the master key.
|
||||
Once all portions are entered, the generated operation token may be used to manage your secondary Disaster Recovery cluster.
|
||||
</p>
|
||||
</ShamirFlow>
|
||||
{{/unless}}
|
||||
</Page.content>
|
||||
</SplashPage>
|
||||
@@ -0,0 +1,45 @@
|
||||
<section class="section">
|
||||
<div class="container is-widescreen">
|
||||
<ReplicationPage @model={{model}} as |Page|>
|
||||
<Page.header
|
||||
@showTabs={{true}}
|
||||
/>
|
||||
{{#if Page.isDisabled}}
|
||||
<EmptyState
|
||||
@title="Disaster Recovery secondary not set up"
|
||||
@message={{Page.message}}
|
||||
@icon="alert-circle-outline"
|
||||
@bottomBorder={{true}}
|
||||
>
|
||||
<nav class="breadcrumb">
|
||||
<ul class="is-grouped-split">
|
||||
<li>
|
||||
{{#link-to "vault.cluster.secrets.backends" }}
|
||||
<span class="sep"/>
|
||||
Go back
|
||||
{{/link-to}}
|
||||
</li>
|
||||
<li>
|
||||
<LearnLink @path="/vault/operations/ops-disaster-recovery" @class="has-text-grey">
|
||||
Need help?
|
||||
</LearnLink>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</EmptyState>
|
||||
{{else}}
|
||||
<Page.dashboard
|
||||
{{!-- passing in component to render so that the yielded components are flexible based on the dashboard --}}
|
||||
@componentToRender='replication-secondary-card' as |Dashboard|>
|
||||
<Dashboard.card
|
||||
@title="Status"
|
||||
/>
|
||||
<Dashboard.card
|
||||
@title="Primary cluster"
|
||||
/>
|
||||
<Dashboard.rows/>
|
||||
</Page.dashboard>
|
||||
{{/if}}
|
||||
</ReplicationPage>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,41 @@
|
||||
<section class="section">
|
||||
<div class="container is-widescreen">
|
||||
<ReplicationPage @model={{model}} as |Page|>
|
||||
<Page.header
|
||||
@showTabs={{true}}
|
||||
/>
|
||||
{{#if Page.isDisabled}}
|
||||
<EmptyState
|
||||
@title="Disaster Recovery secondary not set up"
|
||||
@message={{Page.message}}
|
||||
@icon="alert-circle-outline"
|
||||
@bottomBorder={{true}}
|
||||
>
|
||||
<nav class="breadcrumb">
|
||||
<ul class="is-grouped-split">
|
||||
<li>
|
||||
{{#link-to "vault.cluster.secrets.backends" }}
|
||||
<span class="sep"/>
|
||||
Go back
|
||||
{{/link-to}}
|
||||
</li>
|
||||
<li>
|
||||
<LearnLink @path="/vault/operations/ops-disaster-recovery" @class="has-text-grey">
|
||||
Need help?
|
||||
</LearnLink>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</EmptyState>
|
||||
{{else}}
|
||||
<section>
|
||||
<ReplicationActions
|
||||
@replicationMode="dr"
|
||||
@model={{model}}
|
||||
@onPromote={{action "onPromote"}}
|
||||
/>
|
||||
</section>
|
||||
{{/if}}
|
||||
</ReplicationPage>
|
||||
</div>
|
||||
</section>
|
||||
@@ -12,9 +12,11 @@ import layout from '../templates/components/alert-banner';
|
||||
* <AlertBanner @type="danger" @message="{{model.keyId}} is not a valid lease ID"/>
|
||||
* ```
|
||||
*
|
||||
* @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.
|
||||
* @param {String} type=null - The banner type. This comes from the message-types helper.
|
||||
* @param {String} [secondIconType=null] - If you want a second icon to appear to the right of the title. This comes from the message-types helper.
|
||||
* @param {Object} [progressBar=null] - An object containing a value and maximum for a progress bar. Will be displayed next to the message title.
|
||||
* @param {String} [message=null] - The message to display within the banner.
|
||||
* @param {String} [title=null] - A title to show above the message. If this is not provided, there are default values for each type of alert.
|
||||
*
|
||||
*/
|
||||
|
||||
@@ -23,6 +25,8 @@ export default Component.extend({
|
||||
type: null,
|
||||
message: null,
|
||||
title: null,
|
||||
secondIconType: null,
|
||||
progressBar: null,
|
||||
yieldWithoutColumn: false,
|
||||
classNameBindings: ['containerClass'],
|
||||
|
||||
@@ -33,4 +37,8 @@ export default Component.extend({
|
||||
alertType: computed('type', function() {
|
||||
return messageTypes([this.get('type')]);
|
||||
}),
|
||||
|
||||
secondAlertType: computed('secondIconType', function() {
|
||||
return messageTypes([this.get('secondIconType')]);
|
||||
}),
|
||||
});
|
||||
|
||||
37
ui/lib/core/addon/components/confirmation-modal.js
Normal file
37
ui/lib/core/addon/components/confirmation-modal.js
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* @module ConfirmationModal
|
||||
* ConfirmationModal components are used to provide an alternative to ConfirmationButton that automatically prompts the user to fill in confirmation text before they can continue with a potentially destructive action. It is built off the Modal component
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* <ConfirmationModal
|
||||
* @onConfirm={action "destructiveAction"}
|
||||
* @title="Do Dangerous Thing?"
|
||||
* @isActive={{isModalActive}}
|
||||
* @onClose={{action (mut isModalActive) false}}
|
||||
* />
|
||||
* ```
|
||||
* @param {function} onConfirm - onConfirm is the action that happens when user clicks onConfirm after filling in the confirmation block
|
||||
* @param {boolean} isActive - Controls whether the modal is "active" eg. visible or not.
|
||||
* @param {string} title - Title of the modal
|
||||
* @param {function} onClose - specify what to do when user attempts to close modal
|
||||
* @param {string} [buttonText=Confirm] - Button text on the confirm button
|
||||
* @param {string} [confirmText=Yes] - The confirmation text that the user must type before continuing
|
||||
* @param {string} [buttonClass=is-danger] - extra class to add to confirm button (eg. "is-danger")
|
||||
* @param {sting} [type=warning] - Applies message-type styling to header. Override to default with empty string
|
||||
* @param {string} [toConfirmMsg] - Finishes the sentence "Type YES to confirm ..."
|
||||
* @param {string} [testSelector] - The unique test selector used on the input to fill in text during tests.
|
||||
*/
|
||||
|
||||
import Component from '@ember/component';
|
||||
import layout from '../templates/components/confirmation-modal';
|
||||
|
||||
export default Component.extend({
|
||||
layout,
|
||||
buttonClass: 'is-danger',
|
||||
buttonText: 'Confirm',
|
||||
confirmText: 'Yes',
|
||||
type: 'warning',
|
||||
toConfirmMsg: '',
|
||||
testSelector: '',
|
||||
});
|
||||
@@ -13,7 +13,7 @@ import layout from '../templates/components/empty-state';
|
||||
*
|
||||
* @param title=null{String} - A short label for the empty state
|
||||
* @param message=null{String} - A description of why a user might be seeing the empty state and possibly instructions for actions they may take.
|
||||
*
|
||||
* @param [icon='']{String} - A optional param to display icon to the right of the title
|
||||
*/
|
||||
|
||||
export default Component.extend({
|
||||
@@ -21,4 +21,5 @@ export default Component.extend({
|
||||
tagName: '',
|
||||
title: null,
|
||||
message: null,
|
||||
icon: '',
|
||||
});
|
||||
|
||||
@@ -11,11 +11,12 @@ import layout from '../templates/components/info-table-row';
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* <InfoTableRow @value={{5}} @label="TTL" />
|
||||
* <InfoTableRow @value={{5}} @label="TTL" @helperText="Some description"/>
|
||||
* ```
|
||||
*
|
||||
* @param value=null {any} - The the data to be displayed - by default the content of the component will only show if there is a value. Also note that special handling is given to boolean values - they will render `Yes` for true and `No` for false.
|
||||
* @param label=null {string} - The display name for the value.
|
||||
* @param helperText=null {string} - Text to describe the value displayed beneath the label.
|
||||
* @param alwaysRender=false {Boolean} - Indicates if the component content should be always be rendered. When false, the value of `value` will be used to determine if the component should render.
|
||||
*
|
||||
*/
|
||||
@@ -27,6 +28,7 @@ export default Component.extend({
|
||||
|
||||
alwaysRender: false,
|
||||
label: null,
|
||||
helperText: null,
|
||||
value: null,
|
||||
|
||||
valueIsBoolean: computed('value', function() {
|
||||
|
||||
27
ui/lib/core/addon/components/info-table.js
Normal file
27
ui/lib/core/addon/components/info-table.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import Component from '@ember/component';
|
||||
import layout from '../templates/components/info-table';
|
||||
|
||||
/**
|
||||
* @module InfoTable
|
||||
* InfoTable components are a table with a single column and header. They are used to render a list of InfoTableRow components.
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* <InfoTable
|
||||
@title="Known Primary Cluster Addrs"
|
||||
@header="cluster_addr"
|
||||
@items={{knownPrimaryClusterAddrs}}
|
||||
/>
|
||||
* ```
|
||||
* @param {String} [title=Info Table] - The title of the table. Used for accessibility purposes.
|
||||
* @param {String} header=null - The column header.
|
||||
* @param {Array} items=null - An array of strings which will be used as the InfoTableRow value.
|
||||
*/
|
||||
|
||||
export default Component.extend({
|
||||
layout,
|
||||
tagName: '',
|
||||
title: 'Info Table',
|
||||
header: null,
|
||||
items: null,
|
||||
});
|
||||
@@ -1,9 +1,11 @@
|
||||
import { computed } from '@ember/object';
|
||||
import Component from '@ember/component';
|
||||
import utils from 'vault/lib/key-utils';
|
||||
import layout from '../templates/components/key-value-header';
|
||||
import { encodePath } from 'vault/utils/path-encoding-helpers';
|
||||
|
||||
export default Component.extend({
|
||||
layout,
|
||||
tagName: 'nav',
|
||||
classNames: 'key-value-header breadcrumb',
|
||||
ariaLabel: 'breadcrumbs',
|
||||
@@ -10,12 +10,32 @@
|
||||
* @param {function} onClose - onClose is the action taken when someone clicks the modal background or close button (if shown).
|
||||
* @param {string} [title] - This text shows up in the header section of the modal.
|
||||
* @param {boolean} [showCloseButton=false] - controls whether the close button in the top right corner shows.
|
||||
* @param {string} type=null - The header type. This comes from the message-types helper.
|
||||
*/
|
||||
|
||||
import Component from '@ember/component';
|
||||
import { computed } from '@ember/object';
|
||||
import { messageTypes } from 'core/helpers/message-types';
|
||||
import layout from '../templates/components/modal';
|
||||
|
||||
export default Component.extend({
|
||||
layout,
|
||||
title: null,
|
||||
showCloseButton: false,
|
||||
type: null,
|
||||
glyph: computed('type', function() {
|
||||
const modalType = this.get('type');
|
||||
if (!modalType) {
|
||||
return;
|
||||
}
|
||||
return messageTypes([this.get('type')]);
|
||||
}),
|
||||
modalClass: computed('type', function() {
|
||||
const modalType = this.get('type');
|
||||
if (!modalType) {
|
||||
return 'modal';
|
||||
}
|
||||
return 'modal ' + messageTypes([this.get('type')]).class;
|
||||
}),
|
||||
onClose: () => {},
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
import Actions from './replication-actions-single';
|
||||
import layout from '../templates/components/replication-action-generate-token';
|
||||
|
||||
export default Actions.extend({
|
||||
layout,
|
||||
});
|
||||
@@ -3,7 +3,7 @@ import Component from '@ember/component';
|
||||
export default Component.extend({
|
||||
onSubmit() {},
|
||||
replicationMode: null,
|
||||
replicationDisplayMode: null,
|
||||
replicationModeForDisplay: null,
|
||||
model: null,
|
||||
|
||||
actions: {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { alias } from '@ember/object/computed';
|
||||
import Component from '@ember/component';
|
||||
import { computed } from '@ember/object';
|
||||
import ReplicationActions from 'core/mixins/replication-actions';
|
||||
import layout from '../templates/components/replication-actions';
|
||||
|
||||
@@ -10,7 +9,6 @@ const DEFAULTS = {
|
||||
primary_cluster_addr: null,
|
||||
errors: [],
|
||||
id: null,
|
||||
replicationMode: null,
|
||||
force: false,
|
||||
};
|
||||
|
||||
@@ -19,7 +17,6 @@ export default Component.extend(ReplicationActions, DEFAULTS, {
|
||||
replicationMode: null,
|
||||
model: null,
|
||||
cluster: alias('model'),
|
||||
|
||||
reset() {
|
||||
if (!this || this.isDestroyed || this.isDestroying) {
|
||||
return;
|
||||
@@ -27,19 +24,9 @@ export default Component.extend(ReplicationActions, DEFAULTS, {
|
||||
this.setProperties(DEFAULTS);
|
||||
},
|
||||
|
||||
replicationDisplayMode: computed('replicationMode', function() {
|
||||
const replicationMode = this.get('replicationMode');
|
||||
if (replicationMode === 'dr') {
|
||||
return 'DR';
|
||||
}
|
||||
if (replicationMode === 'performance') {
|
||||
return 'Performance';
|
||||
}
|
||||
}),
|
||||
|
||||
actions: {
|
||||
onSubmit() {
|
||||
return this.submitHandler(...arguments);
|
||||
return this.submitHandler.perform(...arguments);
|
||||
},
|
||||
clear() {
|
||||
this.reset();
|
||||
|
||||
102
ui/lib/core/addon/components/replication-dashboard.js
Normal file
102
ui/lib/core/addon/components/replication-dashboard.js
Normal file
@@ -0,0 +1,102 @@
|
||||
import Component from '@ember/component';
|
||||
import { computed } from '@ember/object';
|
||||
import { clusterStates } from 'core/helpers/cluster-states';
|
||||
import { capitalize } from '@ember/string';
|
||||
import { htmlSafe } from '@ember/template';
|
||||
import layout from '../templates/components/replication-dashboard';
|
||||
|
||||
/**
|
||||
* @module ReplicationDashboard
|
||||
* The `ReplicationDashboard` component is a contextual component of the replication-page component.
|
||||
* It organizes cluster data specific to mode (dr or performance) and also the type (primary or secondary).
|
||||
* It is the parent contextual component of the replication-<name>-card components.
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* <ReplicationDashboard
|
||||
@data={{model}}
|
||||
@componentToRender='replication-primary-card'
|
||||
@isSecondary=false
|
||||
@isSummaryDashboard=false
|
||||
@replicationDetailsSummary={}
|
||||
@replicationDetails={{replicationDetails}}
|
||||
@clusterMode=primary
|
||||
@reindexingDetails={{reindexingDetails}}
|
||||
/>
|
||||
* ```
|
||||
* @param {Object} data=null - An Ember data object that is pulled from the Ember Cluster Model.
|
||||
* @param {String} [componentToRender=''] - A string that determines which card component is displayed. There are three options, replication-primary-card, replication-secondary-card, replication-summary-card.
|
||||
* @param {Boolean} [isSecondary=false] - Used to determine the title and display logic.
|
||||
* @param {Boolean} [isSummaryDashboard=false] - Only true when the cluster is both a dr and performance primary. If true, replicationDetailsSummary is populated and used to pass through the cluster details.
|
||||
* @param {Object} replicationDetailsSummary=null - An Ember data object computed off the Ember Model. It combines the Model.dr and Model.performance objects into one and contains details specific to the mode replication.
|
||||
* @param {Object} replicationDetails=null - An Ember data object pulled from the Ember Model. It contains details specific to the whether the replication is dr or performance.
|
||||
* @param {String} clusterMode=null - The cluster mode passed through to a table component.
|
||||
* @param {Object} reindexingDetails=null - An Ember data object used to show a reindexing progress bar.
|
||||
*/
|
||||
|
||||
export default Component.extend({
|
||||
layout,
|
||||
componentToRender: '',
|
||||
data: null,
|
||||
isSecondary: false,
|
||||
isSummaryDashboard: false,
|
||||
replicationDetails: null,
|
||||
replicationDetailsSummary: null,
|
||||
isSyncing: computed('replicationDetails.{state}', 'isSecondary', function() {
|
||||
const { state } = this.replicationDetails;
|
||||
const isSecondary = this.isSecondary;
|
||||
return isSecondary && state && clusterStates([state]).isSyncing;
|
||||
}),
|
||||
isReindexing: computed('replicationDetails.{reindex_in_progress}', function() {
|
||||
const { replicationDetails } = this;
|
||||
return !!replicationDetails.reindex_in_progress;
|
||||
}),
|
||||
reindexingStage: computed('replicationDetails.{reindex_stage}', function() {
|
||||
const { replicationDetails } = this;
|
||||
const stage = replicationDetails.reindex_stage;
|
||||
// specify the stage if we have one
|
||||
if (stage) {
|
||||
return `: ${capitalize(stage)}`;
|
||||
}
|
||||
return '';
|
||||
}),
|
||||
progressBar: computed('replicationDetails.{reindex_building_progress,reindex_building_total}', function() {
|
||||
const { reindex_building_progress, reindex_building_total } = this.replicationDetails;
|
||||
let progressBar = null;
|
||||
|
||||
if (reindex_building_progress && reindex_building_total) {
|
||||
progressBar = {
|
||||
value: reindex_building_progress,
|
||||
max: reindex_building_total,
|
||||
};
|
||||
}
|
||||
|
||||
return progressBar;
|
||||
}),
|
||||
summaryState: computed(
|
||||
'replicationDetailsSummary.dr.{state}',
|
||||
'replicationDetailsSummary.performance.{state}',
|
||||
function() {
|
||||
const { replicationDetailsSummary } = this;
|
||||
const drState = replicationDetailsSummary.dr.state;
|
||||
const performanceState = replicationDetailsSummary.performance.state;
|
||||
|
||||
if (drState !== performanceState) {
|
||||
// when DR and Performance is enabled on the same cluster,
|
||||
// the states should always be the same
|
||||
// we are leaving this console log statement to be sure
|
||||
console.log('DR State: ', drState, 'Performance State: ', performanceState);
|
||||
}
|
||||
|
||||
return drState;
|
||||
}
|
||||
),
|
||||
reindexMessage: computed('isSecondary', 'progressBar', function() {
|
||||
if (!this.isSecondary) {
|
||||
return htmlSafe(
|
||||
'This can cause a delay depending on the size of the data store. You can <b>not</b> use Vault during this time.'
|
||||
);
|
||||
}
|
||||
return 'This can cause a delay depending on the size of the data store. You can use Vault during this time.';
|
||||
}),
|
||||
});
|
||||
17
ui/lib/core/addon/components/replication-doc-link.js
Normal file
17
ui/lib/core/addon/components/replication-doc-link.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import Component from '@ember/component';
|
||||
import layout from '../templates/components/replication-doc-link';
|
||||
|
||||
/**
|
||||
* @module ReplicationDocLink
|
||||
* The `ReplicationDocLink` component is a learn link with helper text used on the Replication Dashboards.
|
||||
* The link takes you to the key monitoring metrics learn doc.
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* <ReplicationDocLink />
|
||||
* ```
|
||||
*/
|
||||
|
||||
export default Component.extend({
|
||||
layout,
|
||||
});
|
||||
31
ui/lib/core/addon/components/replication-header.js
Normal file
31
ui/lib/core/addon/components/replication-header.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import Component from '@ember/component';
|
||||
import layout from '../templates/components/replication-header';
|
||||
|
||||
/**
|
||||
* @module ReplicationHeader
|
||||
* The `ReplicationHeader` is a header component used on the Replication Dashboards.
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* <ReplicationHeader
|
||||
@data={{model}}
|
||||
@title="Secondary"
|
||||
@secondaryID="meep_123"
|
||||
@isSummaryDashboard=false
|
||||
/>
|
||||
* ```
|
||||
* @param {Object} model=null - An Ember data object pulled from the Ember cluster model.
|
||||
* @param {String} title=null - The title of the header.
|
||||
* @param {String} [secondaryID=null] - The secondaryID pulled off of the model object.
|
||||
* @param {Boolean} isSummaryDashboard=false - True when you have both a primary performance and dr cluster dashboard.
|
||||
*/
|
||||
|
||||
export default Component.extend({
|
||||
layout,
|
||||
data: null,
|
||||
classNames: ['replication-header'],
|
||||
isSecondary: null,
|
||||
secondaryId: null,
|
||||
isSummaryDashboard: false,
|
||||
'data-test-replication-header': true,
|
||||
});
|
||||
@@ -15,7 +15,7 @@ export default Component.extend({
|
||||
version: service(),
|
||||
router: service(),
|
||||
namespace: service(),
|
||||
classNameBindings: ['isMenu::box', 'isMenu::level'],
|
||||
classNameBindings: ['isMenu::box'],
|
||||
attributeBindings: ['href', 'target'],
|
||||
display: 'banner',
|
||||
isMenu: equal('display', 'menu'),
|
||||
@@ -48,4 +48,9 @@ export default Component.extend({
|
||||
clusterIdDisplay: replicationAttr('clusterIdDisplay'),
|
||||
mode: null,
|
||||
cluster: null,
|
||||
modeState: computed('cluster', 'mode', function() {
|
||||
const { cluster, mode } = this;
|
||||
const clusterState = cluster[mode].state;
|
||||
return clusterState;
|
||||
}),
|
||||
});
|
||||
|
||||
142
ui/lib/core/addon/components/replication-page.js
Normal file
142
ui/lib/core/addon/components/replication-page.js
Normal file
@@ -0,0 +1,142 @@
|
||||
import Component from '@ember/component';
|
||||
import { computed } from '@ember/object';
|
||||
import layout from '../templates/components/replication-page';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { task } from 'ember-concurrency';
|
||||
|
||||
/**
|
||||
* @module ReplicationPage
|
||||
* The `ReplicationPage` component is the parent contextual component that holds the replication-dashboard, and various replication-<name>-card components.
|
||||
* It is the top level component on routes displaying replication dashboards.
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* <ReplicationPage
|
||||
@model={{cluster}}
|
||||
/>
|
||||
* ```
|
||||
* @param {Object} cluster=null - An Ember data object that is pulled from the Ember Cluster Model.
|
||||
*/
|
||||
|
||||
const MODE = {
|
||||
dr: 'Disaster Recovery',
|
||||
performance: 'Performance',
|
||||
};
|
||||
|
||||
export default Component.extend({
|
||||
layout,
|
||||
store: service(),
|
||||
router: service(),
|
||||
reindexingDetails: null,
|
||||
didReceiveAttrs() {
|
||||
this._super(arguments);
|
||||
this.getReplicationModeStatus.perform();
|
||||
},
|
||||
getReplicationModeStatus: task(function*() {
|
||||
let resp;
|
||||
const { replicationMode } = this.model;
|
||||
|
||||
if (this.isSummaryDashboard) {
|
||||
// the summary dashboard is not mode specific and will error
|
||||
// while running replication/null/status in the replication-mode adapter
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
resp = yield this.get('store')
|
||||
.adapterFor('replication-mode')
|
||||
.fetchStatus(replicationMode);
|
||||
} catch (e) {
|
||||
// do not handle error
|
||||
}
|
||||
this.set('reindexingDetails', resp);
|
||||
}),
|
||||
isSummaryDashboard: computed('model.dr.{mode}', 'model.performance.{mode}', function() {
|
||||
const router = this.router;
|
||||
const currentRoute = router.get('currentRouteName');
|
||||
|
||||
// we only show the summary dashboard in the replication index route
|
||||
if (currentRoute === 'vault.cluster.replication.index') {
|
||||
const drMode = this.model.dr.mode;
|
||||
const performanceMode = this.model.performance.mode;
|
||||
return drMode === 'primary' && performanceMode === 'primary';
|
||||
}
|
||||
}),
|
||||
formattedReplicationMode: computed('model.{replicationMode}', 'isSummaryDashboard', function() {
|
||||
// dr or performance 🤯
|
||||
const { isSummaryDashboard } = this;
|
||||
if (isSummaryDashboard) {
|
||||
return 'Disaster Recovery & Performance';
|
||||
}
|
||||
const mode = this.model.replicationMode;
|
||||
return MODE[mode];
|
||||
}),
|
||||
clusterMode: computed('model.{replicationAttrs}', 'isSummaryDashboard', function() {
|
||||
// primary or secondary
|
||||
const { model } = this;
|
||||
const { isSummaryDashboard } = this;
|
||||
if (isSummaryDashboard) {
|
||||
// replicationAttrs does not exist when summaryDashboard
|
||||
return 'primary';
|
||||
}
|
||||
return model.replicationAttrs.mode;
|
||||
}),
|
||||
isLoadingData: computed('clusterMode', 'model.{replicationAttrs}', function() {
|
||||
const { clusterMode } = this;
|
||||
const { model } = this;
|
||||
const { isSummaryDashboard } = this;
|
||||
if (isSummaryDashboard) {
|
||||
return false;
|
||||
}
|
||||
const clusterId = model.replicationAttrs.clusterId;
|
||||
const replicationDisabled = model.replicationAttrs.replicationDisabled;
|
||||
if (clusterMode === 'bootstrapping' || (!clusterId && !replicationDisabled)) {
|
||||
// if clusterMode is bootstrapping
|
||||
// if no clusterId, the data hasn't loaded yet, wait for another status endpoint to be called
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}),
|
||||
isSecondary: computed('clusterMode', function() {
|
||||
const { clusterMode } = this;
|
||||
return clusterMode === 'secondary';
|
||||
}),
|
||||
replicationDetailsSummary: computed('isSummaryDashboard', function() {
|
||||
const { model } = this;
|
||||
const { isSummaryDashboard } = this;
|
||||
if (!isSummaryDashboard) {
|
||||
return;
|
||||
}
|
||||
if (isSummaryDashboard) {
|
||||
let combinedObject = {};
|
||||
combinedObject.dr = model['dr'];
|
||||
combinedObject.performance = model['performance'];
|
||||
return combinedObject;
|
||||
}
|
||||
}),
|
||||
replicationDetails: computed('model.{replicationMode}', 'isSummaryDashboard', function() {
|
||||
const { model } = this;
|
||||
const { isSummaryDashboard } = this;
|
||||
if (isSummaryDashboard) {
|
||||
// Cannot return null
|
||||
return {};
|
||||
}
|
||||
const replicationMode = model.replicationMode;
|
||||
return model[replicationMode];
|
||||
}),
|
||||
isDisabled: computed('replicationDetails.{mode}', function() {
|
||||
if (this.replicationDetails.mode === 'disabled' || this.replicationDetails.mode === 'primary') {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}),
|
||||
message: computed('model.{anyReplicationEnabled}', 'formattedReplicationMode', function() {
|
||||
let msg;
|
||||
if (this.model.anyReplicationEnabled) {
|
||||
msg = `This ${this.formattedReplicationMode} secondary has not been enabled. You can do so from the ${this.formattedReplicationMode} Primary.`;
|
||||
} else {
|
||||
msg = `This cluster has not been enabled as a ${this.formattedReplicationMode} Secondary. You can do so by enabling replication and adding a secondary from the ${this.formattedReplicationMode} Primary.`;
|
||||
}
|
||||
return msg;
|
||||
}),
|
||||
});
|
||||
58
ui/lib/core/addon/components/replication-secondary-card.js
Normal file
58
ui/lib/core/addon/components/replication-secondary-card.js
Normal file
@@ -0,0 +1,58 @@
|
||||
import Component from '@ember/component';
|
||||
import { computed } from '@ember/object';
|
||||
import layout from '../templates/components/replication-secondary-card';
|
||||
import { clusterStates } from 'core/helpers/cluster-states';
|
||||
|
||||
/**
|
||||
* @module ReplicationSecondaryCard
|
||||
* The `ReplicationSecondaryCard` component is a card-like component. It displays cluster mode details specific for DR and Performance Secondaries.
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* <ReplicationSecondaryCard
|
||||
@title='States'
|
||||
@replicationDetails={{replicationDetails}}
|
||||
/>
|
||||
* ```
|
||||
* @param {String} [title=null] - The title to be displayed on the top left corner of the card.
|
||||
* @param {Object} replicationDetails=null - An Ember data object pulled from the Ember Model. It contains details specific to the mode's replication.
|
||||
*/
|
||||
|
||||
export default Component.extend({
|
||||
layout,
|
||||
tagName: '',
|
||||
title: null,
|
||||
replicationDetails: null,
|
||||
state: computed('replicationDetails.{state}', function() {
|
||||
return this.replicationDetails && this.replicationDetails.state
|
||||
? this.replicationDetails.state
|
||||
: 'unknown';
|
||||
}),
|
||||
connection: computed('replicationDetails.{connection_state}', function() {
|
||||
return this.replicationDetails.connection_state ? this.replicationDetails.connection_state : 'unknown';
|
||||
}),
|
||||
lastRemoteWAL: computed('replicationDetails.{lastRemoteWAL}', function() {
|
||||
return this.replicationDetails && this.replicationDetails.lastRemoteWAL
|
||||
? this.replicationDetails.lastRemoteWAL
|
||||
: 0;
|
||||
}),
|
||||
inSyncState: computed('state', function() {
|
||||
// if our definition of what is considered 'synced' changes,
|
||||
// we should use the clusterStates helper instead
|
||||
return this.state === 'stream-wals';
|
||||
}),
|
||||
hasErrorClass: computed('replicationDetails', 'title', 'state', 'connection', function() {
|
||||
const { title, state, connection } = this;
|
||||
|
||||
// only show errors on the state card
|
||||
if (title === 'Status') {
|
||||
const currentClusterisOk = clusterStates([state]).isOk;
|
||||
const primaryIsOk = clusterStates([connection]).isOk;
|
||||
return !(currentClusterisOk && primaryIsOk);
|
||||
}
|
||||
return false;
|
||||
}),
|
||||
knownPrimaryClusterAddrs: computed('replicationDetails.{knownPrimaryClusterAddrs}', function() {
|
||||
return this.replicationDetails.knownPrimaryClusterAddrs;
|
||||
}),
|
||||
});
|
||||
44
ui/lib/core/addon/components/replication-summary-card.js
Normal file
44
ui/lib/core/addon/components/replication-summary-card.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import Component from '@ember/component';
|
||||
import { computed } from '@ember/object';
|
||||
import layout from '../templates/components/replication-summary-card';
|
||||
|
||||
/**
|
||||
* @module ReplicationSummaryCard
|
||||
* The `ReplicationSummaryCard` is a card-like component. It displays cluster mode details for both DR and Performance
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* <ReplicationSummaryCard
|
||||
@title='States'
|
||||
@replicationDetails={DS.Model.replicationDetailsSummary}
|
||||
/>
|
||||
* ```
|
||||
* @param {String} [title=null] - The title to be displayed on the top left corner of the card.
|
||||
* @param {Object} replicationDetails=null - An Ember data object computed off the Ember Model. It combines the Model.dr and Model.performance objects into one and contains details specific to the mode replication.
|
||||
*/
|
||||
|
||||
export default Component.extend({
|
||||
layout,
|
||||
title: null,
|
||||
replicationDetails: null,
|
||||
lastDrWAL: computed('replicationDetails.dr.{lastWAL}', function() {
|
||||
return this.replicationDetails.dr.lastWAL || 0;
|
||||
}),
|
||||
lastPerformanceWAL: computed('replicationDetails.performance.{lastWAL}', function() {
|
||||
return this.replicationDetails.performance.lastWAL || 0;
|
||||
}),
|
||||
merkleRootDr: computed('replicationDetails.dr.{merkleRoot}', function() {
|
||||
return this.replicationDetails.dr.merkleRoot || '';
|
||||
}),
|
||||
merkleRootPerformance: computed('replicationDetails.performance.{merkleRoot}', function() {
|
||||
return this.replicationDetails.performance.merkleRoot || '';
|
||||
}),
|
||||
knownSecondariesDr: computed('replicationDetails.dr.{knownSecondaries}', function() {
|
||||
const knownSecondaries = this.replicationDetails.dr.knownSecondaries;
|
||||
return knownSecondaries.length;
|
||||
}),
|
||||
knownSecondariesPerformance: computed('replicationDetails.performance.{knownSecondaries}', function() {
|
||||
const knownSecondaries = this.replicationDetails.performance.knownSecondaries;
|
||||
return knownSecondaries.length;
|
||||
}),
|
||||
});
|
||||
37
ui/lib/core/addon/components/replication-table-rows.js
Normal file
37
ui/lib/core/addon/components/replication-table-rows.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import Component from '@ember/component';
|
||||
import { computed } from '@ember/object';
|
||||
import layout from '../templates/components/replication-table-rows';
|
||||
|
||||
/**
|
||||
* @module ReplicationTableRows
|
||||
* The `ReplicationTableRows` component is table component. It displays cluster mode details specific to the cluster of the Dashboard it is used on.
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* <ReplicationTableRows
|
||||
@replicationDetails={{replicationDetails}}
|
||||
@clusterMode="primary"
|
||||
/>
|
||||
* ```
|
||||
* @param {Object} replicationDetails=null - An Ember data object pulled from the Ember Model. It contains details specific to the whether the replication is dr or performance.
|
||||
* @param {String} clusterMode=null - The cluster mode (e.g. primary or secondary) passed through to a table component.
|
||||
*/
|
||||
|
||||
export default Component.extend({
|
||||
layout,
|
||||
classNames: ['replication-table-rows'],
|
||||
replicationDetails: null,
|
||||
clusterMode: null,
|
||||
secondaryId: computed('replicationDetails.{secondaryId}', function() {
|
||||
return this.replicationDetails.secondaryId;
|
||||
}),
|
||||
primaryClusterAddr: computed('replicationDetails.{primaryClusterAddr}', function() {
|
||||
return this.replicationDetails.primaryClusterAddr || 'None set';
|
||||
}),
|
||||
merkleRoot: computed('replicationDetails.{merkleRoot}', function() {
|
||||
return this.replicationDetails.merkleRoot || 'unknown';
|
||||
}),
|
||||
clusterId: computed('replicationDetails.{clusterId}', function() {
|
||||
return this.replicationDetails.clusterId || 'unknown';
|
||||
}),
|
||||
});
|
||||
64
ui/lib/core/addon/helpers/cluster-states.js
Normal file
64
ui/lib/core/addon/helpers/cluster-states.js
Normal file
@@ -0,0 +1,64 @@
|
||||
import { helper as buildHelper } from '@ember/component/helper';
|
||||
|
||||
// A hash of cluster states to ensure that the status menu and replication dashboards
|
||||
// display states and glyphs consistently
|
||||
// this includes states for the primary vault cluster and the connection_state
|
||||
|
||||
export const CLUSTER_STATES = {
|
||||
running: {
|
||||
glyph: 'check-circle-outline',
|
||||
isOk: true,
|
||||
isSyncing: false,
|
||||
},
|
||||
ready: {
|
||||
glyph: 'check-circle-outline',
|
||||
isOk: true,
|
||||
isSyncing: false,
|
||||
},
|
||||
'stream-wals': {
|
||||
glyph: 'check-circle-outline',
|
||||
isOk: true,
|
||||
isSyncing: false,
|
||||
},
|
||||
'merkle-diff': {
|
||||
glyph: 'android-sync',
|
||||
isOk: true,
|
||||
isSyncing: true,
|
||||
},
|
||||
connecting: {
|
||||
glyph: 'android-sync',
|
||||
isOk: true,
|
||||
isSyncing: true,
|
||||
},
|
||||
'merkle-sync': {
|
||||
glyph: 'android-sync',
|
||||
isOk: true,
|
||||
isSyncing: true,
|
||||
},
|
||||
idle: {
|
||||
glyph: 'cancel-square-outline',
|
||||
isOk: false,
|
||||
isSyncing: false,
|
||||
},
|
||||
transient_failure: {
|
||||
glyph: 'cancel-circle-outline',
|
||||
isOk: false,
|
||||
isSyncing: false,
|
||||
},
|
||||
shutdown: {
|
||||
glyph: 'cancel-circle-outline',
|
||||
isOk: false,
|
||||
isSyncing: false,
|
||||
},
|
||||
};
|
||||
|
||||
export function clusterStates([state]) {
|
||||
const defaultDisplay = {
|
||||
glyph: '',
|
||||
isOk: null,
|
||||
isSyncing: null,
|
||||
};
|
||||
return CLUSTER_STATES[state] || defaultDisplay;
|
||||
}
|
||||
|
||||
export default buildHelper(clusterStates);
|
||||
@@ -25,6 +25,12 @@ export const MESSAGE_TYPES = {
|
||||
glyph: 'alert-triangle',
|
||||
text: 'Warning',
|
||||
},
|
||||
loading: {
|
||||
class: 'is-success',
|
||||
glyphClass: 'has-text-success',
|
||||
glyph: 'loading',
|
||||
text: 'Loading',
|
||||
},
|
||||
};
|
||||
|
||||
export function messageTypes([type]) {
|
||||
|
||||
@@ -8,7 +8,8 @@ const ACTIONS = {
|
||||
},
|
||||
dr: {
|
||||
primary: ['disable', 'recover', 'reindex', 'demote'],
|
||||
secondary: ['promote'],
|
||||
// TODO: add disable, recover, and reindex when API is ready
|
||||
secondary: ['promote', 'update-primary', 'generate-token'],
|
||||
bootstrapping: ['disable', 'recover', 'reindex'],
|
||||
},
|
||||
};
|
||||
|
||||
14
ui/lib/core/addon/helpers/replication-mode-description.js
Normal file
14
ui/lib/core/addon/helpers/replication-mode-description.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import { helper as buildHelper } from '@ember/component/helper';
|
||||
|
||||
const REPLICATION_MODE_DESCRIPTIONS = {
|
||||
dr:
|
||||
'Disaster Recovery Replication is designed to protect against catastrophic failure of entire clusters. Secondaries do not forward service requests until they are elected and become a new primary.',
|
||||
performance:
|
||||
'Performance Replication scales workloads horizontally across clusters to make requests faster. Local secondaries handle read requests but forward writes to the primary to be handled.',
|
||||
};
|
||||
|
||||
export function replicationModeDescription([mode]) {
|
||||
return REPLICATION_MODE_DESCRIPTIONS[mode];
|
||||
}
|
||||
|
||||
export default buildHelper(replicationModeDescription);
|
||||
@@ -10,7 +10,8 @@ export default Mixin.create({
|
||||
loading: or('save.isRunning', 'submitSuccess.isRunning'),
|
||||
onEnable() {},
|
||||
onDisable() {},
|
||||
submitHandler(action, clusterMode, data, event) {
|
||||
onPromote() {},
|
||||
submitHandler: task(function*(action, clusterMode, data, event) {
|
||||
let replicationMode = (data && data.replicationMode) || this.get('replicationMode');
|
||||
if (event && event.preventDefault) {
|
||||
event.preventDefault();
|
||||
@@ -22,15 +23,18 @@ export default Mixin.create({
|
||||
data = Object.keys(data).reduce((newData, key) => {
|
||||
var val = data[key];
|
||||
if (isPresent(val)) {
|
||||
if (key === 'dr_operation_token_primary' || key === 'dr_operation_token_promote') {
|
||||
newData['dr_operation_token'] = val;
|
||||
} else {
|
||||
newData[key] = val;
|
||||
}
|
||||
}
|
||||
return newData;
|
||||
}, {});
|
||||
delete data.replicationMode;
|
||||
}
|
||||
|
||||
return this.save.perform(action, replicationMode, clusterMode, data);
|
||||
},
|
||||
return yield this.save.perform(action, replicationMode, clusterMode, data);
|
||||
}),
|
||||
|
||||
save: task(function*(action, replicationMode, clusterMode, data) {
|
||||
let resp;
|
||||
@@ -41,7 +45,7 @@ export default Mixin.create({
|
||||
} catch (e) {
|
||||
return this.submitError(e);
|
||||
}
|
||||
yield this.submitSuccess.perform(resp, action, clusterMode);
|
||||
return yield this.submitSuccess.perform(resp, action, clusterMode);
|
||||
}).drop(),
|
||||
|
||||
submitSuccess: task(function*(resp, action, mode) {
|
||||
@@ -89,12 +93,13 @@ export default Mixin.create({
|
||||
if (action === 'disable') {
|
||||
yield this.onDisable();
|
||||
}
|
||||
if (action === 'enable') {
|
||||
yield this.onEnable(replicationMode);
|
||||
if (action === 'promote') {
|
||||
yield this.onPromote();
|
||||
}
|
||||
|
||||
if (mode === 'secondary' && replicationMode === 'dr') {
|
||||
yield this.router.transitionTo('vault.cluster');
|
||||
if (action === 'enable') {
|
||||
/// onEnable is a method available only to route vault.cluster.replication.index
|
||||
// if action 'enable' is called from vault.cluster.replication.mode.index this method is not called
|
||||
yield this.onEnable(replicationMode, mode);
|
||||
}
|
||||
}).drop(),
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<div data-test-flash-message=true>
|
||||
<div data-test-flash-message="true">
|
||||
<div class="columns is-mobile is-variable is-1">
|
||||
<div class="column is-narrow message-icon">
|
||||
<Icon
|
||||
@size="l"
|
||||
class="{{alertType.glyphClass}}"
|
||||
aria-hidden=true
|
||||
aria-hidden="true"
|
||||
@glyph={{alertType.glyph}}
|
||||
/>
|
||||
</div>
|
||||
@@ -14,6 +14,21 @@
|
||||
<div class="column">
|
||||
<div class="message-title">
|
||||
{{or @title this.alertType.text}}
|
||||
{{#if @secondIconType}}
|
||||
<Icon
|
||||
@size="l"
|
||||
class="{{secondAlertType.glyphClass}}"
|
||||
aria-hidden="true"
|
||||
@glyph={{secondAlertType.glyph}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{#if @progressBar}}
|
||||
<progress
|
||||
value={{progressBar.value}}
|
||||
max={{progressBar.max}}
|
||||
class="progress is-success is-medium is-inline-block"
|
||||
></progress>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{#if @message}}
|
||||
<p class="message-body">
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
@glyph={{this.alertType.glyph}}
|
||||
class={{this.alertType.glyphClass}}
|
||||
/>
|
||||
<p class="{{this.textClass}}">
|
||||
<p class="{{this.textClass}}" data-test-inline-error-message>
|
||||
{{@message}}
|
||||
</p>
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
<Modal
|
||||
@title={{title}}
|
||||
@onClose={{onClose}}
|
||||
@isActive={{isActive}}
|
||||
@type={{type}}
|
||||
@showCloseButton={{true}}
|
||||
>
|
||||
<section class="modal-card-body">
|
||||
|
||||
{{yield}}
|
||||
|
||||
<div class="modal-confirm-section">
|
||||
<p class="has-text-weight-semibold is-size-6">
|
||||
Confirm
|
||||
</p>
|
||||
<p class="is-help">Type <strong>{{confirmText}}</strong> to confirm {{toConfirmMsg}}</p>
|
||||
{{input
|
||||
type="text"
|
||||
value=confirmationInput
|
||||
name="confirmationInput"
|
||||
class="input has-margin-top"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
data-test-confirmation-modal-input=testSelector
|
||||
}}
|
||||
</div>
|
||||
</section>
|
||||
<footer class="modal-card-foot modal-card-foot-outlined">
|
||||
<button
|
||||
class="button {{buttonClass}}"
|
||||
disabled={{unless (eq confirmationInput confirmText) true}}
|
||||
onclick={{onConfirm}}
|
||||
data-test-confirm-button={{testSelector}}
|
||||
>
|
||||
{{buttonText}}
|
||||
</button>
|
||||
<button
|
||||
class="button is-secondary"
|
||||
onclick={{action (mut isActive) false}}
|
||||
data-test-cancel-button>
|
||||
Cancel
|
||||
</button>
|
||||
</footer>
|
||||
</Modal>
|
||||
@@ -1,8 +1,17 @@
|
||||
<div data-test-component="empty-state" class="empty-state" ...attributes>
|
||||
<div class="empty-state-content">
|
||||
{{#if icon}}
|
||||
<div class="empty-state-icon">
|
||||
<Icon @glyph={{icon}} @size="xxl" />
|
||||
<h3 class="empty-state-title" data-test-empty-state-title>
|
||||
{{title}}
|
||||
</h3>
|
||||
</div>
|
||||
{{else}}
|
||||
<h3 class="empty-state-title" data-test-empty-state-title>
|
||||
{{title}}
|
||||
</h3>
|
||||
{{/if}}
|
||||
{{#if message}}
|
||||
<p class="empty-state-message" data-test-empty-state-message>
|
||||
{{message}}
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
{{#if (or alwaysRender value)}}
|
||||
{{#if label}}
|
||||
<div class="column is-one-quarter">
|
||||
<span class="is-label has-text-grey-dark" data-test-row-label="{{label}}">{{label}}</span>
|
||||
{{#if helperText}}
|
||||
<div>
|
||||
<span class="is-label helper-text has-text-grey">{{helperText}}</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
<div class="column is-flex">
|
||||
{{#if (has-block)}}
|
||||
{{yield}}
|
||||
|
||||
23
ui/lib/core/addon/templates/components/info-table.hbs
Normal file
23
ui/lib/core/addon/templates/components/info-table.hbs
Normal file
@@ -0,0 +1,23 @@
|
||||
<div class="vlt-table info-table sticky-header" data-test-info-table>
|
||||
<table class="is-fullwidth">
|
||||
<caption class="is-collapsed">
|
||||
{{title}}
|
||||
</caption>
|
||||
<thead class="has-text-weight-semibold">
|
||||
<tr>
|
||||
<th scope="col">
|
||||
{{header}}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each items as |item|}}
|
||||
<tr>
|
||||
<td>
|
||||
{{info-table-row value=item}}
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="is-flex-v-centered is-flex-1 loader-inner-page" >
|
||||
<div class="is-flex-v-centered is-flex-1 loader-inner-page" data-test-layout-loading>
|
||||
<div class="columns is-centered">
|
||||
<div class="column is-narrow has-text-centered has-text-grey-dark has-current-color-fill">
|
||||
<div class="level is-mobile">
|
||||
|
||||
@@ -1,9 +1,20 @@
|
||||
{{#ember-wormhole to="modal-wormhole"}}
|
||||
<div class="modal {{if isActive 'is-active'}}" aria-modal="true">
|
||||
<div class="{{modalClass}} {{if isActive 'is-active'}}" aria-modal="true">
|
||||
<div class="modal-background" onclick={{onClose}} data-test-modal-background></div>
|
||||
<div class="modal-card">
|
||||
<header class="modal-card-head">
|
||||
<h2 class="modal-card-title title is-5" data-test-modal-title>{{title}}</h2>
|
||||
<h2 class="modal-card-title title is-5" data-test-modal-title>
|
||||
{{#if glyph}}
|
||||
<Icon
|
||||
@size="l"
|
||||
class="{{glyph.glyphClass}}"
|
||||
aria-hidden="true"
|
||||
@glyph={{glyph.glyph}}
|
||||
data-test-modal-glyph={{glyph.glyph}}
|
||||
/>
|
||||
{{/if}}
|
||||
<span>{{title}}</span>
|
||||
</h2>
|
||||
{{#if showCloseButton}}
|
||||
<button class="delete" aria-label="close" onclick={{onClose}} data-test-modal-close-button></button>
|
||||
{{/if}}
|
||||
@@ -1,47 +1,56 @@
|
||||
<h4 class="title is-5 is-marginless">
|
||||
<div class="action-block is-rounded" data-test-demote-replication>
|
||||
<div class="action-block-info">
|
||||
<h4 class="title is-5 is-marginless">
|
||||
Demote cluster
|
||||
</h4>
|
||||
<div class="content">
|
||||
</h4>
|
||||
<p>
|
||||
Demote this {{model.replicationModeForDisplay}} primary to a secondary.
|
||||
</p>
|
||||
</div>
|
||||
<div class="action-block-action">
|
||||
<button
|
||||
class="button is-secondary"
|
||||
onclick={{action (mut isModalActive) true}}
|
||||
data-test-replication-action-trigger
|
||||
>
|
||||
Demote
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ConfirmationModal
|
||||
@title="Demote to secondary?"
|
||||
@onClose={{action (mut isModalActive) false}}
|
||||
@isActive={{isModalActive}}
|
||||
@buttonClass="is-primary"
|
||||
@confirmText={{model.replicationModeForDisplay}}
|
||||
@toConfirmMsg="demoting this cluster"
|
||||
@onConfirm={{action "onSubmit" "demote" model.replicationAttrs.modeForUrl}}
|
||||
@testSelector="demote"
|
||||
>
|
||||
<p class="has-bottom-margin-m">
|
||||
{{#if (and
|
||||
(eq replicationMode 'dr')
|
||||
(not model.performance.replicationDisabled)
|
||||
)
|
||||
}}
|
||||
|
||||
<div data-test-demote-warning>
|
||||
<AlertInline
|
||||
@type="danger"
|
||||
@message="Demoting this DR primary cluster
|
||||
would result in a DR secondary and in that mode Vault is read-only. This
|
||||
cluster is also currently operating as a Performance
|
||||
{{capitalize model.performance.modeForUrl}}, demoting it will leave your
|
||||
replication setup without a performance primary cluster until a new
|
||||
cluster is promoted."
|
||||
/>
|
||||
</div>
|
||||
)}}
|
||||
<p class="has-bottom-margin-m" data-test-demote-warning>
|
||||
This cluster is currently operating as a performance secondary. Demoting it will leave your replication setup <strong>without a performance primary cluster</strong> until a new cluster is promoted.
|
||||
</p>
|
||||
{{/if}}
|
||||
<p>
|
||||
Demote this {{replicationDisplayMode}} primary cluster to a {{replicationDisplayMode}} secondary. The resulting secondary cluster will not
|
||||
attempt to connect to a primary, but will maintain knowledge of its cluster
|
||||
ID and can be reconnected to the same set of replication clusters without
|
||||
wiping local storage.
|
||||
Demoting this {{model.replicationModeForDisplay}} primary to a {{model.replicationModeForDisplay}} secondary means that the resulting secondary cluster:
|
||||
</p>
|
||||
<p>
|
||||
In order to connect the resulting secondary to a new primary, use the <code>Update primary</code> action.
|
||||
<ul class="bullet has-bottom-margin-m">
|
||||
{{#if (and
|
||||
(eq replicationMode 'dr')
|
||||
(not model.performance.replicationDisabled)
|
||||
)}}
|
||||
<li>Will be read-only</li>
|
||||
{{/if}}
|
||||
<li>Will not attempt to connect to a primary</li>
|
||||
<li>Will maintain knowledge of its cluster ID</li>
|
||||
<li>Can be reconnected to the same set of replication clusters without wiping local storage.</li>
|
||||
</ul>
|
||||
<p class="has-bottom-margin-m">
|
||||
To connect the resulting secondary to a new primary, use the Update primary action.
|
||||
</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<ConfirmAction
|
||||
@buttonClasses="button is-primary"
|
||||
@confirmTitle="Demote this cluster?"
|
||||
@confirmMessage="This will affect how your Vault data is replicated."
|
||||
@confirmButtonText="Demote"
|
||||
@horizontalPosition="auto-left"
|
||||
@onConfirmAction={{action "onSubmit" "demote" model.replicationAttrs.modeForUrl}}
|
||||
data-test-replication-demote="true"
|
||||
>
|
||||
Demote cluster
|
||||
</ConfirmAction>
|
||||
</div>
|
||||
</div>
|
||||
</ConfirmationModal>
|
||||
|
||||
@@ -1,56 +1,31 @@
|
||||
<h4 class="title is-5 is-marginless">
|
||||
<div class="action-block is-rounded" data-test-disable-replication>
|
||||
<div class="action-block-info">
|
||||
<h4 class="title is-5 is-marginless">
|
||||
Disable Replication
|
||||
</h4>
|
||||
<div class="content">
|
||||
</h4>
|
||||
<p>
|
||||
Disable {{replicationDisplayMode}} Replication entirely on the cluster.
|
||||
{{#if model.replicationAttrs.isPrimary}}
|
||||
Any secondaries will no longer be able to connect.
|
||||
{{else if (eq model.replicationAttrs.modeForUrl 'bootstrapping')}}
|
||||
<br>
|
||||
Since the cluster is currently bootstrapping, we need to know which mode to disable.
|
||||
Be sure to choose it below.
|
||||
<label for="replication-mode" class="is-label">
|
||||
Replication cluster mode
|
||||
</label>
|
||||
<div class="field is-expanded">
|
||||
<div class="control select is-fullwidth">
|
||||
<select onchange={{action (mut mode) value="target.value"}} id="replication-mode" name="replication-mode">
|
||||
{{#each (array 'primary' 'secondary') as |modeOption|}}
|
||||
<option
|
||||
selected={{if mode (eq mode modeOption) (eq modeOption 'primary')}}
|
||||
value={{modeOption}}
|
||||
Disable {{model.replicationModeForDisplay}} Replication entirely on the cluster.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="action-block-action">
|
||||
<button
|
||||
class="button is-danger"
|
||||
onclick={{action (mut isModalActive) true}}
|
||||
data-test-replication-action-trigger
|
||||
>
|
||||
{{modeOption}}
|
||||
</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
Disable Replication
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
The cluster will no longer be able to connect to the primary.
|
||||
{{/if}}
|
||||
<AlertInline
|
||||
@type="danger"
|
||||
@message="Caution: re-enabling this node as a primary or secondary will change its cluster ID."
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
In the secondary case this means a wipe of the
|
||||
underlying storage when connected to a primary, and in the primary case,
|
||||
secondaries connecting back to the cluster (even if they have connected
|
||||
before) will require a wipe of the underlying storage.
|
||||
</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<ConfirmAction
|
||||
@buttonClasses="button is-primary"
|
||||
@confirmTitle="Disable this cluster?"
|
||||
@confirmMessage="Data in this cluster will no longer be replicated."
|
||||
@confirmButtonText="Disable"
|
||||
@horizontalPosition="auto-left"
|
||||
@onConfirmAction={{action
|
||||
|
||||
<ConfirmationModal
|
||||
@title="Disable Replication?"
|
||||
@onClose={{action (mut isModalActive) false}}
|
||||
@isActive={{isModalActive}}
|
||||
@confirmText={{model.replicationModeForDisplay}}
|
||||
@toConfirmMsg="disabling {{model.replicationModeForDisplay}} Replication on this cluster"
|
||||
@onConfirm={{action
|
||||
"onSubmit"
|
||||
"disable"
|
||||
(if
|
||||
@@ -59,9 +34,20 @@
|
||||
model.replicationAttrs.modeForUrl
|
||||
)
|
||||
}}
|
||||
data-test-disable-replication="true"
|
||||
>
|
||||
Disable Replication
|
||||
</ConfirmAction>
|
||||
</div>
|
||||
</div>
|
||||
@testSelector="disable"
|
||||
>
|
||||
<p class="has-bottom-margin-m">
|
||||
{{#if (and model.replicationAttrs.isPrimary (eq model.replicationMode "dr"))}}This cannot be undone. {{/if}}
|
||||
Disabling {{model.replicationModeForDisplay}} Replication entirely on this {{if (eq model.replicationAttrs.isPrimary true) "primary" "secondary"}} cluster means that:
|
||||
</p>
|
||||
<ul class="bullet">
|
||||
{{#if model.replicationAttrs.isPrimary}}
|
||||
<li>Secondaries will no longer be able to connect</li>
|
||||
<li>We will wipe the underlying storage of connected secondaries</li>
|
||||
<li>Secondaries connecting back to the cluster will require a wipe of the underlying storage</li>
|
||||
{{else}}
|
||||
<li>We will wipe the underlying storage of this secondary when connected to a primary</li>
|
||||
{{/if}}
|
||||
<li>Re-enabling this node will change its cluster ID</li>
|
||||
</ul>
|
||||
</ConfirmationModal>
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<div class="action-block is-rounded" data-test-generate-token-replication>
|
||||
<div class="action-block-info">
|
||||
<h4 class="title is-5 is-marginless">
|
||||
Generate operation token
|
||||
</h4>
|
||||
<p>
|
||||
This token is needed for update and promote actions.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="action-block-action">
|
||||
<button
|
||||
class="button is-tertiary"
|
||||
onclick={{action (mut isModalActive) true}}
|
||||
data-test-replication-action-trigger
|
||||
>
|
||||
Generate token
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<ShamirModalFlow
|
||||
@action="generate-dr-operation-token"
|
||||
@buttonText="Generate token"
|
||||
@fetchOnInit=true
|
||||
@generateAction=true
|
||||
@onClose={{action (mut isModalActive) false}}
|
||||
@isActive={{isModalActive}}
|
||||
>
|
||||
<p>
|
||||
Updating or promoting this cluster requires an operation token, generated by inputting the master key shares. If you'd like to first encrypt the token with a PGP Key, click "Encrypt with PGP key" below, otherwise we can begin generation of the operation token.
|
||||
</p>
|
||||
</ShamirModalFlow>
|
||||
@@ -1,28 +1,54 @@
|
||||
{{#if (and (eq replicationMode 'dr') (eq model.replicationAttrs.modeForUrl 'secondary'))}}
|
||||
<div class="box is-marginless is-shadowless">
|
||||
<div class="action-block is-rounded" data-test-promote-replication>
|
||||
<div class="action-block-info">
|
||||
<h4 class="title is-5 is-marginless">
|
||||
Promote cluster
|
||||
</h4>
|
||||
<p>
|
||||
This cluster is currently running as a DR Replication Secondary.
|
||||
Promote the cluster to a primary by entering DR Operation Token.
|
||||
Promote this cluster to a {{model.replicationModeForDisplay}} primary
|
||||
</p>
|
||||
<div class="field">
|
||||
<label for="dr_operation_token" class="is-label">
|
||||
</div>
|
||||
|
||||
<div class="action-block-action">
|
||||
<button
|
||||
class="button is-tertiary"
|
||||
onclick={{action (mut isModalActive) true}}
|
||||
data-test-replication-action-trigger
|
||||
>
|
||||
Promote
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
@title="Promote cluster?"
|
||||
@onClose={{action (mut isModalActive) false}}
|
||||
@isActive={{isModalActive}}
|
||||
@type="warning"
|
||||
@showCloseButton={{true}}
|
||||
>
|
||||
<section class="modal-card-body">
|
||||
{{#if (eq replicationMode "dr")}}
|
||||
<p class="has-bottom-margin-m">
|
||||
To promote this DR Replication Secondary to a primary, enter the DR Operation token.
|
||||
</p>
|
||||
{{/if}}
|
||||
<p class="has-bottom-margin-m">
|
||||
Vault Replication is not designed for active-active usage. Enabling two primaries should never be done, as it can lead to data loss if they or their secondaries are ever reconnected. If the cluster has a primary, be sure to demote it before promoting a secondary.
|
||||
</p>
|
||||
|
||||
<div data-test-promote-dr-inputs>
|
||||
{{#if (eq replicationMode "dr")}}
|
||||
<div class="field is-borderless">
|
||||
<label for="dr_operation_token_promote" class="is-label is-size-6">
|
||||
DR Operation Token
|
||||
</label>
|
||||
<div class="control">
|
||||
{{input class="input" id="dr_operation_token" name="dr_operation_token" value=dr_operation_token}}
|
||||
{{input class="input" id="dr_operation_token_promote" name="dr_operation_token_promote" value=dr_operation_token_promote}}
|
||||
</div>
|
||||
</div>
|
||||
<ToggleButton
|
||||
@class="is-block"
|
||||
@toggleAttr="showOptions"
|
||||
@toggleTarget={{this}}
|
||||
@openLabel="Hide options"
|
||||
@closedLabel="Options"
|
||||
/>
|
||||
{{#if showOptions}}
|
||||
<div class="box is-marginless">
|
||||
{{/if}}
|
||||
<div class="field">
|
||||
<label for="primary_cluster_addr" class="is-label">
|
||||
<label for="primary_cluster_addr" class="is-label is-size-6">
|
||||
Primary cluster address <em class="is-optional">(optional)</em>
|
||||
</label>
|
||||
<div class="control">
|
||||
@@ -41,89 +67,28 @@
|
||||
checked={{force}}
|
||||
onchange={{action (mut force) value="target.checked"}}
|
||||
/>
|
||||
<label for="forcePromote" class="is-label">
|
||||
<label for="forcePromote" class="is-label is-size-6">
|
||||
Force promotion of this cluster
|
||||
</label>
|
||||
</div>
|
||||
<AlertInline
|
||||
@type="warning"
|
||||
@message="Forcing promotion could result in data loss if data isn't fully replicated. Force promotion
|
||||
promotes the cluster even if certain safety checks fail."
|
||||
/>
|
||||
<p>Promote the cluster even if certain safety checks fail. This could result in data loss of data isn't fully replicated</p>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="box is-marginless is-shadowless">
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<ConfirmAction
|
||||
@buttonClasses="button is-primary"
|
||||
@confirmTitle="Promote this cluster?"
|
||||
@confirmMessage="This will affect how your Vault data is replicated."
|
||||
@confirmButtonText="Promote"
|
||||
@horizontalPosition="auto-left"
|
||||
@onConfirmAction={{action "onSubmit" "promote" model.replicationAttrs.modeForUrl (hash dr_operation_token=dr_operation_token primary_cluster_addr=primary_cluster_addr force=force)}}
|
||||
</section>
|
||||
<footer class="modal-card-foot modal-card-foot-outlined">
|
||||
<button
|
||||
class="button is-primary"
|
||||
disabled={{if (and (eq replicationMode "dr") (not dr_operation_token_promote)) true}}
|
||||
onclick={{action "onSubmit" "promote" model.replicationAttrs.modeForUrl (hash dr_operation_token_promote=dr_operation_token_promote primary_cluster_addr=primary_cluster_addr force=force)}}
|
||||
data-test-promote-confirm-button
|
||||
>
|
||||
Promote cluster
|
||||
</ConfirmAction>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<h4 class="title is-5 is-marginless">
|
||||
Promote cluster
|
||||
</h4>
|
||||
<div class="content">
|
||||
<p>Promote the cluster to primary.
|
||||
<AlertInline
|
||||
@type="warning"
|
||||
@message="Vault Replication is not designed for active-active usage and enabling two primaries should never be done, as it can lead to data loss if they or their secondaries are ever reconnected.
|
||||
If the cluster has a primary, be sure to demote it before promoting a secondary."
|
||||
/>
|
||||
</p>
|
||||
<div class="field">
|
||||
<label for="primary_cluster_addr" class="is-label">
|
||||
Primary cluster address <em class="is-optional">(optional)</em>
|
||||
</label>
|
||||
<div class="control">
|
||||
{{input class="input" id="primary_cluster_addr" name="primary_cluster_addr" value=primary_cluster_addr}}
|
||||
</div>
|
||||
<p class="help has-text-grey">
|
||||
Overrides the cluster address that the primary gives to secondary nodes.
|
||||
</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="b-checkbox">
|
||||
<input type="checkbox"
|
||||
id="forcePromote"
|
||||
class="styled"
|
||||
checked={{force}}
|
||||
onchange={{action (mut force) value="target.checked"}}
|
||||
/>
|
||||
<label for="forcePromote" class="is-label">
|
||||
Force promotion of this cluster
|
||||
</label>
|
||||
</div>
|
||||
<AlertInline
|
||||
@type="warning"
|
||||
@message="Forcing promotion could result in data loss if data isn't fully replicated. Force promotion
|
||||
promotes the cluster even if certain safety checks fail."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<ConfirmAction
|
||||
@buttonClasses="button is-primary"
|
||||
@confirmTitle="Promote this cluster?"
|
||||
@confirmMessage="This will affect how your Vault data is replicated."
|
||||
@confirmButtonText="Promote"
|
||||
@horizontalPosition="auto-left"
|
||||
@onConfirmAction={{action "onSubmit" "promote" model.replicationAttrs.modeForUrl (hash primary_cluster_addr=primary_cluster_addr force=force)}}
|
||||
>
|
||||
Promote cluster
|
||||
</ConfirmAction>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
Promote
|
||||
</button>
|
||||
<button
|
||||
class="button is-secondary"
|
||||
onclick={{action (mut isModalActive) false}}
|
||||
data-test-promote-cancel-button>
|
||||
Cancel
|
||||
</button>
|
||||
</footer>
|
||||
</Modal>
|
||||
|
||||
@@ -1,23 +1,49 @@
|
||||
<h4 class="title is-5 is-marginless">
|
||||
<div class="action-block is-rounded" data-test-recover-replication>
|
||||
<div class="action-block-info">
|
||||
<h4 class="title is-5 is-marginless">
|
||||
Recover
|
||||
</h4>
|
||||
<div class="content">
|
||||
</h4>
|
||||
<p>
|
||||
Attempt recovery if replication is in a bad state, for instance if an error
|
||||
has caused replication to stop syncing.
|
||||
Attempt recovery if replication is in a bad state.
|
||||
</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<ConfirmAction
|
||||
@buttonClasses="button is-primary"
|
||||
@confirmTitle="Begin recovery?"
|
||||
@confirmMessage="This will attempt to recover to continue syncing."
|
||||
@confirmButtonText="Recover"
|
||||
@horizontalPosition="auto-left"
|
||||
@onConfirmAction={{action "onSubmit" "recover"}}
|
||||
</div>
|
||||
|
||||
<div class="action-block-action">
|
||||
<button
|
||||
class="button is-secondary"
|
||||
onclick={{action (mut isModalActive) true}}
|
||||
data-test-replication-action-trigger
|
||||
>
|
||||
Recover
|
||||
</ConfirmAction>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
@title="Begin recovery?"
|
||||
@onClose={{action (mut isModalActive) false}}
|
||||
@isActive={{isModalActive}}
|
||||
@type="warning"
|
||||
@showCloseButton={{true}}
|
||||
>
|
||||
<section class="modal-card-body">
|
||||
<p>
|
||||
If replication is in an adverse state, we can begin recovery. This will attempt to recover to continue syncing.
|
||||
</p>
|
||||
</section>
|
||||
<footer class="modal-card-foot modal-card-foot-outlined">
|
||||
<button
|
||||
class="button is-primary"
|
||||
onclick={{action "onSubmit" "recover"}}
|
||||
data-test-recover-confirm-button
|
||||
>
|
||||
Recover
|
||||
</button>
|
||||
<button
|
||||
class="button is-secondary"
|
||||
onclick={{action (mut isModalActive) false}}
|
||||
data-test-recover-cancel-button>
|
||||
Cancel
|
||||
</button>
|
||||
</footer>
|
||||
</Modal>
|
||||
|
||||
@@ -1,23 +1,54 @@
|
||||
<h4 class="title is-5 is-marginless">
|
||||
<div class="action-block is-rounded" data-test-reindex-replication>
|
||||
<div class="action-block-info">
|
||||
<h4 class="title is-5 is-marginless">
|
||||
Reindex
|
||||
</h4>
|
||||
<div class="content">
|
||||
</h4>
|
||||
<p>
|
||||
Reindex the local data storage. This can cause a very long delay depending
|
||||
on the number and size of objects in the data store.
|
||||
Reindex the local data storage
|
||||
</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<ConfirmAction
|
||||
@buttonClasses="button is-primary"
|
||||
@confirmTitle="Begin reindex?"
|
||||
@confirmMessage="This will initiate reindexing of the local data storage."
|
||||
@confirmButtonText="Reindex"
|
||||
@horizontalPosition="auto-left"
|
||||
@onConfirmAction={{action "onSubmit" "reindex"}}
|
||||
</div>
|
||||
<div class="action-block-action">
|
||||
<button
|
||||
class="button is-secondary"
|
||||
onclick={{action (mut isModalActive) true}}
|
||||
data-test-replication-action-trigger
|
||||
>
|
||||
Reindex
|
||||
</ConfirmAction>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<Modal
|
||||
@title="Begin reindex?"
|
||||
@onClose={{action (mut isModalActive) false}}
|
||||
@isActive={{isModalActive}}
|
||||
@type="warning"
|
||||
@showCloseButton={{true}}
|
||||
>
|
||||
<section class="modal-card-body">
|
||||
<p class="has-bottom-margin-m">
|
||||
Reindexing can cause a very long delay depending on the number and size of objects in the data store.
|
||||
{{if model.replicationAttrs.isPrimary 'You should always re-index your secondary first.'}}
|
||||
</p>
|
||||
<p>
|
||||
Progress will be shown, and you will
|
||||
{{if model.replicationAttrs.isPrimary 'not'}}
|
||||
be able to use Vault during this time.
|
||||
</p>
|
||||
</section>
|
||||
<footer class="modal-card-foot modal-card-foot-outlined">
|
||||
<button
|
||||
class="button is-primary"
|
||||
onclick={{action 'onSubmit' 'reindex'}}
|
||||
data-test-reindex-confirm-button
|
||||
>
|
||||
Reindex
|
||||
</button>
|
||||
<button
|
||||
class="button is-secondary"
|
||||
onclick={{action (mut isModalActive) false}}
|
||||
data-test-reindex-cancel-button
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</footer>
|
||||
</Modal>
|
||||
|
||||
@@ -1,98 +1,47 @@
|
||||
{{#if (and (eq replicationMode 'dr') (eq model.replicationAttrs.modeForUrl 'secondary'))}}
|
||||
<div class="box is-marginless is-shadowless">
|
||||
<p>
|
||||
Change a secondary cluster’s assigned primary cluster using a secondary
|
||||
activation token. This does not wipe all data in the cluster.
|
||||
</p>
|
||||
<div class="field">
|
||||
<label for="dr_operation_token" class="is-label">
|
||||
DR operation token
|
||||
</label>
|
||||
<div class="control">
|
||||
{{input class="input" id="dr_operation_token" name="dr_operation_token" value=dr_operation_token}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="secondary-token" class="is-label">
|
||||
Secondary activation token
|
||||
</label>
|
||||
<div class="control">
|
||||
{{textarea value=token id="secondary-token" name="secondary-token" class="textarea"}}
|
||||
</div>
|
||||
</div>
|
||||
<ToggleButton
|
||||
@class="is-block"
|
||||
@toggleAttr="showOptions"
|
||||
@toggleTarget={{this}}
|
||||
@openLabel="Hide options"
|
||||
@closedLabel="Options"
|
||||
/>
|
||||
{{#if showOptions}}
|
||||
<div class="box is-marginless">
|
||||
<div class="field">
|
||||
<label for="primary_api_addr" class="is-label">
|
||||
Primary API address <em class="is-optional">(optional)</em>
|
||||
</label>
|
||||
<div class="control">
|
||||
{{input class="input" value=primary_api_addr id="primary_api_addr" name="primary_api_addr"}}
|
||||
</div>
|
||||
<p class="help">
|
||||
Set this to the API address (normal Vault address) to override the
|
||||
value embedded in the token.
|
||||
</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="ca_file" class="is-label">
|
||||
CA file <em class="is-optional">(optional)</em>
|
||||
</label>
|
||||
<div class="control">
|
||||
{{input value=ca_file id="ca_file" name="ca_file" class="input"}}
|
||||
</div>
|
||||
<p class="help">
|
||||
Specifies the path to a CA root file (PEM format) that the secondary can use when unwrapping the token from the primary.
|
||||
</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="ca_path" class="is-label">
|
||||
CA path <em class="is-optional">(optional)</em>
|
||||
</label>
|
||||
<div class="control">
|
||||
{{input value=ca_path id="ca_path" name="ca_file" class="input"}}
|
||||
</div>
|
||||
<p class="help">
|
||||
Specifies the path to a CA root directory containing PEM-format files that the secondary can use when unwrapping the token from the primary.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<div class="box is-marginless is-shadowless">
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<ConfirmAction
|
||||
@buttonClasses="button is-primary"
|
||||
@confirmTitle="Update primary?"
|
||||
@confirmMessage="This will update this cluster's primary."
|
||||
@confirmButtonText="Update"
|
||||
@horizontalPosition="auto-left"
|
||||
@onConfirmAction={{action "onSubmit" "update-primary" model.replicationAttrs.modeForUrl (hash dr_operation_token=dr_operation_token token=token primary_api_addr=primary_api_addr ca_path=ca_path ca_file=ca_file)}}
|
||||
>
|
||||
Update primary
|
||||
</ConfirmAction>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="action-block is-rounded" data-test-update-primary-replication>
|
||||
<div class="action-block-info">
|
||||
<h4 class="title is-5 is-marginless">
|
||||
Update primary
|
||||
</h4>
|
||||
<div class="content">
|
||||
<p>
|
||||
Change a secondary cluster’s assigned primary cluster using a secondary
|
||||
activation token. This does not wipe all data in the cluster.
|
||||
Change this secondary's assigned primary cluster
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="action-block-action">
|
||||
<button
|
||||
class="button is-secondary"
|
||||
onclick={{action (mut isModalActive) true}}
|
||||
data-test-update-primary-action-trigger
|
||||
>
|
||||
Update
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
@title="Update primary"
|
||||
@onClose={{action (mut isModalActive) false}}
|
||||
@isActive={{isModalActive}}
|
||||
@type="warning"
|
||||
@showCloseButton={{true}}
|
||||
>
|
||||
<section class="modal-card-body">
|
||||
<p class="has-bottom-margin-m">
|
||||
Use a secondary activation token to change this secondary’s assigned primary. This does not wipe all data in the cluster.
|
||||
</p>
|
||||
|
||||
<div data-test-update-primary-inputs>
|
||||
{{#if (eq replicationMode "dr")}}
|
||||
<div class="field">
|
||||
<label for="dr_operation_token_primary" class="is-label">
|
||||
DR operation token
|
||||
</label>
|
||||
<div class="control">
|
||||
{{input class="input" id="dr_operation_token_primary" name="dr_operation_token_primary" value=dr_operation_token_primary}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
<div class="field">
|
||||
<label for="secondary-token" class="is-label">
|
||||
Secondary activation token
|
||||
@@ -108,7 +57,7 @@
|
||||
<div class="control">
|
||||
{{input class="input" value=primary_api_addr id="primary_api_addr" name="primary_api_addr"}}
|
||||
</div>
|
||||
<p class="help has-text-grey">
|
||||
<p class="help">
|
||||
Set this to the API address (normal Vault address) to override the
|
||||
value embedded in the token.
|
||||
</p>
|
||||
@@ -120,7 +69,7 @@
|
||||
<div class="control">
|
||||
{{input value=ca_file id="ca_file" name="ca_file" class="input"}}
|
||||
</div>
|
||||
<p class="help has-text-grey">
|
||||
<p class="help">
|
||||
Specifies the path to a CA root file (PEM format) that the secondary can use when unwrapping the token from the primary.
|
||||
</p>
|
||||
</div>
|
||||
@@ -131,23 +80,25 @@
|
||||
<div class="control">
|
||||
{{input value=ca_path id="ca_path" name="ca_file" class="input"}}
|
||||
</div>
|
||||
<p class="help has-text-grey">
|
||||
<p class="help">
|
||||
Specifies the path to a CA root directory containing PEM-format files that the secondary can use when unwrapping the token from the primary.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<ConfirmAction
|
||||
@buttonClasses="button is-primary"
|
||||
@confirmTitle="Update primary?"
|
||||
@confirmMessage="This will update this cluster's primary."
|
||||
@confirmButtonText="Update"
|
||||
@horizontalPosition="auto-left"
|
||||
@onConfirmAction={{action "onSubmit" "update-primary" model.replicationAttrs.modeForUrl (hash token=token primary_api_addr=primary_api_addr ca_path=ca_path ca_file=ca_file)}}
|
||||
</div>
|
||||
</section>
|
||||
<footer class="modal-card-foot modal-card-foot-outlined">
|
||||
<button
|
||||
class="button is-primary"
|
||||
onclick={{action "onSubmit" "update-primary" model.replicationAttrs.modeForUrl (hash token=token primary_api_addr=primary_api_addr ca_path=ca_path ca_file=ca_file)}}
|
||||
data-test-confirm-action-trigger
|
||||
>
|
||||
Update primary
|
||||
</ConfirmAction>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
Update
|
||||
</button>
|
||||
<button
|
||||
class="button is-secondary"
|
||||
onclick={{action (mut isModalActive) false}}
|
||||
data-test-update-primary-cancel-button>
|
||||
Cancel
|
||||
</button>
|
||||
</footer>
|
||||
</Modal>
|
||||
|
||||
@@ -2,14 +2,18 @@
|
||||
<LayoutLoading />
|
||||
{{else}}
|
||||
<MessageError @errors={{errors}} />
|
||||
{{#each (if selectedAction (array selectedAction) (replication-action-for-mode replicationMode model.replicationAttrs.modeForUrl)) as |replicationAction index|}}
|
||||
<div class="box is-fullwidth is-marginless {{if (gt index 0) 'is-bottomless' 'is-shadowless'}}">
|
||||
<div class="replication-actions-grid-layout">
|
||||
{{#each
|
||||
(replication-action-for-mode replicationMode model.replicationAttrs.modeForUrl)
|
||||
as |replicationAction|
|
||||
}}
|
||||
<div class="replication-actions-grid-item">
|
||||
{{component (concat 'replication-action-' replicationAction)
|
||||
onSubmit=(action "onSubmit")
|
||||
replicationMode=replicationMode
|
||||
model=model
|
||||
replicationDisplayMode=replicationDisplayMode
|
||||
}}
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
<div class="replication-dashboard box is-sideless is-fullwidth is-marginless" data-test-replication-dashboard>
|
||||
{{#if isReindexing}}
|
||||
<div class="reindexing-alert">
|
||||
<AlertBanner
|
||||
@title={{concat "Re-indexing in progress" reindexingStage}}
|
||||
@type="info"
|
||||
@progressBar={{progressBar}}
|
||||
@message={{reindexMessage}}
|
||||
data-test-isReindexing />
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if isSummaryDashboard}}
|
||||
<div class="summary-state">
|
||||
<h6 class="title is-5 {{unless (get (cluster-states summaryState) "isOk") "has-text-danger"}}" data-test-error>state</h6>
|
||||
{{#unless (get (cluster-states summaryState) "isOk")}}
|
||||
<AlertInline
|
||||
@type="danger"
|
||||
@message="The cluster is {{summaryState}}. Please check your server logs." />
|
||||
{{else}}
|
||||
<p class="has-text-grey is-size-8">The current operating state of both Disaster Recovery and Performance. This value should be ‘running.’ If the value is idle, it indicates an issue and needs to be investigated.</p>
|
||||
{{/unless}}
|
||||
<h2 class="title is-3" data-test-summary-state>{{summaryState}}
|
||||
{{#if (get (cluster-states summaryState) "isOk")}}
|
||||
<Icon @glyph={{get (cluster-states summaryState) "glyph"}} class="{{if (get (cluster-states summaryState) "isOk") "has-text-success" "has-text-danger"}}" data-test-icon />
|
||||
{{/if}}
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
class="selectable-card-container summary"
|
||||
data-test-selectable-card-container-summary>
|
||||
{{yield (hash
|
||||
card=(component componentToRender replicationDetails=replicationDetailsSummary)
|
||||
)}}
|
||||
</div>
|
||||
{{else}}
|
||||
<div
|
||||
class="selectable-card-container {{if isSecondary 'secondary' 'primary'}}"
|
||||
data-test-selectable-card-container={{if isSecondary 'secondary' 'primary'}}>
|
||||
{{yield (hash
|
||||
card=(component componentToRender replicationDetails=replicationDetails)
|
||||
)}}
|
||||
{{#if (not isSecondary)}}
|
||||
{{yield (hash secondaryCard=(component 'known-secondaries-card'))}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if isSyncing}}
|
||||
<div class="syncing-alert">
|
||||
<AlertBanner
|
||||
@title="Syncing in progress"
|
||||
@type="info"
|
||||
@secondIconType="loading"
|
||||
@message="The cluster is syncing. This happens when the secondary is too far behind the primary to use the normal stream-wals state for catching up."
|
||||
data-test-isSyncing/>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#unless isSummaryDashboard}}
|
||||
<ReplicationTableRows
|
||||
@replicationDetails={{replicationDetails}}
|
||||
@clusterMode={{clusterMode}}/>
|
||||
<ReplicationDocLink data-test-replication-doc-link/>
|
||||
{{/unless}}
|
||||
</div>
|
||||
@@ -0,0 +1,6 @@
|
||||
<div class="documentation-link">
|
||||
<p class="has-text-grey">We have additional timeseries telemetry that can be found
|
||||
<LearnLink @path="/vault/operations/monitor-replication#key-monitoring-metrics"> in our documentation</LearnLink>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
@@ -0,0 +1,84 @@
|
||||
{{!-- DR Secondary has a different Nav Header with access only to the Status menu --}}
|
||||
{{#if isSecondary}}
|
||||
<NavHeader as |Nav|>
|
||||
<Nav.home>
|
||||
<HomeLink @class="navbar-item splash-page-logo has-text-white">
|
||||
<LogoEdition />
|
||||
</HomeLink>
|
||||
</Nav.home>
|
||||
<Nav.items>
|
||||
<div class="navbar-item status-indicator-button" data-status="{{if data.unsealed "good" "bad"}}">
|
||||
<StatusMenu @label="Status" @onLinkClick={{action Nav.closeDrawer}} />
|
||||
</div>
|
||||
</Nav.items>
|
||||
</NavHeader>
|
||||
{{/if}}
|
||||
<PageHeader as |p|>
|
||||
<p.top>
|
||||
{{#if (not isSummaryDashboard) }}
|
||||
{{#key-value-header
|
||||
baseKey=baseKey
|
||||
path="vault.cluster.replication-dr-promote"
|
||||
root=backendCrumb
|
||||
}}
|
||||
<li>
|
||||
<span class="sep">
|
||||
/
|
||||
</span>
|
||||
{{#link-to "vault.cluster.replication-dr-promote"}}
|
||||
Disaster Recovery
|
||||
{{/link-to}}
|
||||
</li>
|
||||
{{/key-value-header}}
|
||||
{{/if}}
|
||||
</p.top>
|
||||
<p.levelLeft>
|
||||
<h1 class="title is-3">
|
||||
{{title}}
|
||||
{{#if data.anyReplicationEnabled}}
|
||||
<span class="tag is-light has-text-grey-dark" data-test-mode>
|
||||
{{if isSecondary 'secondary' 'primary'}}
|
||||
</span>
|
||||
<span class="tag is-light has-text-grey-dark" data-test-secondaryId>
|
||||
{{secondaryId}}
|
||||
</span>
|
||||
{{/if}}
|
||||
</h1>
|
||||
</p.levelLeft>
|
||||
</PageHeader>
|
||||
|
||||
{{#if showTabs}}
|
||||
<div class="tabs-container box is-bottomless is-fullwidth is-paddingless" data-test-tabs>
|
||||
<nav class="tabs">
|
||||
{{#if (not isSummaryDashboard )}}
|
||||
<ul>
|
||||
{{#link-to
|
||||
"vault.cluster.replication-dr-promote.details"
|
||||
tagName="li"
|
||||
activeClass="is-active"
|
||||
}}
|
||||
{{#link-to "vault.cluster.replication-dr-promote.details"}}
|
||||
Details
|
||||
{{/link-to}}
|
||||
{{/link-to}}
|
||||
{{#link-to
|
||||
"vault.cluster.replication-dr-promote"
|
||||
tagName="li"
|
||||
activeClass="is-active"
|
||||
current-when="vault.cluster.replication-dr-promote.index"
|
||||
}}
|
||||
{{#link-to "vault.cluster.replication-dr-promote"}}
|
||||
Manage
|
||||
{{/link-to}}
|
||||
{{/link-to}}
|
||||
</ul>
|
||||
{{else}}
|
||||
<ul>
|
||||
<li class="is-active">
|
||||
{{link-to-external "Summary" "replication"}}
|
||||
</li>
|
||||
</ul>
|
||||
{{/if}}
|
||||
</nav>
|
||||
</div>
|
||||
{{/if}}
|
||||
@@ -1,4 +1,5 @@
|
||||
{{#if (eq display 'menu')}}
|
||||
{{#if isMenu}}
|
||||
{{!-- this is the status menu --}}
|
||||
<div class="level is-mobile">
|
||||
<div class="is-flex-1">
|
||||
{{#if replicationUnsupported}}
|
||||
@@ -31,11 +32,11 @@
|
||||
</div>
|
||||
<div class="level-right">
|
||||
{{#if replicationEnabled}}
|
||||
{{#if (get cluster (concat mode 'StateGlyph'))}}
|
||||
<span class="has-text-success">
|
||||
{{#if (cluster-states modeState)}}
|
||||
<span class="{{if (get (cluster-states modeState) "isOk") "has-text-success" "has-text-danger"}}">
|
||||
<Icon
|
||||
aria-hidden="true"
|
||||
@glyph={{get cluster (concat mode 'StateGlyph')}}
|
||||
@glyph={{get (cluster-states modeState) "glyph"}}
|
||||
/>
|
||||
</span>
|
||||
{{else if syncProgress}}
|
||||
@@ -53,9 +54,11 @@
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="level-left is-flex-1">
|
||||
{{!-- this is the replication index page --}}
|
||||
<div class="level">
|
||||
<div class="replication-description level-left">
|
||||
<div>
|
||||
{{#if (and (eq mode 'performance') (not (has-feature "Performance Replication")))}}
|
||||
{{#if (and (eq mode 'performance') (not (has-feature 'Performance Replication')))}}
|
||||
<p>
|
||||
Performance Replication is a feature of Vault Enterprise Premium.
|
||||
</p>
|
||||
@@ -70,44 +73,42 @@
|
||||
</a>
|
||||
</p>
|
||||
{{else if replicationEnabled}}
|
||||
<h5 class="title is-7 is-uppercase is-marginless">
|
||||
<h6 class="title is-6 is-uppercase">
|
||||
Enabled
|
||||
</h5>
|
||||
</h6>
|
||||
<div class="detail-tags">
|
||||
<span class="has-text-grey">
|
||||
{{capitalize modeForUrl}}
|
||||
</span>
|
||||
{{#if secondaryId}}
|
||||
<span class="tag">
|
||||
<span class="tag is-light has-text-grey-dark">
|
||||
<code>
|
||||
{{secondaryId}}
|
||||
</code>
|
||||
</span>
|
||||
{{/if}}
|
||||
<span class="tag">
|
||||
<span class="tag is-light has-text-grey-dark">
|
||||
<code>
|
||||
{{clusterIdDisplay}}
|
||||
</code>
|
||||
</span>
|
||||
{{else}}
|
||||
</div>
|
||||
{{/if}}
|
||||
<p class="help has-text-grey-dark">
|
||||
{{#if (eq mode 'dr')}}
|
||||
DR is designed to protect against catastrophic failure of entire clusters. Secondaries do not forward service requests (until they are elected and become a new primary).
|
||||
{{else}}
|
||||
Performance Replication scales workloads horizontally across clusters to make requests faster. Local secondaries handle read requests but forward writes to the primary to be handled.
|
||||
{{/if}}
|
||||
{{replication-mode-description mode}}
|
||||
</p>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-right">
|
||||
</div>
|
||||
<div class="level-right">
|
||||
{{#if replicationDisabled}}
|
||||
{{#link-to "vault.cluster.replication.mode.index" cluster.name mode class="button is-primary"}}
|
||||
{{#link-to 'mode.index' cluster.name mode class='button is-primary'}}
|
||||
Enable
|
||||
{{/link-to}}
|
||||
{{else if (eq mode 'dr')}}
|
||||
{{cluster.drReplicationStateDisplay}}
|
||||
{{else if (eq mode 'performance')}}
|
||||
{{cluster.perfReplicationStateDisplay}}
|
||||
{{else}}
|
||||
{{#link-to 'mode.index' cluster.name mode class="button is-secondary"}}
|
||||
Details
|
||||
{{/link-to}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
29
ui/lib/core/addon/templates/components/replication-page.hbs
Normal file
29
ui/lib/core/addon/templates/components/replication-page.hbs
Normal file
@@ -0,0 +1,29 @@
|
||||
<div class="replication-page" data-test-replication-page>
|
||||
{{#if isLoadingData }}
|
||||
<LayoutLoading />
|
||||
{{else}}
|
||||
{{yield
|
||||
(hash
|
||||
header=(component 'replication-header'
|
||||
data=model
|
||||
title=formattedReplicationMode
|
||||
isSecondary=isSecondary
|
||||
secondaryId=replicationDetails.secondaryId
|
||||
isSummaryDashboard=isSummaryDashboard
|
||||
)
|
||||
dashboard=(component
|
||||
'replication-dashboard'
|
||||
data=model
|
||||
isSecondary=isSecondary
|
||||
isSummaryDashboard=isSummaryDashboard
|
||||
replicationDetailsSummary=replicationDetailsSummary
|
||||
replicationDetails=replicationDetails
|
||||
clusterMode=clusterMode
|
||||
reindexingDetails=reindexingDetails
|
||||
)
|
||||
isDisabled=isDisabled
|
||||
message=message
|
||||
)
|
||||
}}
|
||||
{{/if}}
|
||||
</div>
|
||||
@@ -0,0 +1,110 @@
|
||||
<div
|
||||
class="selectable-card is-rounded card-container {{if hasErrorClass 'has-border-danger'}}"
|
||||
data-test-replication-secondary-card
|
||||
>
|
||||
{{! Check if Status or Primary Cluster Card }}
|
||||
{{#if (eq title 'Status')}}
|
||||
<h3 class="title is-5 grid-item-top-left card-title">
|
||||
{{title}}
|
||||
</h3>
|
||||
<div class="grid-item-left">
|
||||
{{#if (get (cluster-states state) 'isOk')}}
|
||||
<h6 class="title is-6">
|
||||
state
|
||||
</h6>
|
||||
<p class="has-text-grey is-size-8">
|
||||
How this cluster is communicating with others at this moment.
|
||||
</p>
|
||||
{{else}}
|
||||
<h6 class="has-text-danger" data-test-error>
|
||||
state
|
||||
</h6>
|
||||
<AlertInline @type="danger" @message="Please check your server logs." />
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="grid-item-right">
|
||||
{{#if (eq connection 'transient_failure')}}
|
||||
<h6 class="title is-6 has-text-danger" data-test-error>
|
||||
connection_state
|
||||
</h6>
|
||||
<AlertInline
|
||||
@type="danger"
|
||||
@message="There has been some transient failure. Your cluster will eventually switch back to connection and try to establish a connection again."
|
||||
/>
|
||||
{{else if (eq connection 'shutdown')}}
|
||||
<h6 class="title is-6 has-text-danger" data-test-error>
|
||||
connection_state
|
||||
</h6>
|
||||
<AlertInline
|
||||
@type="danger"
|
||||
@message="Your connection has shut down. This may be because the application explicitly requested a shutdown or a non-recoverable error has happened during attempts to communicate."
|
||||
/>
|
||||
{{else}}
|
||||
<h6 class="title is-6">
|
||||
connection_state
|
||||
</h6>
|
||||
<p class="has-text-grey is-size-8">
|
||||
The health of the connection between this cluster and others.
|
||||
</p>
|
||||
{{/if}}
|
||||
</div>
|
||||
<h2 class="title is-3 grid-item-bottom-left" data-test-state>
|
||||
{{state}}
|
||||
{{#if inSyncState}}
|
||||
<ToolTip @verticalPosition="above" as |T|>
|
||||
<T.trigger>
|
||||
<Icon @glyph={{'check-circle-outline'}} @size="m" class={{'has-text-success'}} data-test-glyph />
|
||||
</T.trigger>
|
||||
<T.content @class="tool-tip">
|
||||
<div class="box">
|
||||
Everything is in sync
|
||||
</div>
|
||||
</T.content>
|
||||
</ToolTip>
|
||||
{{/if}}
|
||||
</h2>
|
||||
<h2 class="title is-3 grid-item-bottom-right" data-test-connection>
|
||||
{{connection}}
|
||||
</h2>
|
||||
<div class="grid-item-bottom-row">
|
||||
<h6 class="title is-6">
|
||||
last_remote_wal
|
||||
</h6>
|
||||
<p class="has-text-grey is-size-8">
|
||||
The last WAL index that the secondary received from the primary. Updates every 10 seconds.
|
||||
</p>
|
||||
<h2 class="title is-3">
|
||||
{{format-number lastRemoteWAL}}
|
||||
</h2>
|
||||
</div>
|
||||
{{else}}
|
||||
<h3 class="title is-5 grid-item-top-left card-title">
|
||||
{{title}}
|
||||
</h3>
|
||||
<div class="grid-item-second-row">
|
||||
<h6 class="title is-6">
|
||||
known_primary_cluster_addr
|
||||
</h6>
|
||||
<p class="has-text-grey">
|
||||
A list of all the nodes in the primary's cluster. This value is updated every ten seconds.
|
||||
</p>
|
||||
</div>
|
||||
<div class="grid-item-third-row">
|
||||
{{#if (is-empty knownPrimaryClusterAddrs)}}
|
||||
<EmptyState
|
||||
@title="No known_primary_cluster_addrs"
|
||||
@message="These addresses are used by the secondary to communicate with the primary cluster. Should always be non-zero in a functioning replication setup.">
|
||||
<LearnLink @path="/vault/operations/monitor-replication">
|
||||
Learn more
|
||||
</LearnLink>
|
||||
</EmptyState>
|
||||
{{else}}
|
||||
<InfoTable
|
||||
@title="Known Primary Cluster Addrs"
|
||||
@header="cluster_addr"
|
||||
@items={{knownPrimaryClusterAddrs}}
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
@@ -0,0 +1,48 @@
|
||||
<div class="selectable-card is-rounded card-container summary {{if hasErrorClass "has-border-danger"}}" data-test-replication-summary-card>
|
||||
{{!-- Check if DR or Performance Card --}}
|
||||
{{#if (eq title "Disaster Recovery")}}
|
||||
<h3 class="title is-5 grid-item-top-left card-title">{{title}}</h3>
|
||||
<div class="grid-item-top-right">
|
||||
<ToolbarLink @params={{array 'mode.index' 'dr'}} data-test-manage-link={{title}}>
|
||||
Details
|
||||
</ToolbarLink>
|
||||
</div>
|
||||
<div class="grid-item-left">
|
||||
<h6 class="title is-6">last_dr_wal</h6>
|
||||
<p class="helper-text is-label has-text-grey">Index of last WAL entry written on local storage. Updates every ten seconds.</p>
|
||||
</div>
|
||||
<div class="grid-item-right">
|
||||
<h6 class="title is-6">known_secondaries</h6>
|
||||
<p class="helper-text is-label has-text-grey">Number of secondaries connected to this primary.</p>
|
||||
</div>
|
||||
<h2 class="title is-3 grid-item-bottom-left" data-test-lastWAL>{{format-number lastDrWAL}}</h2>
|
||||
<h2 class="title is-3 grid-item-bottom-right" data-test-known-secondaries>{{format-number knownSecondariesDr}}</h2>
|
||||
<div class="grid-item-bottom-row">
|
||||
<h6 class="title is-6">merkle_root</h6>
|
||||
<p class="helper-text is-label has-text-grey">A snapshot of the merkle tree's root hash.</p>
|
||||
<div><code class="is-word-break has-text-black">{{merkleRootDr}}</code></div>
|
||||
</div>
|
||||
{{else}}
|
||||
<h3 class="title is-5 grid-item-top-left card-title">{{title}}</h3>
|
||||
<div class="grid-item-top-right">
|
||||
<ToolbarLink @params={{array 'mode.index' 'performance'}} data-test-manage-link={{title}}>
|
||||
Details
|
||||
</ToolbarLink>
|
||||
</div>
|
||||
<div class="grid-item-left">
|
||||
<h6 class="title is-6">last_performance_wal</h6>
|
||||
<p class="helper-text is-label has-text-grey">Index of last WAL entry written on local storage. Updates every ten seconds.</p>
|
||||
</div>
|
||||
<div class="grid-item-right">
|
||||
<h6 class="title is-6">known_secondaries</h6>
|
||||
<p class="helper-text is-label has-text-grey">Number of secondaries connected to this primary.</p>
|
||||
</div>
|
||||
<h2 class="title is-3 grid-item-bottom-left" data-test-lastWAL>{{format-number lastPerformanceWAL}}</h2>
|
||||
<h2 class="title is-3 grid-item-bottom-right" data-test-known-secondaries>{{format-number knownSecondariesPerformance}}</h2>
|
||||
<div class="grid-item-bottom-row">
|
||||
<h6 class="title is-6">merkle_root</h6>
|
||||
<p class="helper-text is-label has-text-grey">A snapshot of the merkle tree's root hash.</p>
|
||||
<div><code class="is-word-break has-text-black">{{merkleRootPerformance}}</code></div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
@@ -0,0 +1,26 @@
|
||||
<div class="has-top-margin-xl has-bottom-margin-s"
|
||||
data-test-table-rows>
|
||||
{{#if (eq clusterMode 'secondary')}}
|
||||
{{info-table-row
|
||||
label='secondary_id'
|
||||
helperText="The ID of the secondary activation token used to enable replication."
|
||||
value=secondaryId}}
|
||||
{{else}}
|
||||
{{info-table-row
|
||||
label='primary_cluster_addr'
|
||||
helperText='The configuration of the cluster. This was set when replication was enabled.'
|
||||
value=primaryClusterAddr
|
||||
}}
|
||||
{{/if}}
|
||||
{{info-table-row
|
||||
label="Merkle root index"
|
||||
helperText="A snapshot in time of the merkle tree's root hash. Changes on every update to storage."
|
||||
value=merkleRoot}}
|
||||
{{info-table-row
|
||||
label="Mode"
|
||||
value=clusterMode}}
|
||||
{{info-table-row
|
||||
label="Replication set"
|
||||
helperText="The ID for the group of clusters communicating for this replication mode"
|
||||
value=clusterId}}
|
||||
</div>
|
||||
1
ui/lib/core/app/components/confirmation-modal.js
Normal file
1
ui/lib/core/app/components/confirmation-modal.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from 'core/components/confirmation-modal';
|
||||
1
ui/lib/core/app/components/info-table.js
Normal file
1
ui/lib/core/app/components/info-table.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from 'core/components/info-table';
|
||||
1
ui/lib/core/app/components/key-value-header.js
Normal file
1
ui/lib/core/app/components/key-value-header.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from 'core/components/key-value-header';
|
||||
1
ui/lib/core/app/components/learn-link.js
Normal file
1
ui/lib/core/app/components/learn-link.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from 'core/components/learn-link';
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user