Sidebar Navigation (#19296)

* Add Helios Design System Components (#19278)

* adds hds dependency

* updates reset import path

* sets minifyCSS advanced option to false

* Remove node-sass (#19376)

* removes node-sass and fixes sass compilation

* fixes active tab li class

* Sidebar Navigation Components (#19446)

* links ember-shared-components addon and imports styles

* adds sidebar frame and nav components

* updates HcNav component name to HcAppFrame and adds sidebar UserMenu component

* adds tests for sidebar components

* fixes tests

* updates user menu styling

* fixes typos in nav cluster component

* changes padding value in sidebar stylesheet to use variable

* Replace and remove old nav components with new ones (#19447)

* links ember-shared-components addon and imports styles

* adds sidebar frame and nav components

* updates activeCluster on auth service and adds activeSession prop for sidebar visibility

* replaces old nav components with new ones in templates

* fixes sidebar visibility issue and updates user menu label class

* removes NavHeader usage

* adds clients index route to redirect to dashboard

* removes unused HcAppFrame footer block and reduces page header top margin

* Nav component cleanup (#19681)

* removes nav-header components

* removes navbar styling

* removes status-menu component and styles

* removes cluster and auth info components

* removes menu-sidebar component and styling

* fixes tests

* Console Panel Updates (#19741)

* updates console panel styling

* adds test for opening and closing the console panel

* updates console panel background color to use hds token

* adds right margin to console panel input

* updates link-status banner styling

* updates hc nav components to new API

* Namespace Picker Updates (#19753)

* updates namespace-picker

* updates namespace picker menu styling

* adds bottom margin to env banner

* updates class order on namespace picker link

* restores manage namespaces refresh icon

* removes manage namespaces nav icon

* removes home link component (#20027)

* Auth and Error View Updates (#19749)

* adds vault logo to auth page

* updates top level error template

* updates loading substate handling and moves policies link from access to cluster nav (#20033)

* moves console panel to bottom of viewport (#20183)

* HDS Sidebar Nav Components (#20197)

* updates nav components to hds

* upgrades project yarn version to 3.5

* fixes issues in app frame component

* updates sidenav actions to use icon button component

* Sidebar navigation acceptance tests (#20270)

* adds sidebar navigation acceptance tests and fixes other test failures

* console panel styling tweaks

* bumps addon version

* remove and ignore yarn install-state file

* fixes auth service and console tests

* moves classes from deleted files after bulma merge

* fixes sass syntax errors blocking build

* cleans up dart sass deprecation warnings

* adds changelog entry

* hides namespace picker when sidebar nav panel is minimized

* style tweaks

* fixes sidebar nav tests

* bumps hds addon to latest version and removes style override

* updates modify-passthrough-response helper

* updates sidebar nav tests

* mfa-setup test fix attempt

* fixes cluster mfa setup test

* remove deprecated yarn ignore-optional flag from makefile

* removes another instance of yarn ignore-optional and updates ui readme

* removes unsupported yarn verbose flag from ci-helper

* hides nav headings when user does not have access to any sub links

* removes unused optional deps and moves lint-staged to dev deps

* updates has-permission helper and permissions service tests

* fixes issue with console panel not filling container width
This commit is contained in:
Jordan Reimer 2023-05-02 19:36:15 -06:00 committed by GitHub
parent 00e43b88b4
commit c84d267c61
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
140 changed files with 27643 additions and 343889 deletions

View File

@ -179,7 +179,7 @@ static-assets-dir:
install-ui-dependencies:
@echo "--> Installing JavaScript assets"
@cd ui && yarn --ignore-optional
@cd ui && yarn
test-ember: install-ui-dependencies
@echo "--> Running ember tests"

3
changelog/19296.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:feature
**Sidebar Navigation in UI**: A new sidebar navigation panel has been added in the UI to replace the top navigation bar.
```

View File

@ -132,9 +132,9 @@ function build_ui() {
mkdir -p http/web_ui
popd
pushd "$repo_root/ui"
yarn install --ignore-optional
yarn install
npm rebuild node-sass
yarn --verbose run build
yarn run build
popd
}

9
ui/.gitignore vendored
View File

@ -29,3 +29,12 @@ package-lock.json
# broccoli-debug
/DEBUG/
# yarn
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

873
ui/.yarn/releases/yarn-3.5.0.cjs vendored Executable file

File diff suppressed because one or more lines are too long

3
ui/.yarnrc.yml Normal file
View File

@ -0,0 +1,3 @@
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-3.5.0.cjs

View File

@ -39,12 +39,6 @@ You will need the following things properly installed on your computer.
* [Yarn](https://yarnpkg.com/)
* [Ember CLI](https://cli.emberjs.com/release/)
* [Google Chrome](https://google.com/chrome/)
- [lint-staged\*](https://www.npmjs.com/package/lint-staged)
\* lint-staged is an optional dependency - running `yarn` will install it.
If don't want optional dependencies installed you can run `yarn --ignore-optional`. If you've ignored the optional deps
previously and want to install them, you have to tell yarn to refetch all deps by
running `yarn --force`.
In order to enforce the same version of `yarn` across installs, the `yarn` binary is included in the repo
in the `.yarn/releases` folder. To update to a different version of `yarn`, use the `yarn policies set-version VERSION` command. For more information on this, see the [documentation](https://yarnpkg.com/en/docs/cli/policies).

View File

@ -42,7 +42,7 @@ export default ApplicationAdapter.extend({
}
} catch (error) {
// no path means this was an error on listing
if (!query.path) {
if (!query.path || !mountModel) {
throw error;
}
// control groups will throw a 403 permission denied error. If this happens return the mountModel

View File

@ -22,7 +22,7 @@ export default ApplicationAdapter.extend({
// concerns and we only want to send "list" to the server
query(store, type, query) {
let { backend, id } = query;
return this.ajax(this._url(backend, id), 'GET', { data: { list: true } }).then(resp => {
return this.ajax(this._url(backend, id), 'GET', { data: { list: true } }).then((resp) => {
resp.id = id;
resp.backend = backend;
return resp;
@ -36,7 +36,7 @@ export default ApplicationAdapter.extend({
queryRecord(store, type, query) {
let { backend, id } = query;
return this.ajax(this._url(backend, id), 'GET').then(resp => {
return this.ajax(this._url(backend, id), 'GET').then((resp) => {
resp.id = id;
resp.backend = backend;
return resp;

View File

@ -1,32 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import { inject as service } from '@ember/service';
import Component from '@glimmer/component';
/**
* @module ClusterInfo
*
* @example
* ```js
* <ClusterInfo @cluster={{cluster}} @onLinkClick={{action}} />
* ```
*
* @param {object} cluster - details of the current cluster, passed from the parent.
* @param {Function} onLinkClick - parent action which determines the behavior on link click
*/
export default class ClusterInfoComponent extends Component {
@service auth;
@service store;
@service version;
get activeCluster() {
return this.store.peekRecord('cluster', this.auth.activeCluster);
}
transitionToRoute() {
this.router.transitionTo(...arguments);
}
}

View File

@ -1,33 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import Component from '@glimmer/component';
/**
* @module HomeLink
* `HomeLink` is a span that contains either the text `home` or the `LogoEdition` component.
*
* @example
* ```js
* <HomeLink @class="navbar-item splash-page-logo">
* <LogoEdition />
* </HomeLink>
* ```
* @param {string} class - Classes attached to the the component.
* @param {string} text - Text displayed instead of logo.
*
* @see {@link https://github.com/hashicorp/vault/search?l=Handlebars&q=HomeLink|Uses of HomeLink}
* @see {@link https://github.com/hashicorp/vault/blob/main/ui/app/components/home-link.js|HomeLink Source Code}
*/
export default class HomeLink extends Component {
get text() {
return 'home';
}
get computedClasses() {
return this.classNames.join(' ');
}
}

View File

@ -1,20 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import Component from '@ember/component';
export default Component.extend({
classNames: ['column', 'is-sidebar'],
classNameBindings: ['isActive:is-active'],
isActive: false,
actions: {
openMenu() {
this.set('isActive', true);
},
closeMenu() {
this.set('isActive', false);
},
},
});

View File

@ -155,7 +155,9 @@ export default Component.extend({
namespaceDisplay: computed('namespacePath', 'accessibleNamespaces', 'accessibleNamespaces.[]', function () {
const namespace = this.namespacePath;
if (!namespace) return '';
if (!namespace) {
return 'root';
}
const parts = namespace?.split('/');
return parts[parts.length - 1];
}),

View File

@ -1,34 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import Component from '@ember/component';
import { inject as service } from '@ember/service';
import { computed } from '@ember/object';
export default Component.extend({
router: service(),
currentCluster: service(),
'data-test-navheader': true,
attributeBindings: ['data-test-navheader'],
classNameBindings: 'consoleFullscreen:panel-fullscreen',
tagName: 'header',
navDrawerOpen: false,
consoleFullscreen: false,
hideLinks: computed('router.currentRouteName', function () {
const currentRoute = this.router.currentRouteName;
if ('vault.cluster.oidc-provider' === currentRoute) {
return true;
}
return false;
}),
actions: {
toggleNavDrawer(isOpen) {
if (isOpen !== undefined) {
return this.set('navDrawerOpen', isOpen);
}
this.toggleProperty('navDrawerOpen');
},
},
});

View File

@ -1,10 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import Component from '@ember/component';
export default Component.extend({
tagName: '',
});

View File

@ -1,10 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import Component from '@ember/component';
export default Component.extend({
tagName: '',
});

View File

@ -1,10 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import Component from '@ember/component';
export default Component.extend({
tagName: '',
});

View File

@ -0,0 +1,51 @@
<Hds::AppFrame @hasSidebar={{@showSidebar}} @hasHeader={{false}} @hasFooter={{false}} as |Frame|>
<Frame.Sidebar data-test-sidebar-nav>
<Hds::SideNav @isResponsive={{true}} @hasA11yRefocus={{true}} @a11yRefocusSkipTo="app-main-content">
<:header>
<Hds::SideNav::Header>
<:logo>
<Hds::SideNav::Header::HomeLink
@icon="vault"
@route="vault.cluster"
@model={{this.currentCluster.cluster.name}}
@ariaLabel="home link"
data-test-sidebar-logo
/>
</:logo>
<:actions>
<Hds::SideNav::Header::IconButton
@icon="terminal-screen"
@ariaLabel="Console toggle"
data-test-console-toggle
{{on "click" (fn (mut this.console.isOpen) (not this.console.isOpen))}}
/>
<Sidebar::UserMenu />
</:actions>
</Hds::SideNav::Header>
</:header>
{{! this block is where the Hds::SideNav::Portal components render into }}
<:body>
<Hds::SideNav::Portal::Target aria-label="sidebar navigation links" />
</:body>
<:footer>
{{#if (has-feature "Namespaces")}}
<NamespacePicker
@namespace={{this.clusterController.namespaceQueryParam}}
class="hds-side-nav-hide-when-minimized"
/>
{{/if}}
</:footer>
</Hds::SideNav>
</Frame.Sidebar>
<Frame.Main id="app-main-content">
{{! outlet for app content }}
<div id="modal-wormhole"></div>
<LinkStatus @status={{this.currentCluster.cluster.hcpLinkStatus}} />
{{yield}}
<div data-test-console-panel class={{if this.console.isOpen "panel-open"}}>
<Console::UiPanel @isFullscreen={{this.consoleFullscreen}} />
</div>
</Frame.Main>
</Hds::AppFrame>

View File

@ -0,0 +1,9 @@
import Component from '@glimmer/component';
import { inject as service } from '@ember/service';
import { inject as controller } from '@ember/controller';
export default class SidebarNavComponent extends Component {
@service currentCluster;
@service console;
@controller('vault.cluster') clusterController;
}

View File

@ -0,0 +1,65 @@
<Hds::SideNav::Portal @ariaLabel="Access Navigation Links" data-test-sidebar-nav-panel="Access" as |Nav|>
<Nav.BackLink
@route="vault.cluster"
@current-when={{false}}
@icon="arrow-left"
@text="Back to main navigation"
data-test-sidebar-nav-link="Back to main navigation"
/>
{{#if (has-permission "access" routeParams=(array "methods" "mfa" "oidc"))}}
<Nav.Title data-test-sidebar-nav-heading="Authentication">Authentication</Nav.Title>
{{/if}}
{{#if (has-permission "access" routeParams="methods")}}
<Nav.Link
@route="vault.cluster.access.methods"
@current-when="vault.cluster.access.methods vault.cluster.access.method"
@text="Authentication methods"
data-test-sidebar-nav-link="Authentication methods"
/>
{{/if}}
{{#if (has-permission "access" routeParams="mfa")}}
<Nav.Link
@route="vault.cluster.access.mfa.methods"
@current-when="vault.cluster.access.mfa.methods vault.cluster.access.mfa.enforcements vault.cluster.access.mfa.index"
@text="Multi-factor authentication"
data-test-sidebar-nav-link="Multi-factor authentication"
/>
{{/if}}
{{#if (has-permission "access" routeParams="oidc")}}
<Nav.Link @route="vault.cluster.access.oidc" @text="OIDC" data-test-sidebar-nav-link="OIDC" />
{{/if}}
{{#if (and (has-feature "Control Groups") (has-permission "access" routeParams="control-groups"))}}
<Nav.Title data-test-sidebar-nav-heading="Access Control">Access Control</Nav.Title>
<Nav.Link
@route="vault.cluster.access.control-groups"
@current-when="vault.cluster.access.control-groups vault.cluster.access.control-group-accessor vault.cluster.access.control-groups-configure"
@text="Control Groups"
data-test-sidebar-nav-link="Control Groups"
/>
{{/if}}
{{#if (has-permission "access" routeParams=(array "namespaces" "groups" "entities"))}}
<Nav.Title data-test-sidebar-nav-heading="Organization">Organization</Nav.Title>
{{/if}}
{{#if (and (has-feature "Namespaces") (has-permission "access" routeParams="namespaces"))}}
<Nav.Link @route="vault.cluster.access.namespaces" @text="Namespaces" data-test-sidebar-nav-link="Namespaces" />
{{/if}}
{{#if (has-permission "access" routeParams="groups")}}
<Nav.Link @route="vault.cluster.access.identity" @model="groups" @text="Groups" data-test-sidebar-nav-link="Groups" />
{{/if}}
{{#if (has-permission "access" routeParams="entities")}}
<Nav.Link
@route="vault.cluster.access.identity"
@model="entities"
@text="Entities"
data-test-sidebar-nav-link="Entities"
/>
{{/if}}
{{#if (has-permission "access" routeParams="leases")}}
<Nav.Title data-test-sidebar-nav-heading="Administration">Administration</Nav.Title>
<Nav.Link @route="vault.cluster.access.leases" @text="Leases" data-test-sidebar-nav-link="Leases" />
{{/if}}
</Hds::SideNav::Portal>

View File

@ -0,0 +1,98 @@
<Hds::SideNav::Portal @ariaLabel="Cluster Navigation Links" data-test-sidebar-nav-panel="Cluster" as |Nav|>
<Nav.Title data-test-sidebar-nav-heading="Vault">Vault</Nav.Title>
<Nav.Link
@route="vault.cluster.secrets"
@current-when="vault.cluster.secrets vault.cluster.settings.mount-secret-backend vault.cluster.settings.configure-secret-backend"
@text="Secrets engines"
data-test-sidebar-nav-link="Secrets engines"
/>
{{#if (has-permission "access")}}
<Nav.Link
@route={{get (route-params-for "access") "route"}}
@models={{get (route-params-for "access") "models"}}
@current-when="vault.cluster.access vault.cluster.settings.auth"
@text="Access"
@hasSubItems={{true}}
data-test-sidebar-nav-link="Access"
/>
{{/if}}
{{#if (has-permission "policies")}}
<Nav.Link
@route="vault.cluster.policies"
@models={{get (route-params-for "policies") "models"}}
@text="Policies"
@hasSubItems={{true}}
data-test-sidebar-nav-link="Policies"
/>
{{/if}}
{{#if (has-permission "tools")}}
<Nav.Link
@route="vault.cluster.tools.tool"
@models={{get (route-params-for "tools") "models"}}
@text="Tools"
@hasSubItems={{true}}
data-test-sidebar-nav-link="Tools"
/>
{{/if}}
{{#if
(and this.version.isEnterprise this.cluster.anyReplicationEnabled (has-permission "status" routeParams="replication"))
}}
<Nav.Title data-test-sidebar-nav-heading="Replication">Replication</Nav.Title>
<Nav.Link
@route="vault.cluster.replication.mode.index"
@model="dr"
@text="DR Primary"
data-test-sidebar-nav-link="DR Primary"
/>
{{#if (has-feature "Performance Replication")}}
<Nav.Link
@route="vault.cluster.replication.mode.index"
@model="performance"
@text="Performance Secondary"
data-test-sidebar-nav-link="Performance Secondary"
/>
{{/if}}
{{/if}}
{{#if
(or
(has-permission "status" routeParams=(array "replication" "raft" "license" "seal"))
(has-permission "clients" routeParams="activity")
)
}}
<Nav.Title data-test-sidebar-nav-heading="Monitoring">Monitoring</Nav.Title>
{{/if}}
{{#if (and this.version.isEnterprise (has-permission "status" routeParams="replication"))}}
<Nav.Link @route="vault.cluster.replication.index" @text="Replication" data-test-sidebar-nav-link="Replication" />
{{/if}}
{{#if (and this.cluster.usingRaft (has-permission "status" routeParams="raft"))}}
<Nav.Link
@route="vault.cluster.storage"
@model={{this.cluster.name}}
@text="Raft Storage"
data-test-sidebar-nav-link="Raft Storage"
/>
{{/if}}
{{#if (and (has-permission "clients" routeParams="activity") (not this.cluster.dr.isSecondary))}}
<Nav.Link @route="vault.cluster.clients" @text="Client count" data-test-sidebar-nav-link="Client count" />
{{/if}}
{{#if (and this.version.features (has-permission "status" routeParams="license") (not this.cluster.dr.isSecondary))}}
<Nav.Link
@route="vault.cluster.license"
@model={{this.cluster.name}}
@text="License"
data-test-sidebar-nav-link="License"
/>
{{/if}}
{{#if (and (has-permission "status" routeParams="seal") (not this.cluster.dr.isSecondary))}}
<Nav.Link
@route="vault.cluster.settings.seal"
@model={{this.cluster.name}}
@text="Seal Vault"
data-test-sidebar-nav-link="Seal Vault"
/>
{{/if}}
</Hds::SideNav::Portal>

View File

@ -0,0 +1,12 @@
import Component from '@glimmer/component';
import { inject as service } from '@ember/service';
export default class SidebarNavClusterComponent extends Component {
@service currentCluster;
@service version;
@service auth;
get cluster() {
return this.currentCluster.cluster;
}
}

View File

@ -0,0 +1,39 @@
<Hds::SideNav::Portal @ariaLabel="Policies Navigation Links" data-test-sidebar-nav-panel="Policies" as |Nav|>
<Nav.BackLink
@route="vault.cluster"
@current-when={{false}}
@icon="arrow-left"
@text="Back to main navigation"
data-test-sidebar-nav-link="Back to main navigation"
/>
<Nav.Title data-test-sidebar-nav-heading="Policies">Policies</Nav.Title>
{{#if (has-permission "policies" routeParams="acl")}}
<Nav.Link
@route="vault.cluster.policies"
@model="acl"
@current-when="vault.cluster.policies vault.cluster.policy"
@text="ACL Policies"
data-test-sidebar-nav-link="ACL Policies"
/>
{{/if}}
{{#if (and (has-feature "Sentinel") (has-permission "policies" routeParams="rgp"))}}
<Nav.Link
@route="vault.cluster.policies"
@model="rgp"
@current-when="vault.cluster.policies vault.cluster.policy"
@text="Role-Governing Policies"
data-test-sidebar-nav-link="Role-Governing Policies"
/>
{{/if}}
{{#if (and (has-feature "Sentinel") (has-permission "policies" routeParams="egp"))}}
<Nav.Link
@route="vault.cluster.policies"
@model="egp"
@current-when="vault.cluster.policies vault.cluster.policy"
@text="Endpoint Governing Policies"
data-test-sidebar-nav-link="Endpoint Governing Policies"
/>
{{/if}}
</Hds::SideNav::Portal>

View File

@ -0,0 +1,22 @@
<Hds::SideNav::Portal @ariaLabel="Tools Navigation Links" data-test-sidebar-nav-panel="Tools" as |Nav|>
<Nav.BackLink
@route="vault.cluster"
@current-when={{false}}
@icon="arrow-left"
@text="Back to main navigation"
data-test-sidebar-nav-link="Back to main navigation"
/>
<Nav.Title data-test-sidebar-nav-heading="Tools">Tools</Nav.Title>
{{#each (tools-actions) as |supportedAction|}}
{{#if (has-permission "tools" routeParams=supportedAction)}}
<Nav.Link
@route="vault.cluster.tools.tool"
@model={{supportedAction}}
@text={{capitalize supportedAction}}
data-test-sidebar-nav-link={{capitalize supportedAction}}
/>
{{/if}}
{{/each}}
</Hds::SideNav::Portal>

View File

@ -0,0 +1,83 @@
<BasicDropdown
@horizontalPosition="right"
@verticalPosition="below"
@renderInPlace={{true}}
class="sidebar-user-menu"
data-test-user-menu
as |Dropdown|
>
<Dropdown.Trigger data-test-user-menu-trigger>
<Hds::SideNav::Header::IconButton @icon="user" @ariaLabel="User menu" />
</Dropdown.Trigger>
<Dropdown.Content>
<Confirm as |c|>
<div class="popup-menu-content" data-test-user-menu-content>
<div class="box">
<div class="menu-label">
{{capitalize this.auth.authData.displayName}}
</div>
<nav class="menu">
<ul class="menu-list">
{{#if this.auth.allowExpiration}}
<li class="token-alert is-flex" data-test-user-menu-item="token alert">
<span><Icon @name="alert-triangle-fill" class="has-text-highlight" /></span>
<span class="is-size-8 has-text-semibold">
We've stopped auto-renewing your token due to inactivity. It will expire on
{{date-format this.auth.tokenExpirationDate "MMMM do yyyy, h:mm:ss a"}}.
</span>
</li>
{{/if}}
{{#if this.hasEntityId}}
<li class="action">
<LinkTo @route="vault.cluster.mfa-setup" data-test-user-menu-item="mfa">
Multi-factor authentication
</LinkTo>
</li>
{{/if}}
<li class="action">
<CopyButton
@clipboardText={{this.auth.currentToken}}
class="link"
@buttonType="button"
@success={{action (set-flash-message "Token copied!")}}
>
Copy token
</CopyButton>
</li>
{{#if (is-before (now interval=1000) this.auth.tokenExpirationDate)}}
{{#if this.auth.authData.renewable}}
<li class="action">
<button
type="button"
{{on "click" this.renewToken}}
class="link button {{if this.isRenewing 'is-loading'}}"
data-test-user-menu-item="renew token"
>
Renew token
</button>
</li>
{{/if}}
<li class="action">
<c.Message
@id={{get this.auth "authData.displayName"}}
@title={{concat "Revoke " (get this.auth "authData.displayName") "?"}}
@onConfirm={{action "revokeToken"}}
@message="You will not be able to log in again with this token."
@triggerText="Revoke token"
@confirmButtonText="Revoke"
data-test-user-menu-item="revoke token"
/>
</li>
{{/if}}
<li class="action">
<LinkTo @route="vault.cluster.logout" @model={{this.currentCluster.cluster.name}} id="logout">
Log out
</LinkTo>
</li>
</ul>
</nav>
</div>
</div>
</Confirm>
</Dropdown.Content>
</BasicDropdown>

View File

@ -1,27 +1,12 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import Component from '@glimmer/component';
import { inject as service } from '@ember/service';
import { later } from '@ember/runloop';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';
/**
* @module AuthInfo
*
* @example
* ```js
* <AuthInfo @activeClusterName={{cluster.name}} @onLinkClick={{action "onLinkClick"}} />
* ```
*
* @param {string} activeClusterName - name of the current cluster, passed from the parent.
* @param {Function} onLinkClick - parent action which determines the behavior on link click
*/
export default class AuthInfoComponent extends Component {
export default class SidebarUserMenuComponent extends Component {
@service auth;
@service currentCluster;
@service router;
@tracked fakeRenew = false;
@ -29,7 +14,7 @@ export default class AuthInfoComponent extends Component {
get hasEntityId() {
// root users will not have an entity_id because they are not associated with an entity.
// in order to use the MFA end user setup they need an entity_id
return !!this.auth.authData.entity_id;
return !!this.auth.authData?.entity_id;
}
get isRenewing() {

View File

@ -31,8 +31,4 @@ export default class SplashPage extends Component {
// default is true unless showTruncatedNavBar is defined as false
return this.args.showTruncatedNavBar === false ? false : true;
}
get activeCluster() {
return this.store.peekRecord('cluster', this.auth.activeCluster);
}
}

View File

@ -1,49 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';
import { next } from '@ember/runloop';
/**
* @module StatusMenu
* StatusMenu component is the drop down menu on the main navigation.
*
* @example
* ```js
* <StatusMenu @label='user' @onLinkClick={{action Nav.closeDrawer}}/>
* ```
* @param {string} [ariaLabel] - aria label for the status icon.
* @param {string} [label] - label for the status menu.
* @param {string} [type] - determines where the component is being used. e.g. replication, auth, etc.
* @param {function} [onLinkClick] - function to handle click on the nested links under content.
*
*/
export default class StatusMenu extends Component {
@service currentCluster;
@service auth;
@service media;
@service router;
get type() {
return this.args.type || 'cluster';
}
get glyphName() {
return this.type === 'user' ? 'user' : 'circle-dot';
}
@action
onLinkClick(dropdown) {
if (dropdown) {
// strange issue where closing dropdown triggers full transition which redirects to auth screen in production builds
// closing dropdown in next tick of run loop fixes it
next(() => dropdown.actions.close());
}
this.args.onLinkClick();
}
}

View File

@ -5,19 +5,10 @@
import { inject as service } from '@ember/service';
import Controller from '@ember/controller';
import { computed } from '@ember/object';
import config from '../config/environment';
export default Controller.extend({
env: config.environment,
auth: service(),
store: service(),
activeCluster: computed('auth.activeCluster', function () {
const id = this.auth.activeCluster;
return id ? this.store.peekRecord('cluster', id) : null;
}),
activeClusterName: computed('activeCluster', function () {
const activeCluster = this.activeCluster;
return activeCluster ? activeCluster.get('name') : null;
}),
});

View File

@ -5,7 +5,6 @@
import { inject as service } from '@ember/service';
import Controller from '@ember/controller';
import { computed } from '@ember/object';
import config from '../config/environment';
export default Controller.extend({
@ -20,12 +19,4 @@ export default Controller.extend({
env: config.environment,
auth: service(),
store: service(),
activeCluster: computed('auth.activeCluster', function () {
const id = this.auth.activeCluster;
return id ? this.store.peekRecord('cluster', id) : null;
}),
activeClusterName: computed('activeCluster', function () {
const activeCluster = this.activeCluster;
return activeCluster ? activeCluster.get('name') : null;
}),
});

View File

@ -7,7 +7,7 @@
import { inject as service } from '@ember/service';
import { alias } from '@ember/object/computed';
import Controller from '@ember/controller';
import { observer, computed } from '@ember/object';
import { observer } from '@ember/object';
export default Controller.extend({
auth: service(),
store: service(),
@ -36,35 +36,7 @@ export default Controller.extend({
}),
consoleOpen: alias('console.isOpen'),
activeCluster: computed('auth.activeCluster', function () {
return this.store.peekRecord('cluster', this.auth.activeCluster);
}),
activeClusterName: computed('activeCluster', function () {
const activeCluster = this.activeCluster;
return activeCluster ? activeCluster.get('name') : null;
}),
showNav: computed(
'router.currentRouteName',
'activeClusterName',
'auth.currentToken',
'activeCluster.{dr.isSecondary,needsInit,sealed}',
function () {
if (this.activeCluster.dr?.isSecondary || this.activeCluster.needsInit || this.activeCluster.sealed) {
return false;
}
if (
this.activeClusterName &&
this.auth.currentToken &&
this.router.currentRouteName !== 'vault.cluster.auth'
) {
return true;
}
return;
}
),
activeCluster: alias('auth.activeCluster'),
actions: {
toggleConsole() {

View File

@ -20,9 +20,9 @@ export default Helper.extend({
),
compute([route], params) {
const { routeParams } = params;
const { routeParams, requireAll } = params;
const permissions = this.permissions;
return permissions.hasNavPermission(route, routeParams);
return permissions.hasNavPermission(route, routeParams, requireAll);
},
});

View File

@ -135,18 +135,5 @@ export default Route.extend(ModelBoundaryRoute, ClusterRoute, {
}
return true;
},
loading(transition) {
const isSameRoute = transition.from?.name === transition.to?.name;
if (isSameRoute || Ember.testing) {
return;
}
// eslint-disable-next-line ember/no-controller-access-in-routes
const controller = this.controllerFor('vault.cluster');
controller.set('currentlyLoading', true);
transition.finally(function () {
controller.set('currentlyLoading', false);
});
},
},
});

View File

@ -35,16 +35,6 @@ export default class ClientsRoute extends Route {
});
}
@action
async loading(transition) {
// eslint-disable-next-line ember/no-controller-access-in-routes
const controller = this.controllerFor(this.routeName);
controller.set('currentlyLoading', true);
transition.promise.finally(function () {
controller.set('currentlyLoading', false);
});
}
@action
deactivate() {
// when navigating away from parent route, delete manually inputted license start date

View File

@ -0,0 +1,10 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
export default class ClientsIndexRoute extends Route {
@service router;
redirect() {
this.router.transitionTo('vault.cluster.clients.dashboard');
}
}

View File

@ -26,20 +26,24 @@ export { TOKEN_SEPARATOR, TOKEN_PREFIX, ROOT_PREFIX };
export default Service.extend({
permissions: service(),
store: service(),
router: service(),
namespaceService: service('namespace'),
IDLE_TIMEOUT: 3 * 60e3,
expirationCalcTS: null,
isRenewing: false,
mfaErrors: null,
init() {
this._super(...arguments);
this.checkForRootToken();
get tokenExpired() {
const expiration = this.tokenExpirationDate;
return expiration ? this.now() >= expiration : null;
},
clusterAdapter() {
return getOwner(this).lookup('adapter:cluster');
get activeCluster() {
return this.activeClusterId ? this.store.peekRecord('cluster', this.activeClusterId) : null;
},
// eslint-disable-next-line
tokens: computed({
get() {
@ -50,6 +54,84 @@ export default Service.extend({
},
}),
isActiveSession: computed(
'router.currentRouteName',
'currentToken',
'activeCluster.{dr.isSecondary,needsInit,sealed,name}',
function () {
if (this.activeCluster) {
if (this.activeCluster.dr?.isSecondary || this.activeCluster.needsInit || this.activeCluster.sealed) {
return false;
}
if (
this.activeCluster.name &&
this.currentToken &&
this.router.currentRouteName !== 'vault.cluster.auth'
) {
return true;
}
}
return false;
}
),
tokenExpirationDate: computed('currentTokenName', 'expirationCalcTS', function () {
const tokenName = this.currentTokenName;
if (!tokenName) {
return;
}
const { tokenExpirationEpoch } = this.getTokenData(tokenName);
const expirationDate = new Date(0);
return tokenExpirationEpoch ? expirationDate.setUTCMilliseconds(tokenExpirationEpoch) : null;
}),
renewAfterEpoch: computed('currentTokenName', 'expirationCalcTS', function () {
const tokenName = this.currentTokenName;
const { expirationCalcTS } = this;
const data = this.getTokenData(tokenName);
if (!tokenName || !data || !expirationCalcTS) {
return null;
}
const { ttl, renewable } = data;
// renew after last expirationCalc time + half of the ttl (in ms)
return renewable ? Math.floor((ttl * 1e3) / 2) + expirationCalcTS : null;
}),
// returns the key for the token to use
currentTokenName: computed('activeClusterId', 'tokens', 'tokens.[]', function () {
const regex = new RegExp(this.activeClusterId);
return this.tokens.find((key) => regex.test(key));
}),
currentToken: computed('currentTokenName', function () {
const name = this.currentTokenName;
const data = name && this.getTokenData(name);
// data.token is undefined so that's why it returns current token undefined
return name && data ? data.token : null;
}),
authData: computed('currentTokenName', function () {
const token = this.currentTokenName;
if (!token) {
return;
}
const backend = this.backendFromTokenName(token);
const stored = this.getTokenData(token);
return assign(stored, {
backend: BACKENDS.findBy('type', backend),
});
}),
init() {
this._super(...arguments);
this.checkForRootToken();
},
clusterAdapter() {
return getOwner(this).lookup('adapter:cluster');
},
generateTokenName({ backend, clusterId }, policies) {
return (policies || []).includes('root')
? `${TOKEN_PREFIX}${ROOT_PREFIX}${TOKEN_SEPARATOR}${clusterId}`
@ -83,7 +165,7 @@ export default Service.extend({
},
setCluster(clusterId) {
this.set('activeCluster', clusterId);
this.set('activeClusterId', clusterId);
},
ajax(url, method, options) {
@ -196,7 +278,7 @@ export default Service.extend({
tokenName = this.generateTokenName(
{
backend,
clusterId: (options && options.clusterId) || this.activeCluster,
clusterId: (options && options.clusterId) || this.activeClusterId,
},
resp.policies
);
@ -231,33 +313,6 @@ export default Service.extend({
return this.storage(token).removeItem(token);
},
tokenExpirationDate: computed('currentTokenName', 'expirationCalcTS', function () {
const tokenName = this.currentTokenName;
if (!tokenName) {
return;
}
const { tokenExpirationEpoch } = this.getTokenData(tokenName);
const expirationDate = new Date(0);
return tokenExpirationEpoch ? expirationDate.setUTCMilliseconds(tokenExpirationEpoch) : null;
}),
get tokenExpired() {
const expiration = this.tokenExpirationDate;
return expiration ? this.now() >= expiration : null;
},
renewAfterEpoch: computed('currentTokenName', 'expirationCalcTS', function () {
const tokenName = this.currentTokenName;
const { expirationCalcTS } = this;
const data = this.getTokenData(tokenName);
if (!tokenName || !data || !expirationCalcTS) {
return null;
}
const { ttl, renewable } = data;
// renew after last expirationCalc time + half of the ttl (in ms)
return renewable ? Math.floor((ttl * 1e3) / 2) + expirationCalcTS : null;
}),
renew() {
const tokenName = this.currentTokenName;
const currentlyRenewing = this.isRenewing;
@ -422,32 +477,6 @@ export default Service.extend({
this.set('tokens', tokenNames);
},
// returns the key for the token to use
currentTokenName: computed('activeCluster', 'tokens', 'tokens.[]', function () {
const regex = new RegExp(this.activeCluster);
return this.tokens.find((key) => regex.test(key));
}),
currentToken: computed('currentTokenName', function () {
const name = this.currentTokenName;
const data = name && this.getTokenData(name);
// data.token is undefined so that's why it returns current token undefined
return name && data ? data.token : null;
}),
authData: computed('currentTokenName', function () {
const token = this.currentTokenName;
if (!token) {
return;
}
const backend = this.backendFromTokenName(token);
const stored = this.getTokenData(token);
return assign(stored, {
backend: BACKENDS.findBy('type', backend),
});
}),
getOktaNumberChallengeAnswer(nonce, mount) {
const url = `/v1/auth/${mount}/verify/${nonce}`;
return this.ajax(url, 'GET', {}).then(

View File

@ -95,12 +95,15 @@ export default Service.extend({
this.set('canViewAll', null);
},
hasNavPermission(navItem, routeParams) {
hasNavPermission(navItem, routeParams, requireAll) {
if (routeParams) {
// viewing the entity and groups pages require the list capability, while the others require the default, which is anything other than deny
const capability = routeParams === 'entities' || routeParams === 'groups' ? ['list'] : [null];
return this.hasPermission(API_PATHS[navItem][routeParams], capability);
// check that the user has permission to access all (requireAll = true) or any of the routes when array is passed
// useful for hiding nav headings when user does not have access to any of the links
const params = Array.isArray(routeParams) ? routeParams : [routeParams];
const evalMethod = !Array.isArray(routeParams) || requireAll ? 'every' : 'some';
return params[evalMethod]((param) => this.hasPermission(API_PATHS[navItem][param], capability));
}
return Object.values(API_PATHS[navItem]).some((path) => this.hasPermission(path));
},

View File

@ -3,8 +3,10 @@
* SPDX-License-Identifier: MPL-2.0
*/
@import './reset';
@import 'ember-basic-dropdown';
@import 'ember-power-select';
@import '@hashicorp/design-system-components';
@import './core';
@mixin font-face($name) {

View File

@ -3,25 +3,26 @@
* SPDX-License-Identifier: MPL-2.0
*/
$console-close-height: 35px;
.console-ui-panel {
background: linear-gradient(to right, #191a1c, #1b212d);
background: var(--token-color-palette-neutral-700);
width: -moz-available;
width: -webkit-fill-available;
height: 0;
left: 0;
position: fixed;
min-height: 0;
overflow: scroll;
right: 0;
top: 4rem;
overflow: auto;
position: fixed;
bottom: 0;
transition: min-height $speed $easing, transform $speed ease-in;
will-change: transform, min-height;
-webkit-overflow-scrolling: touch;
width: 100vw;
z-index: 199;
.button {
background: transparent;
border: none;
color: $grey;
color: $white;
min-width: 0;
padding: 0 $size-8;
@ -40,8 +41,8 @@
font-size: 14px;
font-weight: $font-weight-semibold;
justify-content: flex-end;
min-height: 100%;
padding: $size-8 $size-8 $size-4;
min-height: calc(100% - $console-close-height); // account for close button that is sticky positioned
padding: $size-8 $size-8 $size-5;
transition: justify-content $speed ease-in;
pre,
@ -78,16 +79,17 @@
input {
background-color: rgba($black, 0.5);
border: 0;
border: 1px solid var(--token-color-palette-neutral-500);
border-radius: 2px;
caret-color: $white;
color: $white;
flex: 1 1 auto;
font-family: $family-monospace;
font-size: 16px;
font-weight: $font-weight-bold;
margin-left: -$size-10;
outline: none;
padding: $size-10;
margin-right: $spacing-xs;
transition: background-color $speed;
}
}
@ -125,31 +127,9 @@
.panel-open .console-ui-panel.fullscreen {
bottom: 0;
top: 0;
right: 0;
min-height: 100vh;
}
.panel-open {
.navbar,
.navbar-sections {
transition: transform $speed ease-in;
}
}
.panel-open.panel-fullscreen {
.navbar,
.navbar-sections {
@include from($mobile) {
transform: translateY(-100px);
}
}
}
header .navbar,
header .navbar-sections {
z-index: 200;
transform: translateY(0);
will-change: transform;
width: 100%;
}
.console-spinner.control {
@ -168,12 +148,14 @@ header .navbar-sections {
}
.console-close-button {
position: absolute;
top: -3.25rem;
right: $spacing-xs;
position: sticky;
top: $spacing-xs;
height: $console-close-height;
display: flex;
justify-content: flex-end;
z-index: 210;
@include from($mobile) {
display: none;
button {
margin-right: $spacing-xs;
}
}

View File

@ -14,6 +14,7 @@
animation: env-banner-color-rotate 8s infinite linear alternate;
color: $white;
margin-top: -20px;
margin-bottom: 6px;
.hs-icon {
margin: 0;

View File

@ -74,3 +74,11 @@
width: 32px;
height: 32px;
}
.brand-icon-large {
width: 62px;
}
.error-icon {
width: 48px;
}

View File

@ -5,65 +5,33 @@
.namespace-picker {
position: relative;
color: $white;
color: var(--token-color-palette-neutral-300);
display: flex;
fill: $white;
padding: $spacing-xxs $spacing-xs;
width: 100%;
@include from($mobile) {
margin-left: -$spacing-xs;
padding: $spacing-xxs 0 $spacing-xxs $spacing-s;
width: auto;
}
}
.namespace-picker.no-namespaces {
border: none;
padding-right: 0;
}
.namespace-picker-trigger {
align-items: center;
display: flex;
flex: 1 1 auto;
height: 2rem;
justify-content: space-between;
padding: 0;
text-align: left;
@include from($mobile) {
height: auto;
padding: $spacing-xs $spacing-m;
}
.is-status-chevron {
transform: rotate(-90deg);
@include from($mobile) {
transform: rotate(0deg);
}
}
&.ember-basic-dropdown-trigger--below .is-status-chevron {
transform: rotate(0deg);
@include from($mobile) {
transform: rotate(180deg);
}
}
margin-right: $spacing-xxs;
}
.namespace-name {
display: inline-block;
flex: 1 1 auto;
font-size: 1rem;
margin: 0 $spacing-xs;
@include from($mobile) {
margin-left: $size-10;
}
margin-left: $spacing-xs;
}
.namespace-picker-content {
width: $drawer-width - ($spacing-xs * 2);
width: 250px;
max-height: 300px;
overflow: auto;
border-radius: $radius;
@ -72,10 +40,6 @@
&.ember-basic-dropdown-content {
background: $white;
}
@include from($mobile) {
width: $drawer-width;
}
}
.namespace-picker-content .level-left {
@ -95,6 +59,14 @@
.namespace-manage-link {
border-top: 1px solid rgba($black, 0.1);
.level-left {
font-weight: $font-weight-bold;
font-size: 14px;
}
.level-right {
margin-right: 10px;
}
}
.namespace-list {
@ -112,6 +84,11 @@
.namespace-link.is-current {
margin-top: $size-8;
margin-right: -$size-10;
svg {
margin-top: 2px;
color: var(--token-color-border-strong);
}
}
.leaf-panel {
@ -127,9 +104,11 @@
bottom: 0;
z-index: 1;
}
.leaf-panel-left {
transform: translateX(-$drawer-width);
}
.leaf-panel-adding,
.leaf-panel-current {
position: relative;
@ -137,6 +116,7 @@
margin-bottom: 4px;
}
}
.animated-list {
.leaf-panel-exiting,
.leaf-panel-adding {
@ -144,6 +124,7 @@
z-index: 20;
}
}
.leaf-panel-adding {
z-index: 100;
}

View File

@ -26,7 +26,7 @@
}
.title {
margin-top: $size-1;
margin-top: $size-2;
}
.title-with-icon {

View File

@ -132,16 +132,6 @@
}
}
.status-menu-content {
margin-top: 8px;
.box {
@include until($mobile) {
width: $drawer-width - ($spacing-xs * 2);
}
}
}
.ember-basic-dropdown-content {
background-color: transparent;

View File

@ -3,107 +3,44 @@
* SPDX-License-Identifier: MPL-2.0
*/
.is-sidebar {
border-right: $base-border;
display: flex;
flex: 1 1 auto;
margin: 0.75rem 0.75rem 0.75rem 0;
padding: 0 0 0 0.75rem;
.sidebar-user-menu {
align-self: center;
@include until($mobile) {
background-color: $white;
bottom: 0;
left: -1.5rem;
margin: 0;
max-width: $drawer-width;
padding: $spacing-m 0 0;
position: absolute;
right: $size-2;
transform: translateX(-100%);
transition: transform $speed;
top: 0;
z-index: 5;
}
&.is-active {
@include until($mobile) {
transform: translateX(0);
.popup-menu-content {
.menu-label {
color: $black;
font-size: 14px;
font-weight: $font-weight-bold;
text-transform: unset;
}
.menu-toggle {
left: auto;
right: $size-10;
}
}
.menu-toggle {
color: $blue;
cursor: pointer;
display: none;
margin-left: $size-10;
left: 100%;
position: absolute;
top: 0;
@include until($mobile) {
display: block;
}
.button {
min-width: 0;
}
}
.menu {
flex: 1 1 auto;
padding-top: 5.25rem;
position: relative;
@include until($mobile) {
padding-top: $size-6;
}
}
.menu-label {
color: $grey-light;
font-weight: $font-weight-bold;
font-size: $size-8;
line-height: 1;
margin-bottom: $size-8;
padding-left: $size-5;
}
.menu-list {
border-top: $base-border;
padding: $size-9 0;
@include until($mobile) {
padding-top: $size-4;
}
li {
a {
&.active {
border-right: 4px solid $blue;
color: $blue;
}
}
}
a {
color: $grey-dark;
padding-left: $size-5;
transition: 250ms border-width;
&.active {
border-right: 4px solid $blue;
}
}
// TODO will be removed with the navbar work. Reminder to remove the var $fullhd as this is the only use case.
.tag {
@include from($fullhd) {
float: right;
}
.token-alert {
padding: $spacing-xs;
}
}
}
.link-status {
height: 40px;
display: flex;
justify-content: center;
align-items: center;
font-size: $size-7;
font-weight: $font-weight-semibold;
&.connected {
background-color: var(--token-color-surface-action);
color: var(--token-color-foreground-action-active);
a {
color: var(--token-color-foreground-action-active);
}
}
&.warning {
background-color: var(--token-color-surface-warning);
color: var(--token-color-palette-amber-300);
a {
color: var(--token-color-palette-amber-300);
}
}
}

View File

@ -3,7 +3,7 @@
* SPDX-License-Identifier: MPL-2.0
*/
.navbar-brand .splash-page-logo {
.splash-page-logo {
padding: $spacing-xs $spacing-s $spacing-xs $spacing-l;
@include from($mobile) {

View File

@ -1,24 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
.status-indicator-button {
&[data-status='good'] {
.status-indicator-color {
color: $green-light;
}
}
&[data-status='mixed'] {
.status-indicator-color {
color: $yellow;
}
}
&[data-status='bad'] {
.status-indicator-color {
color: $red;
}
}
}

View File

@ -40,13 +40,12 @@
&.is-active {
border-bottom: 2px solid $blue;
color: $blue;
> button {
color: $blue;
}
}
// solves for tabs on secret engines
> a &.active {
border-bottom: 2px solid $blue;
color: $blue;
}
// solves for tabs on auth mounts
// solves for tabs on auth mounts & secrets engines
> a {
&.active {
color: $blue;

View File

@ -36,7 +36,6 @@
@import './core/lists';
@import './core/menu';
@import './core/message';
@import './core/navbar';
@import './core/progress';
@import './core/select';
@import './core/switch';
@ -66,7 +65,7 @@
@import './components/control-group';
@import './components/diff-version-selector';
@import './components/doc-link';
@import './components/empty-state';
@import './components/empty-state-component';
@import './components/env-banner';
@import './components/features-selection';
@import './components/form-section';
@ -85,7 +84,7 @@
@import './components/loader';
@import './components/login-form';
@import './components/masked-input';
@import './components/modal';
@import './components/modal-component.scss';
@import './components/namespace-picker';
@import './components/namespace-reminder';
@import './components/navigate-input';
@ -112,8 +111,7 @@
@import './components/sidebar';
@import './components/splash-page';
@import './components/stat-text';
@import './components/status-menu';
@import './components/tabs';
@import './components/tabs-component';
@import './components/text-file';
@import './components/token-expire-warning';
@import './components/toolbar';

View File

@ -15,12 +15,6 @@
margin: 0;
}
.is-sidebar + .column & {
@include until($mobile) {
margin-left: $size-2;
}
}
ul,
ol {
align-items: center;

View File

@ -11,10 +11,9 @@
}
.page-container {
min-height: calc(100vh - 4rem);
min-height: 100vh;
display: flex;
flex-direction: column;
margin-top: 4rem;
justify-content: flex-end;
}
@ -39,30 +38,7 @@
.container {
flex-grow: 1;
margin: 0 auto;
max-width: 1024px;
position: relative;
width: auto;
}
@media screen and (min-width: 1024px) {
.container {
max-width: 960px;
}
}
@media screen and (max-width: 1215px) {
.container.is-widescreen:not(.is-max-desktop) {
max-width: 1152px;
}
}
@media screen and (min-width: 1216px) {
.container:not(.is-max-desktop) {
max-width: 1152px;
}
}
@media screen and (min-width: 1408px) {
.container:not(.is-max-desktop):not(.is-max-widescreen) {
max-width: 1344px;
}
}

View File

@ -17,10 +17,10 @@
height: 2.25em;
justify-content: flex-start;
min-width: auto;
padding-bottom: calc(0.375em -1px);
padding-bottom: calc(0.375em - 1px);
padding-left: 1em;
padding-right: 1em;
padding-top: calc(0.375em -1px);
padding-top: calc(0.375em - 1px);
position: relative;
vertical-align: top;
white-space: nowrap;

View File

@ -1,309 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
.navbar {
left: 0;
position: fixed;
right: 0;
top: 0;
@include from($mobile) {
display: block;
}
}
.navbar-status {
height: 40px;
display: flex;
justify-content: center;
align-items: center;
font-size: $size-7;
font-weight: $font-weight-semibold;
&.connected {
background-color: $ui-gray-800;
color: $ui-gray-300;
a {
color: #c2c5cb;
}
}
&.warning {
background-color: #fcf6ea;
color: #975b06;
a {
color: #975b06;
}
}
}
.navbar-actions {
background-color: $black;
display: flex;
height: 4rem;
justify-content: flex-start;
padding: $spacing-xs $spacing-s $spacing-xs 0;
}
.navbar-brand {
align-items: stretch;
background: $grey;
border-radius: 0 $radius-large $radius-large 0;
display: flex;
margin-right: $spacing-s;
min-height: auto;
position: relative;
z-index: 203;
.navbar-item {
align-items: center;
background-color: transparent;
display: flex;
padding: $spacing-xs $spacing-l;
&:hover,
&.is-active {
background-color: transparent;
}
}
}
.navbar-drawer-toggle {
font-size: $size-6;
color: $grey;
cursor: pointer;
font-weight: $font-weight-semibold;
margin-left: -$spacing-s;
padding: $spacing-xs $spacing-xxs;
background: none;
border: none;
.navbar-drawer & {
position: absolute;
top: $spacing-xs;
right: $spacing-xxs;
}
}
.navbar-drawer-overlay {
height: 100vh;
left: 0;
pointer-events: none;
position: fixed;
right: 0;
top: 0;
transition: background-color $speed, opacity $speed;
will-change: background-color, opacity;
z-index: -1;
&.is-active {
background-color: rgba($black, 0.25);
pointer-events: all;
@include from($mobile) {
background-color: transparent;
pointer-events: none;
}
}
}
.navbar-sections,
.navbar-sections li,
.navbar-drawer-scroll,
.navbar-drawer-scroll > * {
@include from($mobile) {
align-items: center;
display: flex;
}
}
.navbar-sections {
a {
color: $grey-light;
display: block;
font-weight: $font-weight-semibold;
line-height: 1.3;
padding: $spacing-xs $spacing-m;
text-decoration: none;
transition: background-color $speed, color $speed;
will-change: background-color, color;
@include from($mobile) {
border-radius: $radius;
display: inline-block;
padding: $spacing-xxs $spacing-s;
}
&.is-active {
background-color: $ui-gray-800;
color: $white;
}
&:hover {
color: $white;
}
}
}
.navbar-end {
margin-left: auto;
}
.navbar-item {
padding: $spacing-xs;
}
.navbar-separator {
background-color: $ui-gray-700;
height: 1px;
margin: $spacing-xs 0;
width: 100%;
@include from($mobile) {
height: $spacing-l;
margin: 0 $spacing-s;
width: 1px;
}
}
.navbar-drawer {
flex: 1 1 auto;
@include until($mobile) {
background-color: $ui-gray-900;
display: flex;
flex-direction: column;
height: 100vh;
left: 0;
padding: 4rem 0 $spacing-m;
position: fixed;
top: 0;
transform: translateX(-100%);
transition: box-shadow $speed, transform $speed-slow;
width: $drawer-width;
will-change: transform, box-shadow;
z-index: 201;
}
&.is-active {
@include until($mobile) {
box-shadow: 5px 0 10px rgba($black, 0.36);
transform: translateX(0);
}
}
.navbar-item .button {
color: $grey-light;
display: flex;
font-size: 1rem;
height: auto;
justify-content: flex-start;
text-align: left;
width: 100%;
@include from($mobile) {
display: inline-flex;
height: $spacing-l;
width: auto;
}
&.popup-open,
&.ember-basic-dropdown-trigger--below {
color: $white;
.is-status-chevron {
transform: rotate(0deg);
@include from($mobile) {
transform: rotate(180deg);
}
}
}
.is-status-chevron {
transform: rotate(270deg);
@include from($mobile) {
transform: rotate(0deg);
}
}
}
.button .icon,
.button .icon:first-child:not(:last-child) {
flex: 0;
margin: 0 $spacing-xs 0 0;
@include from($mobile) {
margin: -$spacing-xxs;
margin-right: 0;
}
}
.status-menu-label {
flex: 1 1 auto;
line-height: 1;
}
.nav-console-button .status-menu-label,
.nav-user-button .status-menu-label {
flex: 1 1 auto;
@include from($mobile) {
display: none;
}
}
}
.nav-user-button .icon {
position: relative;
}
.nav-user-button.may-expire .icon:first-of-type::after {
content: '';
position: absolute;
top: 0;
right: 0;
height: 6px;
width: 6px;
border-radius: 50%;
background: $yellow;
}
.navbar-drawer-scroll {
overflow: auto;
height: 100%;
-webkit-overflow-scrolling: touch;
&::before {
background-image: linear-gradient(to bottom, $ui-gray-900, rgba($ui-gray-900, 0));
content: '';
height: $spacing-xs;
left: 0;
position: absolute;
right: 0;
top: 4rem;
z-index: 1;
@include from($mobile) {
display: none;
}
}
}
.navbar-drawer .ember-basic-dropdown-content {
@include until($mobile) {
position: relative;
}
}
// responsive css
@media screen and (min-width: 1024px) {
.navbar-item,
.navbar-link {
align-items: center;
display: flex;
}
}

View File

@ -95,7 +95,7 @@
content: '';
height: $size-8;
position: absolute;
top: $size-8/ 5;
top: calc($size-8 / 5);
width: $size-8 * 2;
}
&::after {
@ -106,7 +106,7 @@
height: $size-8 * 0.8;
left: 0;
position: absolute;
top: $size-8/ 4;
top: calc($size-8 / 4);
transform: translateX(0.15rem);
transition: all 0.25s ease-out;
width: $size-8 * 0.8;

View File

@ -74,7 +74,7 @@
padding-left: $size-8 * 2.5;
margin: 0 0.25rem;
&::before {
top: $size-8 / 5;
top: calc($size-8 / 5);
height: $size-8;
width: $size-8 * 2;
}
@ -83,7 +83,7 @@
height: $size-8 * 0.8;
transform: translateX(0.15rem);
left: 0;
top: $size-8/ 4;
top: calc($size-8 / 4);
}
}
&:checked + label::after {

View File

@ -34,6 +34,10 @@
padding-bottom: $spacing-s;
}
.has-bottom-padding-l {
padding-bottom: $spacing-l;
}
.has-top-padding-s {
padding-top: $spacing-s;
}

View File

@ -79,6 +79,10 @@
text-align: center !important;
}
.has-line-height-1 {
line-height: 1;
}
// Text color or styling
.is-help {
font-size: $size-8;

6
ui/app/styles/reset.scss Normal file
View File

@ -0,0 +1,6 @@
// reset for HDS
*,
*::before,
*::after {
box-sizing: border-box;
}

View File

@ -6,12 +6,12 @@
/* General sizing in rem values used largely for text sizing.*/
$size-1: 3rem; // 48px, same as $spacing-xxl
$size-2: 2.5rem; // 40px
$size-3: (24/14) + 0rem; // ~1.714rem ~27px
$size-3: calc(24 / 14) + 0rem; // ~1.714rem ~27px
$size-4: 1.5rem; // 24px same as $spacing-l
$size-5: 1.25rem; // 20px
$size-6: 1rem; // 16px same as $spacing-m
$size-7: (13/14) + 0rem; // ~.929rem ~15px
$size-8: (12/14) + 0rem; // ~.857rem ~13.7px
$size-7: calc(13 / 14) + 0rem; // ~.929rem ~15px
$size-8: calc(12 / 14) + 0rem; // ~.857rem ~13.7px
$size-9: 0.75rem; // 12px same as $spacing-s
$size-10: 0.5rem; // 8px same as $spacing-xs
$size-11: 0.25rem; // 4px same as spacing-xxs

View File

@ -84,6 +84,6 @@
@mixin vault-block {
&:not(:last-child) {
margin-bottom: (5/14) + 0rem;
margin-bottom: calc(5 / 14) + 0rem;
}
}

View File

@ -1,3 +1,5 @@
<div class="page-container">
{{outlet}}
</div>
<Sidebar::Frame @showSidebar={{this.auth.isActiveSession}}>
<div class="page-container">
{{outlet}}
</div>
</Sidebar::Frame>

View File

@ -1,6 +1,6 @@
{{#unless this.selectedAuthIsPath}}
<div class="box has-slim-padding is-shadowless">
<ToggleButton @isOpen={{this.isOpen}} @onClick={{fn (mut this.isOpen)}} />
<ToggleButton @isOpen={{this.isOpen}} @onClick={{fn (mut this.isOpen)}} data-test-auth-form-options-toggle />
{{#if this.isOpen}}
<div class="field">
<label for="custom-path" class="is-label">
@ -14,6 +14,7 @@
class="input"
value={{@customPath}}
oninput={{action @onPathChange value="target.value"}}
data-test-auth-form-mount-path
/>
</div>
<AlertInline

View File

@ -1,81 +0,0 @@
<Confirm as |c|>
<div class="popup-menu-content">
<div class="box">
<div class="menu-label">
{{this.auth.authData.displayName}}
</div>
<nav class="menu">
<ul class="menu-list">
{{#if this.auth.allowExpiration}}
<li class="action">
<AlertBanner
@type="warning"
@message="We've stopped auto-renewing your token due to inactivity.
It will expire in {{date-from-now this.auth.tokenExpirationDate interval=1000 hideSuffix=true}}.
On {{date-format this.auth.tokenExpirationDate 'MMMM do yyyy, h:mm:ss a'}}"
/>
</li>
{{/if}}
{{#if this.hasEntityId}}
<li class="action">
<LinkTo @route="vault.cluster.mfa-setup" data-test-status-link="mfa">
Multi-factor authentication
</LinkTo>
</li>
{{/if}}
<li class="action">
<CopyButton
@clipboardText={{this.auth.currentToken}}
class="link"
@buttonType="button"
@success={{action (set-flash-message "Token copied!")}}
>
Copy token
</CopyButton>
</li>
{{#if (is-before (now interval=1000) this.auth.tokenExpirationDate)}}
{{#if this.auth.authData.renewable}}
<li class="action">
<button type="button" {{on "click" this.renewToken}} class="link button {{if this.isRenewing 'is-loading'}}">
Renew token
</button>
</li>
<li class="action">
<c.Message
@id={{get this.auth "authData.displayName"}}
@title={{concat "Revoke " (get this.auth "authData.displayName") "?"}}
@onConfirm={{action "revokeToken"}}
@message="You will not be able to log in again with this token."
@triggerText="Revoke token"
@confirmButtonText="Revoke"
/>
</li>
{{else}}
<li class="action text-right">
<c.Message
@id={{get this.auth "authData.displayName"}}
@title={{concat "Revoke " (get this.auth "authData.displayName") "?"}}
@onConfirm={{action "revokeToken"}}
@message="You will not be able to log in again with this token."
@triggerText="Revoke token"
@confirmButtonText="Revoke"
/>
</li>
{{/if}}
{{/if}}
<li class="action">
<LinkTo
@route="vault.cluster.logout"
@model={{@activeClusterName}}
id="logout"
class="is-destroy"
{{on "click" @onLinkClick}}
>
Sign out
</LinkTo>
</li>
</ul>
</nav>
</div>
</div>
</Confirm>

View File

@ -1,9 +1,5 @@
<BasicDropdown @class="popup-menu" @horizontalPosition="auto-right" @verticalPosition="below" as |D|>
<D.Trigger
data-test-popup-menu-trigger="true"
class={{concat "toolbar-link" (if D.isOpen " is-active")}}
@htmlTag="button"
>
<D.Trigger data-test-calendar-widget-trigger class={{concat "toolbar-link" (if D.isOpen " is-active")}} @htmlTag="button">
{{date-format this.startDate "MMM yyyy"}}
-
{{date-format this.endDate "MMM yyyy"}}

View File

@ -1,150 +0,0 @@
<div class="popup-menu-content">
<div class="box">
{{#unless this.version.isOSS}}
{{#if (and this.activeCluster.unsealed this.auth.currentToken)}}
{{#if @cluster.dr.isSecondary}}
{{#if (has-permission "status" routeParams="replication")}}
<nav class="menu" aria-label="replication secondary menu">
<p class="menu-label">Replication</p>
<ul>
{{#if @cluster.anyReplicationEnabled}}
<li>
<LinkTo
@route="vault.cluster.replication-dr-promote.details"
@disabled={{not this.auth.currentToken}}
{{on "click" @onLinkClick}}
>
<ReplicationModeSummary @mode="dr" @display="menu" @cluster={{@cluster}} />
</LinkTo>
</li>
{{/if}}
</ul>
</nav>
<hr />
{{/if}}
{{else}}
{{#if (has-permission "status" routeParams="replication")}}
<nav class="menu" aria-label="replication menu">
<p class="menu-label">Replication</p>
<ul>
{{#if @cluster.anyReplicationEnabled}}
<li>
<LinkTo
@route="vault.cluster.replication.mode.index"
@model="dr"
@disabled={{not this.auth.currentToken}}
{{on "click" @onLinkClick}}
>
<ReplicationModeSummary @mode="dr" @display="menu" @cluster={{@cluster}} />
</LinkTo>
</li>
<li>
{{#if (has-feature "Performance Replication")}}
<LinkTo
@route="vault.cluster.replication.mode.index"
@model="performance"
@disabled={{not this.auth.currentToken}}
{{on "click" @onLinkClick}}
>
<ReplicationModeSummary @mode="performance" @display="menu" @cluster={{@cluster}} @tagName="span" />
</LinkTo>
{{else}}
<ReplicationModeSummary @mode="performance" @display="menu" @cluster={{@cluster}} @class="menu-item" />
{{/if}}
</li>
{{else}}
<li>
<LinkTo @route="vault.cluster.replication" {{on "click" @onLinkClick}}>
<div class="level is-mobile">
<span class="level-left">Enable</span>
<Icon @name="plus-circle" class="has-text-grey-light level-right" />
</div>
</LinkTo>
</li>
{{/if}}
</ul>
</nav>
<hr />
{{/if}}
{{/if}}
{{/if}}
{{/unless}}
<nav class="menu" aria-label="server menu">
<div class="menu-label">
Server
</div>
<ul class="menu-list">
<li class="action">
{{#if this.activeCluster.unsealed}}
{{#if (and (has-permission "status" routeParams="seal") (not @cluster.dr.isSecondary))}}
<LinkTo @route="vault.cluster.settings.seal" @model={{@cluster.name}} {{on "click" @onLinkClick}}>
<div class="level is-mobile">
<span class="level-left">Unsealed</span>
<Icon @name="check-circle" class="has-text-success level-right" />
</div>
</LinkTo>
{{else}}
<span class="menu-item">
<div class="level is-mobile">
<span class="level-left">Unsealed</span>
<Icon @name="check-circle" class="has-text-success level-right" />
</div>
</span>
{{/if}}
{{else}}
<span class="menu-item">
<div class="level is-mobile">
<span class="level-left has-text-danger">Sealed</span>
<Icon @name="x-circle" class="has-text-danger level-right" />
</div>
</span>
{{/if}}
</li>
</ul>
{{#if
(and
(or
(and this.version.features (has-permission "status" routeParams="license"))
(and @cluster.usingRaft (has-permission "status" routeParams="raft"))
)
(not @cluster.dr.isSecondary)
)
}}
<ul class="menu-list">
{{#if (and this.version.features (has-permission "status" routeParams="license") (not @cluster.dr.isSecondary))}}
<li class="action">
<LinkTo @route="vault.cluster.license" @model={{this.activeCluster.name}} {{on "click" @onLinkClick}}>
<div class="level is-mobile">
<span class="level-left">License</span>
<Chevron class="has-text-grey-light level-right" />
</div>
</LinkTo>
</li>
{{/if}}
{{#if (and @cluster.usingRaft (has-permission "status" routeParams="raft"))}}
<li class="action">
<LinkTo @route="vault.cluster.storage" @model={{this.activeCluster.name}} {{on "click" @onLinkClick}}>
<div class="level is-mobile">
<span class="level-left">Raft Storage</span>
<Chevron class="has-text-grey-light level-right" />
</div>
</LinkTo>
</li>
{{/if}}
</ul>
{{/if}}
{{#if (and (has-permission "clients" routeParams="activity") (not @cluster.dr.isSecondary) this.auth.currentToken)}}
<ul class="menu-list">
<li class="action">
<LinkTo @route="vault.cluster.clients.dashboard" {{on "click" @onLinkClick}}>
<div class="level is-mobile">
<span class="level-left">Client count</span>
<Chevron class="has-text-grey-light level-right" />
</div>
</LinkTo>
</li>
</ul>
{{/if}}
</nav>
</div>
</div>

View File

@ -5,7 +5,7 @@
<Chevron />
{{/if}}
<input onkeyup={{action "handleKeyUp"}} value={{this.value}} autocomplete="off" spellcheck="false" />
<ToolTip @horizontalPosition="auto-right" @verticalPosition={{if this.isFullscreen "above" "below"}} as |d|>
<ToolTip @horizontalPosition="auto-right" @verticalPosition="above" as |d|>
<d.Trigger>
<button
type="button"

View File

@ -1,2 +1,4 @@
{{! using Icon here instead of Chevron because two nested tagless components results in a rendered line break between the tags breaking the layout in the <pre> }}
<p class="console-ui-command is-font-mono"><Icon @name="chevron-right" />{{@content}}</p>
<div class="is-flex-center">
<Icon @name="chevron-right" />
<pre class="console-ui-command">{{@content}}</pre>
</div>

View File

@ -1,6 +1,8 @@
<button type="button" class="button is-ghost console-close-button" {{action "closeConsole"}}>
<Icon @name="x" aria-label="Close console" />
</button>
<div class="console-close-button">
<button type="button" class="button is-ghost" {{action "closeConsole"}} data-test-console-panel-close>
<Icon @name="x" aria-label="Close console" />
</button>
</div>
<div class="console-ui-panel-content">
<div class="content has-bottom-margin-l">
<p class="has-text-grey is-font-mono">

View File

@ -1,7 +0,0 @@
<span class={{@class}}>
{{#if (has-block)}}
{{yield}}
{{else}}
{{@text}}
{{/if}}
</span>

View File

@ -1,5 +1,5 @@
{{#if (and this.state this.version.isEnterprise)}}
<div class="navbar-status {{if (eq this.state 'connected') 'connected' 'warning'}}">
<div class="link-status {{if (eq this.state 'connected') 'connected' 'warning'}}">
<Icon @name="info" />
<p data-test-link-status>
{{#if (eq this.state "connected")}}

View File

@ -1,21 +0,0 @@
<aside class="menu">
{{#if this.title}}
<p class="menu-label">
{{this.title}}
</p>
{{/if}}
<ul class="menu-list">
{{yield}}
</ul>
<div class="menu-toggle">
{{#if this.isActive}}
<button type="button" class="button is-ghost" {{action "closeMenu"}}>
<Icon @name="x" aria-label="Close menu" />
</button>
{{else}}
<button type="button" class="button is-ghost has-text-grey-light" {{action "openMenu"}}>
<Icon @name="more-vertical" aria-label="Open menu" />
</button>
{{/if}}
</div>
</aside>

View File

@ -1,25 +1,32 @@
{{#if (and (not this.accessibleNamespaces.length) this.inRootNamespace)}}
<div class="namespace-picker no-namespaces">
{{! Just yield the logo if they're in the root namespace and only have access to it }}
{{yield}}
</div>
{{else}}
<div class="namespace-picker">
<BasicDropdown @horizontalPosition="auto-left" @verticalPosition="below" as |D|>
<D.Trigger
@htmlTag="button"
class="button is-transparent namespace-picker-trigger has-current-color"
data-test-namespace-toggle
>
{{yield}}
{{#if this.namespaceDisplay}}
<span class="namespace-name">{{this.namespaceDisplay}}</span>
{{else}}
<span class="namespace-name is-hidden-tablet">/ (Root)</span>
{{/if}}
<Chevron @direction="down" @class="has-text-white auto-width is-status-chevron" />
</D.Trigger>
<D.Content @defaultClass="namespace-picker-content">
<div class="namespace-picker" ...attributes>
<BasicDropdown @horizontalPosition="left" @verticalPosition="above" as |D|>
<D.Trigger
@htmlTag="button"
class="button is-transparent namespace-picker-trigger has-current-color"
data-test-namespace-toggle
>
<div class="is-flex-center">
<Icon @name="org" />
<span class="namespace-name">{{this.namespaceDisplay}}</span>
</div>
<Icon @name="caret" />
</D.Trigger>
<D.Content @defaultClass="namespace-picker-content">
<div class="is-mobile level-left">
<h5 class="list-header">Current namespace</h5>
</div>
<div class="namespace-header-bar level is-mobile">
<div class="level-left">
<header>
<div class="level is-mobile namespace-link">
<span class="level-left" data-test-current-namespace>
{{if this.namespacePath (concat this.namespacePath "/") "root"}}
</span>
</div>
</header>
</div>
</div>
<div class="namespace-list {{if this.isAnimating 'animated-list'}}">
<div class="is-mobile level-left">
{{#unless this.isUserRootNamespace}}
<NamespaceLink
@ -27,51 +34,23 @@
(object-at (dec 2 this.menuLeaves.length) this.lastMenuLeaves)
this.auth.authData.userRootNamespace
}}
@class="namespace-link is-current button is-ghost icon"
@class="namespace-link is-current button is-transparent icon"
>
<Chevron @direction="left" @class="has-text-grey" />
</NamespaceLink>
{{/unless}}
<h5 class="list-header">Current namespace</h5>
<h5 class="list-header">Namespaces</h5>
</div>
<div class="namespace-header-bar level is-mobile">
<div class="level-left">
<header>
<div class="level is-mobile namespace-link">
<span class="level-left" data-test-current-namespace>
{{if this.namespacePath (concat this.namespacePath "/") "root"}}
</span>
<span class="level-right">
<Icon @name="check-circle" class="has-text-success" />
</span>
</div>
</header>
{{#if (includes "" this.lastMenuLeaves)}}
{{! leaf is '' which is the root namespace, and then we need to iterate the root leaves }}
<div class="leaf-panel {{if (eq '' this.currentLeaf) 'leaf-panel-current' 'leaf-panel-left'}} ">
{{#each this.rootLeaves as |rootLeaf|}}
<NamespaceLink @targetNamespace={{rootLeaf}} @class="namespace-link" @showLastSegment={{true}} />
{{/each}}
</div>
</div>
<div class="namespace-list {{if this.isAnimating 'animated-list'}}">
<div class="is-mobile level-left">
{{#unless this.isUserRootNamespace}}
<NamespaceLink
@targetNamespace={{or
(object-at (dec 2 this.menuLeaves.length) this.lastMenuLeaves)
this.auth.authData.userRootNamespace
}}
@class="namespace-link is-current button is-ghost icon"
>
<Chevron @direction="left" @class="has-text-grey" />
</NamespaceLink>
{{/unless}}
<h5 class="list-header">Namespaces</h5>
</div>
{{#if (includes "" this.lastMenuLeaves)}}
{{! leaf is '' which is the root namespace, and then we need to iterate the root leaves }}
<div class="leaf-panel {{if (eq '' this.currentLeaf) 'leaf-panel-current' 'leaf-panel-left'}} ">
{{~#each this.rootLeaves as |rootLeaf|}}
<NamespaceLink @targetNamespace={{rootLeaf}} @class="namespace-link" @showLastSegment={{true}} />
{{/each~}}
</div>
{{/if}}
{{#each this.lastMenuLeaves as |leaf|}}
{{/if}}
{{#each this.lastMenuLeaves as |leaf|}}
{{#if leaf}}
<div
class="leaf-panel
{{if (eq leaf this.currentLeaf) 'leaf-panel-current' 'leaf-panel-left'}}
@ -79,32 +58,38 @@
{{if (and (not this.isAdding) (eq leaf this.changedLeaf)) 'leaf-panel-exiting'}}
"
>
{{~#each-in (get this.namespaceTree leaf) as |leafName|}}
{{#each-in (get this.namespaceTree leaf) as |leafName|}}
<NamespaceLink
@targetNamespace={{concat leaf "/" leafName}}
@class="namespace-link"
@showLastSegment={{true}}
/>
{{/each-in~}}
</div>
{{/each}}
{{#if this.canList}}
<div class="leaf-panel leaf-panel-current">
<LinkTo @route="vault.cluster.access.namespaces" class="is-block namespace-link namespace-manage-link">
<div class="level is-mobile">
<span class="level-left">Manage namespaces</span>
<span class="level-right">
<button type="button" class="button is-ghost icon" onclick={{action "refreshNamespaceList"}}>
<Icon @name="reload" class="has-text-grey" />
</button>
</span>
</div>
</LinkTo>
{{/each-in}}
</div>
{{/if}}
</div>
</D.Content>
</BasicDropdown>
</div>
<div class="navbar-separator"></div>
{{/if}}
{{/each}}
{{#if this.canList}}
<div class="leaf-panel leaf-panel-current">
<div class="level">
<span class="level-left">
<LinkTo @route="vault.cluster.access.namespaces" class="is-block namespace-link namespace-manage-link">
Manage Namespaces
</LinkTo>
</span>
<span class="level-right">
<button
type="button"
class="button is-ghost icon has-right-margin-m"
data-test-refresh-namespaces
onclick={{action "refreshNamespaceList"}}
>
<Icon @name="reload" class="has-text-grey" />
</button>
</span>
</div>
</div>
{{/if}}
</div>
</D.Content>
</BasicDropdown>
</div>

View File

@ -1,42 +0,0 @@
<nav class="navbar">
<LinkStatus @status={{this.currentCluster.cluster.hcpLinkStatus}} />
<div class="navbar-actions">
<div class="navbar-brand" data-test-navheader-home>
{{yield (hash home=(component "nav-header/home"))}}
</div>
{{#unless this.navDrawerOpen}}
<button type="button" class="navbar-drawer-toggle is-hidden-tablet" {{action "toggleNavDrawer"}}>
<Icon @name="more-vertical" />
Menu
</button>
{{/unless}}
{{#unless this.hideLinks}}
<div class="navbar-drawer{{if this.navDrawerOpen ' is-active'}}">
<div class="navbar-drawer-scroll">
<div data-test-navheader-main>
{{yield (hash main=(component "nav-header/main") closeDrawer=(action "toggleNavDrawer" false))}}
</div>
<div class="navbar-end" data-test-navheader-items>
{{yield (hash items=(component "nav-header/items") closeDrawer=(action "toggleNavDrawer" false))}}
</div>
</div>
{{#if this.navDrawerOpen}}
<button class="navbar-drawer-toggle is-hidden-tablet" type="button" {{action "toggleNavDrawer" false}}>
<Icon @name="x" />
</button>
{{/if}}
</div>
{{/unless}}
<div
class="navbar-drawer-overlay{{if this.navDrawerOpen ' is-active'}}"
role="button"
onclick={{action "toggleNavDrawer" (not this.navDrawerOpen)}}
></div>
</div>
</nav>
<Console::UiPanel @isFullscreen={{this.consoleFullscreen}} />

View File

@ -8,6 +8,9 @@
</PageHeader>
<div class="box is-sideless has-background-white-bis has-text-grey has-text-centered">
<p>Sorry, we were unable to find any content at <code>{{or this.model.path this.path}}</code>.</p>
<p>Double check the url or go back <HomeLink @text="home" />.</p>
<p>
Double check the url or
<ExternalLink @href="/" @sameTab={{true}}>go back home</ExternalLink>.
</p>
</div>
</div>

View File

@ -1,17 +1,3 @@
{{#if this.showTruncatedNavBar}}
<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 this.activeCluster.unsealed "good" "bad"}}>
<StatusMenu @label="Status" @onLinkClick={{Nav.closeDrawer}} />
</div>
</Nav.items>
</NavHeader>
{{/if}}
{{! bypass container styling }}
{{#if @hasAltContent}}
{{yield (hash altContent=(component "splash-page/splash-content"))}}

View File

@ -1,23 +0,0 @@
<BasicDropdown @horizontalPosition="auto-left" @verticalPosition="below" @renderInPlace={{this.media.isMobile}} as |d|>
<d.Trigger
@htmlTag={{if (eq this.type "replication") "span" "button"}}
class={{if (eq this.type "replication") "" "button is-transparent"}}
>
<span class="status-indicator-color">
<Icon @name={{this.glyphName}} aria-label={{@ariaLabel}} />
</span>
<div class="status-menu-label">
{{@label}}
</div>
<Chevron @direction="down" class="has-text-white is-status-chevron" />
</d.Trigger>
<d.Content @defaultClass={{concat "status-menu-content status-menu-content-" this.type}}>
{{#if (eq this.type "user")}}
{{#if (and this.currentCluster.cluster.name this.auth.currentToken)}}
<AuthInfo @activeClusterName={{this.currentCluster.cluster.name}} @onLinkClick={{fn this.onLinkClick null}} />
{{/if}}
{{else}}
<ClusterInfo @cluster={{this.currentCluster.cluster}} @onLinkClick={{fn this.onLinkClick d}} />
{{/if}}
</d.Content>
</BasicDropdown>

View File

@ -7,9 +7,9 @@
HashiCorp
</span>
<span>
<ExternalLink @href={{changelog-url-for this.activeCluster.leaderNode.version}} class="link has-text-grey">
<ExternalLink @href={{changelog-url-for this.auth.activeCluster.leaderNode.version}} class="link has-text-grey">
Vault
{{this.activeCluster.leaderNode.version}}
{{this.auth.activeCluster.leaderNode.version}}
</ExternalLink>
</span>
{{#if (is-version "OSS")}}
@ -34,4 +34,3 @@
</div>
</div>
{{/if}}
<div id="modal-wormhole"></div>

View File

@ -1,113 +1,5 @@
{{#if this.showNav}}
<NavHeader data-test-header-with-nav @class={{if this.consoleOpen "panel-open"}} as |Nav|>
<Nav.home>
<HomeLink @class="navbar-item has-text-white has-current-color-fill">
<Icon @name="vault-logo" />
</HomeLink>
</Nav.home>
<Nav.main>
<ul class="navbar-sections {{if (has-feature 'Namespaces') 'with-ns-picker'}}">
{{#if (has-feature "Namespaces")}}
<li>
<NamespacePicker @class="navbar-item" @namespace={{this.namespaceQueryParam}} />
</li>
{{/if}}
<li>
<LinkTo
@route="vault.cluster.secrets"
@current-when="vault.cluster.secrets vault.cluster.settings.mount-secret-backend vault.cluster.settings.configure-secret-backend"
class={{if (is-active-route "vault.cluster.secrets") "is-active"}}
{{on "click" Nav.closeDrawer}}
data-test-navbar-item="secrets"
>
Secrets
</LinkTo>
</li>
{{#if (has-permission "access")}}
<li>
<LinkTo
@route={{get (route-params-for "access") "route"}}
@models={{get (route-params-for "access") "models"}}
@current-when="vault.cluster.access vault.cluster.settings.auth"
class={{if (is-active-route "vault.cluster.access") "is-active"}}
{{on "click" Nav.closeDrawer}}
data-test-navbar-item="access"
>
Access
</LinkTo>
</li>
{{/if}}
{{#if (has-permission "policies")}}
<li>
<LinkTo
@route="vault.cluster.policies"
@models={{get (route-params-for "policies") "models"}}
@current-when="vault.cluster.policies vault.cluster.policy"
class={{if (is-active-route (array "vault.cluster.policies" "vault.cluster.policy")) "is-active"}}
{{on "click" Nav.closeDrawer}}
data-test-navbar-item="policies"
>
Policies
</LinkTo>
</li>
{{/if}}
{{#if (has-permission "tools")}}
<li>
<LinkTo
@route="vault.cluster.tools.tool"
@models={{get (route-params-for "tools") "models"}}
class={{if (is-active-route "vault.cluster.tools") "is-active"}}
{{on "click" Nav.closeDrawer}}
>
Tools
</LinkTo>
</li>
{{/if}}
</ul>
</Nav.main>
<Nav.items>
<div class="navbar-separator is-hidden-tablet"></div>
{{! template-lint-disable block-indentation }}
{{#if this.namespaceService.inRootNamespace}}
<div class="navbar-item status-indicator-button" data-status={{if this.activeCluster.unsealed "good" "bad"}}>
<StatusMenu @label="Status" @onLinkClick={{action Nav.closeDrawer}} />
</div>
<div class="navbar-separator is-hidden-mobile"></div>
{{else if (and
(has-permission "clients" routeParams="activity") (not this.cluster.dr.isSecondary) this.auth.currentToken
)}}
<div class="navbar-sections">
<div class={{if (is-active-route "vault.cluster.clients") "is-active"}}>
<LinkTo @route="vault.cluster.clients.dashboard" data-test-navbar-item="metrics">
Client count
</LinkTo>
</div>
</div>
{{/if}}
{{! template-lint-enable block-indentation }}
<div class="navbar-item">
<button
type="button"
class="button is-transparent nav-console-button{{if this.consoleOpen ' popup-open'}}"
{{action (queue (action "toggleConsole") (action Nav.closeDrawer))}}
data-test-console-toggle
>
<Icon @name="terminal-screen" />
<div class="status-menu-label">
Console
</div>
<Chevron @direction="down" class="has-text-white is-status-chevron" />
</button>
</div>
<div
class="navbar-item nav-user-button {{if this.auth.allowExpiration 'may-expire'}}"
data-test-allow-expiration={{this.auth.allowExpiration}}
>
<StatusMenu @type="user" @label="User" @onLinkClick={{action Nav.closeDrawer}} />
</div>
</Nav.items>
</NavHeader>
{{/if}}
<Sidebar::Nav::Cluster />
<LicenseBanners
@expiry={{this.activeCluster.licenseExpiry}}
@autoloaded={{eq this.activeCluster.licenseState "autoloaded"}}
@ -128,18 +20,15 @@
</FlashMessage>
{{/each}}
</div>
{{#if this.currentlyLoading}}
<LogoSplash />
{{#if this.auth.isActiveSession}}
<section class="section">
<div class="container is-widescreen">
<TokenExpireWarning @expirationDate={{this.auth.tokenExpirationDate}}>
{{outlet}}
</TokenExpireWarning>
</div>
</section>
{{else}}
{{#if this.showNav}}
<section class="section">
<div class="container is-widescreen">
<TokenExpireWarning @expirationDate={{this.auth.tokenExpirationDate}}>
{{outlet}}
</TokenExpireWarning>
</div>
</section>
{{else}}
{{outlet}}
{{/if}}
{{outlet}}
{{/if}}

View File

@ -1,75 +1,2 @@
<div class="columns">
<MenuSidebar @title="Access" @class="is-3" @data-test-sidebar={{true}}>
{{#if (has-permission "access" routeParams="methods")}}
<li>
<LinkTo
@route="vault.cluster.access.methods"
data-test-link={{true}}
@current-when="vault.cluster.access.methods vault.cluster.access.method"
>
Auth Methods
</LinkTo>
</li>
{{/if}}
{{#if (has-permission "access" routeParams="mfa")}}
<li>
<LinkTo
@route="vault.cluster.access.mfa.methods"
@current-when="vault.cluster.access.mfa.methods vault.cluster.access.mfa.enforcements vault.cluster.access.mfa.index"
data-test-link="mfa"
>
Multi-factor authentication
</LinkTo>
</li>
{{/if}}
{{#if (has-permission "access" routeParams="entities")}}
<li>
<LinkTo @route="vault.cluster.access.identity" @model="entities" data-test-link={{true}}>
Entities
</LinkTo>
</li>
{{/if}}
{{#if (has-permission "access" routeParams="groups")}}
<li>
<LinkTo @route="vault.cluster.access.identity" @model="groups" data-test-link={{true}}>
Groups
</LinkTo>
</li>
{{/if}}
{{#if (has-permission "access" routeParams="leases")}}
<li>
<LinkTo @route="vault.cluster.access.leases" data-test-link={{true}}>
Leases
</LinkTo>
</li>
{{/if}}
{{#if (and (has-feature "Namespaces") (has-permission "access" routeParams="namespaces"))}}
<li>
<LinkTo @route="vault.cluster.access.namespaces" data-test-link={{true}}>
Namespaces
</LinkTo>
</li>
{{/if}}
{{#if (and (has-feature "Control Groups") (has-permission "access" routeParams="control-groups"))}}
<li>
<LinkTo
@route="vault.cluster.access.control-groups"
data-test-link={{true}}
@current-when="vault.cluster.access.control-groups vault.cluster.access.control-group-accessor vault.cluster.access.control-groups-configure"
>
Control Groups
</LinkTo>
</li>
{{/if}}
{{#if (has-permission "access" routeParams="oidc")}}
<li>
<LinkTo @route="vault.cluster.access.oidc" data-test-link="oidc">
OIDC Provider
</LinkTo>
</li>
{{/if}}
</MenuSidebar>
<div class="column is-9">
{{outlet}}
</div>
</div>
<Sidebar::Nav::Access />
{{outlet}}

View File

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

View File

@ -22,6 +22,11 @@
<LogoEdition aria-label="Sign in with Hashicorp Vault" />
</div>
{{else}}
<div class="is-flex-v-centered has-bottom-margin-xxl">
<div class="brand-icon-large">
<Icon @name="vault" @size="24" @stretched={{true}} />
</div>
</div>
<div class="is-flex-row">
{{#if this.mfaAuthData}}
<button type="button" class="icon-button" {{on "click" (fn (mut this.mfaAuthData) null)}}>

View File

@ -23,8 +23,5 @@
</ul>
</nav>
</div>
{{#if this.currentlyLoading}}
<LayoutLoading />
{{else}}
{{outlet}}
{{/if}}
{{outlet}}

View File

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

View File

@ -1,51 +1,2 @@
{{#if
(and
(has-feature "Sentinel") (or (has-permission "policies" routeParams="rgp") (has-permission "policies" routeParams="egp"))
)
}}
<div class="columns">
<MenuSidebar @title="Policies" @class="is-3" @data-test-sidebar={{true}}>
{{#if (has-permission "policies" routeParams="acl")}}
<li>
<LinkTo
@route="vault.cluster.policies"
@model="acl"
data-test-link={{true}}
class={{if (is-active-route "vault.cluster.policies" "acl") "is-active"}}
>
ACL Policies
</LinkTo>
</li>
{{/if}}
{{#if (has-permission "policies" routeParams="rgp")}}
<li>
<LinkTo
@route="vault.cluster.policies"
@model="rgp"
data-test-link={{true}}
class={{if (is-active-route "vault.cluster.policies" "rgp") "is-active"}}
>
Role Governing Policies
</LinkTo>
</li>
{{/if}}
{{#if (has-permission "policies" routeParams="egp")}}
<li>
<LinkTo
@route="vault.cluster.policies"
@model="egp"
data-test-link={{true}}
class={{if (is-active-route "vault.cluster.policies" "egp") "is-active"}}
>
Endpoint Governing Policies
</LinkTo>
</li>
{{/if}}
</MenuSidebar>
<div class="column is-9">
{{outlet}}
</div>
</div>
{{else}}
{{outlet}}
{{/if}}
<Sidebar::Nav::Policies />
{{outlet}}

View File

@ -1,45 +1,2 @@
<div class="columns">
<MenuSidebar @title="Policies" @class="is-3" @data-test-sidebar={{true}}>
{{#if (has-permission "policies" routeParams="acl")}}
<li>
<LinkTo
@route="vault.cluster.policies"
@model="acl"
data-test-link={{true}}
class={{if (is-active-route "vault.cluster.policy" "acl") "is-active"}}
>
ACL Policies
</LinkTo>
</li>
{{/if}}
{{#if (has-feature "Sentinel")}}
{{#if (has-permission "policies" routeParams="rgp")}}
<li>
<LinkTo
@route="vault.cluster.policies"
@model="rgp"
data-test-link={{true}}
class={{if (is-active-route "vault.cluster.policy" "rgp") "is-active"}}
>
Role Governing Policies
</LinkTo>
</li>
{{/if}}
{{#if (has-permission "policies" routeParams="egp")}}
<li>
<LinkTo
@route="vault.cluster.policies"
@model="egp"
data-test-link={{true}}
class={{if (is-active-route "vault.cluster.policy" "egp") "is-active"}}
>
Endpoint Governing Policies
</LinkTo>
</li>
{{/if}}
{{/if}}
</MenuSidebar>
<div class="column is-9">
{{outlet}}
</div>
</div>
<Sidebar::Nav::Policies />
{{outlet}}

View File

@ -56,7 +56,7 @@
{{#if this.model.secret}}
<LinkTo @route="vault.cluster.secrets.backend.list-root">Navigate back to the root</LinkTo>.
{{else}}
<HomeLink>Go back home</HomeLink>.
<LinkTo @route="vault.cluster">Go back home</LinkTo>.
{{/if}}
</p>
{{/if}}

View File

@ -1,21 +1,2 @@
<div class="columns">
<MenuSidebar @title="Tools" @class="is-3">
{{#each (tools-actions) as |supportedAction|}}
{{#if (has-permission "tools" routeParams=supportedAction)}}
<li>
<LinkTo
@route="vault.cluster.tools.tool"
@model={{supportedAction}}
class={{if (eq supportedAction this.selectedAction) "is-active"}}
data-test-tools-action-link={{supportedAction}}
>
{{capitalize supportedAction}}
</LinkTo>
</li>
{{/if}}
{{/each}}
</MenuSidebar>
<div class="column is-9">
<ToolActionsForm @selectedAction={{this.selectedAction}} />
</div>
</div>
<Sidebar::Nav::Tools />
<ToolActionsForm @selectedAction={{this.selectedAction}} />

View File

@ -1,16 +1,4 @@
{{#if this.showLicenseError}}
<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 this.activeCluster.unsealed "good" "bad"}}>
<StatusMenu @label="Status" @onLinkClick={{action Nav.closeDrawer}} />
</div>
</Nav.items>
</NavHeader>
<div class="section is-flex-v-centered-tablet is-flex-1 is-fullwidth">
<div class="columns is-centered is-gapless is-fullwidth">
<EmptyState

View File

@ -1,34 +1,45 @@
<NavHeader data-test-header-without-nav as |Nav|>
<Nav.home>
<HomeLink @class="navbar-item splash-page-logo">
<LogoEdition />
</HomeLink>
</Nav.home>
</NavHeader>
<section class="section">
<div class="container">
{{#if (eq this.model.httpStatus 404)}}
<NotFound @model={{this.model}} />
{{else}}
<PageHeader as |p|>
<p.levelLeft>
<h1 class="title is-3 has-text-grey">
{{#if (eq this.model.httpStatus 403)}}
Not authorized
{{else}}
Error
{{/if}}
</h1>
</p.levelLeft>
</PageHeader>
<BlockError>
{{#if this.model.message}}
<p>{{this.model.message}}</p>
{{/if}}
{{#each this.model.errors as |error|}}
<p>{{error}}</p>
{{/each}}
</BlockError>
{{/if}}
<div class="is-flex-1 is-flex-v-centered">
<div class="empty-state-content">
<div class="is-flex-v-centered has-bottom-margin-xxl">
<div class="brand-icon-large">
<Icon @name="vault" @size="24" @stretched={{true}} />
</div>
</div>
<div class="is-flex-center">
<div class="error-icon">
<Icon @name="help" @size="24" class="has-text-grey" @stretched={{true}} />
</div>
<div class="has-left-margin-s">
<h1 class="is-size-4 has-text-semibold has-text-grey has-line-height-1">
{{#if (eq this.model.httpStatus 403)}}
Not authorized
{{else if (eq this.model.httpStatus 404)}}
Page not found
{{else}}
Error
{{/if}}
</h1>
<p class="has-text-grey is-size-8">Error {{this.model.httpStatus}}</p>
</div>
</div>
<p class="has-text-grey has-top-margin-m has-bottom-padding-l has-border-bottom-light" data-test-error-description>
{{#if (eq this.model.httpStatus 404)}}
Sorry, we were unable to find any content at that URL. Double check it or go back home.
{{else}}
{{this.model.message}}
{{join ". " this.model.errors}}
{{/if}}
</p>
<div class="is-flex-between has-top-margin-s">
<ExternalLink @href="/" @sameTab={{true}} class="is-no-underline is-flex-center has-text-semibold">
<Chevron @direction="left" />
Go home
</ExternalLink>
<DocLink @path="/vault/api-docs#http-status-codes">
Learn more
</DocLink>
</div>
</div>
</section>
</div>

View File

@ -1,14 +1 @@
<header>
<nav class="navbar is-grouped-split">
<div class="navbar-brand">
<HomeLink @class="navbar-item has-text-white has-current-color-fill">
<Icon @name="vault-logo" />
</HomeLink>
</div>
</nav>
</header>
<section class="section">
<div class="container is-widescreen">
<NotFound @model={{this.model}} />
</div>
</section>
<NotFound @model={{this.model}} />

View File

@ -8,7 +8,6 @@
const EmberApp = require('ember-cli/lib/broccoli/ember-app');
const config = require('./config/environment')();
const nodeSass = require('node-sass');
const environment = EmberApp.env();
const isProd = environment === 'production';
@ -44,9 +43,18 @@ const appConfig = {
enabled: !isProd,
},
sassOptions: {
implementation: nodeSass,
sourceMap: false,
onlyIncluded: true,
precision: 4,
includePaths: [
'./node_modules/@hashicorp/design-system-components/app/styles',
'./node_modules/@hashicorp/design-system-tokens/dist/products/css',
],
},
minifyCSS: {
options: {
advanced: false,
},
},
autoprefixer: {
enabled: isTest || isProd,

View File

@ -1,18 +1,3 @@
{{! DR Secondary has a different Nav Header with access only to the Status menu }}
{{#if this.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 this.data.unsealed "good" "bad"}}>
<StatusMenu @label="Status" @onLinkClick={{action Nav.closeDrawer}} />
</div>
</Nav.items>
</NavHeader>
{{/if}}
<PageHeader as |p|>
<p.top>
{{! template-lint-configure simple-unless "warn" }}
@ -55,14 +40,10 @@
</ul>
{{else}}
<ul>
<LinkTo @route="vault.cluster.replication-dr-promote.details" @activeClass="is-active">
<LinkTo @route="vault.cluster.replication-dr-promote.details">
Details
</LinkTo>
<LinkTo
@route="vault.cluster.replication-dr-promote"
@activeClass="is-active"
@current-when="vault.cluster.replication-dr-promote.index"
>
<LinkTo @route="vault.cluster.replication-dr-promote" @current-when="vault.cluster.replication-dr-promote.index">
Manage
</LinkTo>
</ul>

View File

@ -98,7 +98,12 @@
</div>
<div class="level-right">
{{#if this.replicationDisabled}}
<LinkTo @route="mode.index" @models={{array this.cluster.name this.mode}} class="button is-primary">
<LinkTo
@route="mode.index"
@models={{array this.cluster.name this.mode}}
class="button is-primary"
data-test-replication-promote-secondary
>
Enable
</LinkTo>
{{else}}

View File

@ -21,8 +21,7 @@ export default Route.extend(ClusterRoute, {
},
model() {
const activeClusterId = this.auth.activeCluster;
return this.store.peekRecord('cluster', activeClusterId);
return this.auth.activeCluster;
},
afterModel(model) {

View File

@ -1,7 +1,6 @@
<section class="section">
<div class="container is-widescreen">
{{#if this.model.replicationIsInitializing}}
<nav class="navbar"></nav>
<LayoutLoading />
{{else}}
{{#if (eq this.model.mode "unsupported")}}

View File

@ -1,7 +1,6 @@
<section class="section">
<div class="container is-widescreen">
{{#if (and (eq this.model.drMode "secondary") (eq this.model.drModeInit "primary"))}}
<nav class="navbar" aria-label="loading nav"></nav>
<LayoutLoading />
{{else}}
{{#if this.model.replicationAttrs.replicationEnabled}}

Some files were not shown because too many files have changed in this diff Show More