mirror of
https://github.com/hashicorp/vault.git
synced 2026-02-03 20:40:45 -05:00
* update namespace empty state add in refresh button to empty state and update tests update button design * update copy * update when exit button is shown * update css class * revert state changes Co-authored-by: lane-wetmore <lane.wetmore@hashicorp.com>
This commit is contained in:
parent
c2034cb08a
commit
eb1d3edfb0
6 changed files with 178 additions and 99 deletions
|
|
@ -16,90 +16,150 @@
|
|||
@breadcrumbs={{array (hash label="Vault" route="vault.cluster.dashboard" icon="vault") (hash label="Namespaces")}}
|
||||
/>
|
||||
</:breadcrumbs>
|
||||
<:badges>
|
||||
<Hds::Badge @icon="org" @text={{this.namespacePath}} data-test-badge="namespace-path" />
|
||||
</:badges>
|
||||
<:actions>
|
||||
{{#unless @model.namespaces}}
|
||||
<Hds::Button
|
||||
class="has-right-margin-4"
|
||||
@color="secondary"
|
||||
@icon="bulb"
|
||||
@text="New to Namespaces?"
|
||||
{{on "click" this.enterGuidedStart}}
|
||||
data-test-button="guided-start"
|
||||
/>
|
||||
<Hds::Button
|
||||
class="has-right-margin-4"
|
||||
@icon="plus"
|
||||
@route="vault.cluster.access.namespaces.create"
|
||||
@text="Create namespace"
|
||||
data-test-button="create-namespace"
|
||||
/>
|
||||
{{/unless}}
|
||||
</:actions>
|
||||
</Page::Header>
|
||||
|
||||
<Toolbar>
|
||||
<ToolbarFilters>
|
||||
<FilterInputExplicit
|
||||
@query={{@model.pageFilter}}
|
||||
@placeholder="Search"
|
||||
@handleSearch={{this.handleSearch}}
|
||||
@handleInput={{this.handleInput}}
|
||||
@handleKeyDown={{this.handleKeyDown}}
|
||||
/>
|
||||
</ToolbarFilters>
|
||||
<ToolbarActions>
|
||||
<Hds::Button
|
||||
class="has-right-margin-4"
|
||||
@color="secondary"
|
||||
@icon="reload"
|
||||
@iconPosition="trailing"
|
||||
@text="Refresh list"
|
||||
{{on "click" this.refreshNamespaceList}}
|
||||
data-test-button="refresh-namespace-list"
|
||||
/>
|
||||
<ToolbarLink @route="vault.cluster.access.namespaces.create" @type="add" data-test-link-to="create-namespace">
|
||||
Create namespace
|
||||
</ToolbarLink>
|
||||
</ToolbarActions>
|
||||
</Toolbar>
|
||||
|
||||
<ListView
|
||||
@items={{@model.namespaces}}
|
||||
@itemNoun="namespace"
|
||||
@paginationRouteName="vault.cluster.access.namespaces"
|
||||
@onPageChange={{this.handlePageChange}}
|
||||
as |list|
|
||||
>
|
||||
{{#if @model.namespaces.length}}
|
||||
<ListItem as |Item|>
|
||||
<Item.content>
|
||||
{{list.item.id}}
|
||||
</Item.content>
|
||||
<Item.menu>
|
||||
<Hds::Dropdown @isInline={{true}} @listPosition="bottom-right" as |dd|>
|
||||
<dd.ToggleIcon
|
||||
@icon="more-horizontal"
|
||||
@text="More options"
|
||||
@hasChevron={{false}}
|
||||
data-test-popup-menu-trigger
|
||||
/>
|
||||
{{#let (concat this.namespace.path (if this.namespace.path "/") list.item.id) as |targetNamespace|}}
|
||||
{{#if (includes targetNamespace this.namespace.accessibleNamespaces)}}
|
||||
<dd.Interactive
|
||||
{{on "click" (fn this.switchNamespace targetNamespace)}}
|
||||
data-test-popup-menu="switch"
|
||||
>Switch to namespace</dd.Interactive>
|
||||
{{/if}}
|
||||
{{/let}}
|
||||
<dd.Interactive
|
||||
@color="critical"
|
||||
{{on "click" (fn (mut this.nsToDelete) list.item)}}
|
||||
data-test-popup-menu="delete"
|
||||
>Delete</dd.Interactive>
|
||||
</Hds::Dropdown>
|
||||
{{#if (eq this.nsToDelete list.item)}}
|
||||
<ConfirmModal
|
||||
@color="critical"
|
||||
@onClose={{fn (mut this.nsToDelete) null}}
|
||||
@onConfirm={{fn this.deleteNamespace list.item}}
|
||||
@confirmTitle="Delete this namespace?"
|
||||
@confirmMessage="Any engines or mounts in this namespace will also be removed."
|
||||
/>
|
||||
{{/if}}
|
||||
</Item.menu>
|
||||
</ListItem>
|
||||
{{else}}
|
||||
<list.empty>
|
||||
<Hds::Link::Standalone
|
||||
@icon="learn-link"
|
||||
@text="Secure multi-tenancy with namespaces tutorial"
|
||||
@href={{doc-link "/vault/tutorials/enterprise/namespaces"}}
|
||||
{{#if @model.namespaces}}
|
||||
<Toolbar>
|
||||
<ToolbarFilters>
|
||||
<FilterInputExplicit
|
||||
@query={{@model.pageFilter}}
|
||||
@placeholder="Search"
|
||||
@handleSearch={{this.handleSearch}}
|
||||
@handleInput={{this.handleInput}}
|
||||
@handleKeyDown={{this.handleKeyDown}}
|
||||
/>
|
||||
</list.empty>
|
||||
</ToolbarFilters>
|
||||
<ToolbarActions>
|
||||
<Hds::Button
|
||||
class="has-right-margin-4"
|
||||
@color="secondary"
|
||||
@icon="reload"
|
||||
@iconPosition="trailing"
|
||||
@text="Refresh list"
|
||||
{{on "click" this.refreshNamespaceList}}
|
||||
data-test-button="refresh-namespace-list"
|
||||
/>
|
||||
<ToolbarLink @route="vault.cluster.access.namespaces.create" @type="add" data-test-link-to="create-namespace">
|
||||
Create namespace
|
||||
</ToolbarLink>
|
||||
</ToolbarActions>
|
||||
</Toolbar>
|
||||
|
||||
<ListView
|
||||
@items={{@model.namespaces}}
|
||||
@itemNoun="namespace"
|
||||
@paginationRouteName="vault.cluster.access.namespaces"
|
||||
@onPageChange={{this.handlePageChange}}
|
||||
as |list|
|
||||
>
|
||||
{{#if @model.namespaces.length}}
|
||||
<ListItem as |Item|>
|
||||
<Item.content>
|
||||
{{list.item.id}}
|
||||
</Item.content>
|
||||
<Item.menu>
|
||||
<Hds::Dropdown @isInline={{true}} @listPosition="bottom-right" as |dd|>
|
||||
<dd.ToggleIcon
|
||||
@icon="more-horizontal"
|
||||
@text="More options"
|
||||
@hasChevron={{false}}
|
||||
data-test-popup-menu-trigger
|
||||
/>
|
||||
{{#let (concat this.namespace.path (if this.namespace.path "/") list.item.id) as |targetNamespace|}}
|
||||
{{#if (includes targetNamespace this.namespace.accessibleNamespaces)}}
|
||||
<dd.Interactive
|
||||
{{on "click" (fn this.switchNamespace targetNamespace)}}
|
||||
data-test-popup-menu="switch"
|
||||
>Switch to namespace</dd.Interactive>
|
||||
{{/if}}
|
||||
{{/let}}
|
||||
<dd.Interactive
|
||||
@color="critical"
|
||||
{{on "click" (fn (mut this.nsToDelete) list.item)}}
|
||||
data-test-popup-menu="delete"
|
||||
>Delete</dd.Interactive>
|
||||
</Hds::Dropdown>
|
||||
{{#if (eq this.nsToDelete list.item)}}
|
||||
<ConfirmModal
|
||||
@color="critical"
|
||||
@onClose={{fn (mut this.nsToDelete) null}}
|
||||
@onConfirm={{fn this.deleteNamespace list.item}}
|
||||
@confirmTitle="Delete this namespace?"
|
||||
@confirmMessage="Any engines or mounts in this namespace will also be removed."
|
||||
/>
|
||||
{{/if}}
|
||||
</Item.menu>
|
||||
</ListItem>
|
||||
{{else}}
|
||||
<list.empty>
|
||||
<Hds::Link::Standalone
|
||||
@icon="learn-link"
|
||||
@text="Secure multi-tenancy with namespaces tutorial"
|
||||
@href={{doc-link "/vault/tutorials/enterprise/namespaces"}}
|
||||
/>
|
||||
</list.empty>
|
||||
{{/if}}
|
||||
</ListView>
|
||||
{{else}}
|
||||
{{#if this.showSetupAlert}}
|
||||
<Hds::Alert @type="inline" class="top-margin-32" as |A|>
|
||||
<A.Title>Your current setup is 1 namespace.</A.Title>
|
||||
<A.Description>Based on your answer about your security policy in the Guided start, no new namespaces are required.</A.Description>
|
||||
</Hds::Alert>
|
||||
{{/if}}
|
||||
</ListView>
|
||||
|
||||
<Hds::Card::Container @hasBorder={{true}} class="has-padding-l top-margin-32">
|
||||
<Hds::ApplicationState @align="left" class="is-marginless" as |A|>
|
||||
<A.Header
|
||||
@title="No namespaces yet"
|
||||
@titleTag="h2"
|
||||
@icon="skip"
|
||||
class="align-items-end"
|
||||
data-test-empty-state-title
|
||||
/>
|
||||
<A.Body @text="Your namespaces will be listed here. Add a namespace to get started." />
|
||||
<A.Footer as |F|>
|
||||
<F.Button
|
||||
@color="secondary"
|
||||
@icon="reload"
|
||||
@text="Refresh"
|
||||
{{on "click" this.refreshNamespaceList}}
|
||||
data-test-button="refresh-namespace-list"
|
||||
/>
|
||||
<F.LinkStandalone
|
||||
@icon="learn-link"
|
||||
@text="Secure multi-tenancy with namespaces tutorial"
|
||||
@href={{doc-link "/vault/tutorials/enterprise/namespaces"}}
|
||||
/>
|
||||
</A.Footer>
|
||||
</Hds::ApplicationState>
|
||||
</Hds::Card::Container>
|
||||
{{/if}}
|
||||
|
||||
{{/if}}
|
||||
|
||||
{{else}}
|
||||
<UpgradePage @title="Namespaces" @minimumEdition="Vault Enterprise Pro" />
|
||||
{{/if}}
|
||||
|
|
@ -10,6 +10,7 @@ import Component from '@glimmer/component';
|
|||
import keys from 'core/utils/keys';
|
||||
|
||||
import type ApiService from 'vault/services/api';
|
||||
import type FlagsService from 'vault/services/flags';
|
||||
import type FlashMessageService from 'vault/services/flash-messages';
|
||||
import type NamespaceService from 'vault/services/namespace';
|
||||
import type RouterService from '@ember/routing/router-service';
|
||||
|
|
@ -45,6 +46,7 @@ interface NamespaceModel {
|
|||
export default class PageNamespacesComponent extends Component<Args> {
|
||||
@service declare readonly api: ApiService;
|
||||
@service declare readonly router: RouterService;
|
||||
@service declare readonly flags: FlagsService;
|
||||
@service declare readonly flashMessages: FlashMessageService;
|
||||
@service declare namespace: NamespaceService;
|
||||
|
||||
|
|
@ -53,6 +55,7 @@ export default class PageNamespacesComponent extends Component<Args> {
|
|||
// browser query param to prevent unnecessary re-renders.
|
||||
@tracked query;
|
||||
@tracked nsToDelete = null;
|
||||
@tracked showSetupAlert = false;
|
||||
@tracked hasDismissedWizard = false;
|
||||
|
||||
wizardId = 'namespace';
|
||||
|
|
@ -68,6 +71,21 @@ export default class PageNamespacesComponent extends Component<Args> {
|
|||
}
|
||||
}
|
||||
|
||||
// show the full available namespace path e.g. "root/ns1/child2", "admin/ns1/child2"
|
||||
get namespacePath() {
|
||||
if (this.namespace.inRootNamespace) {
|
||||
return 'root';
|
||||
}
|
||||
|
||||
// For nested namespaces, show "root/" prefix if not HVD managed and no separate user root
|
||||
if (!this.namespace.userRootNamespace && !this.flags.isHvdManaged) {
|
||||
return `root/${this.namespace.path}`;
|
||||
}
|
||||
|
||||
// If there is a userRootNamespace or it is HVD managed, then the path alone will suffice
|
||||
return this.namespace.path;
|
||||
}
|
||||
|
||||
get showWizard() {
|
||||
// Show when there are no existing namespaces and it is not in a dismissed state
|
||||
return !this.hasDismissedWizard && !this.args.model.namespaces?.length;
|
||||
|
|
@ -123,6 +141,11 @@ export default class PageNamespacesComponent extends Component<Args> {
|
|||
}
|
||||
}
|
||||
|
||||
@action
|
||||
enterGuidedStart() {
|
||||
this.hasDismissedWizard = false;
|
||||
}
|
||||
|
||||
@action handlePageChange() {
|
||||
this.args.onRefresh();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,9 +17,9 @@
|
|||
<Wizard::Namespaces::Welcome />
|
||||
</:welcome>
|
||||
<:exit>
|
||||
{{#unless (eq this.wizardState.creationMethod this.methods.UI)}}
|
||||
{{#if this.shouldShowExitButton}}
|
||||
<Hds::Button @text={{this.exitText}} @color="secondary" {{on "click" this.onDismiss}} />
|
||||
{{/unless}}
|
||||
{{/if}}
|
||||
</:exit>
|
||||
<:submit>
|
||||
{{#if (eq this.wizardState.securityPolicyChoice this.policy.FLEXIBLE)}}
|
||||
|
|
|
|||
|
|
@ -72,9 +72,17 @@ export default class WizardNamespacesWizardComponent extends Component<Args> {
|
|||
}
|
||||
}
|
||||
|
||||
get isFinalStep() {
|
||||
return this.currentStep === this.steps.length - 1;
|
||||
}
|
||||
|
||||
get shouldShowExitButton() {
|
||||
// Show exit button unless we're on the final step with UI creation method
|
||||
return !(this.wizardState.creationMethod === CreationMethod.UI && this.isFinalStep);
|
||||
}
|
||||
|
||||
get exitText() {
|
||||
return this.currentStep === this.steps.length - 1 &&
|
||||
this.wizardState.securityPolicyChoice === SecurityPolicy.STRICT
|
||||
return this.isFinalStep && this.wizardState.securityPolicyChoice === SecurityPolicy.STRICT
|
||||
? 'Done & Exit'
|
||||
: 'Exit';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ export default class WizardNamespacesStep3 extends Component<Args> {
|
|||
icon: 'sidebar',
|
||||
label: CreationMethod.UI,
|
||||
description:
|
||||
'Apply changes immediately. Note: Changes made here may be overwritten if you also use Infrastructure as Code (Terraform).',
|
||||
'Apply changes immediately. Note: Changes made in the UI will be overwritten by any future updates made via Infrastructure as Code (Terraform).',
|
||||
},
|
||||
];
|
||||
tabOptions = ['API', 'CLI'];
|
||||
|
|
|
|||
|
|
@ -74,21 +74,9 @@ module('Acceptance | Enterprise | /access/namespaces', function (hooks) {
|
|||
const testNS = 'test-create-ns-ui';
|
||||
|
||||
// Verify test-create-ns does not exist in the Manage Namespace page
|
||||
await fillIn(GENERAL.filterInputExplicit, testNS);
|
||||
await click(GENERAL.button('Search'));
|
||||
await waitFor(GENERAL.emptyStateTitle, {
|
||||
timeout: 2000,
|
||||
timeoutMessage: 'timed out waiting for empty state title to render',
|
||||
});
|
||||
assert
|
||||
.dom(GENERAL.emptyStateTitle)
|
||||
.hasText(
|
||||
'No namespaces yet',
|
||||
'Empty state is displayed when searching for the namespace we have created in the UI but have not refreshed the list yet'
|
||||
);
|
||||
|
||||
// Create a new namespace in the UI
|
||||
await click(GENERAL.linkTo('create-namespace'));
|
||||
await click(GENERAL.button('create-namespace'));
|
||||
await fillIn(GENERAL.inputByAttr('path'), testNS);
|
||||
await click(GENERAL.submitButton);
|
||||
|
||||
|
|
@ -113,11 +101,11 @@ module('Acceptance | Enterprise | /access/namespaces', function (hooks) {
|
|||
// Setup: Create namespace(s) via the CLI
|
||||
const testNS = 'asdf';
|
||||
await runCmd(createNS(testNS), false);
|
||||
await click(GENERAL.button('refresh-namespace-list'));
|
||||
|
||||
// Search for created namespace// Enter search text
|
||||
await fillIn(GENERAL.filterInputExplicit, testNS);
|
||||
await click(GENERAL.button('Search'));
|
||||
await click(GENERAL.button('refresh-namespace-list'));
|
||||
|
||||
// Verify the menu options
|
||||
await waitFor(GENERAL.menuTrigger, {
|
||||
|
|
@ -135,11 +123,11 @@ module('Acceptance | Enterprise | /access/namespaces', function (hooks) {
|
|||
// Setup: Create namespace(s) via the CLI
|
||||
const testNS = 'test-create-ns-switch';
|
||||
await runCmd(createNS(testNS), false);
|
||||
await click(GENERAL.button('refresh-namespace-list'));
|
||||
|
||||
// Search for created namespace
|
||||
await fillIn(GENERAL.filterInputExplicit, testNS);
|
||||
await click(GENERAL.button('Search'));
|
||||
await click(GENERAL.button('refresh-namespace-list'));
|
||||
|
||||
// Switch namespace
|
||||
await waitFor(GENERAL.menuTrigger);
|
||||
|
|
|
|||
Loading…
Reference in a new issue