UI: LDAP Hierarchical roles (#28824)

* remove named path adapter extension, add subdirectory query logic to adapter

* add subdirectory route and logic to page::roles component

* fix overview page search select

* breadcrumbs

* update tests and mirage

* revert ss changes

* oops

* cleanup adapter, add _ for private methods

* add acceptance test

* remove type

* add changelog

* add ldap breadcrumb test

* VAULT-31905 link jira

* update breadcrumbs in Edit route

* rename type interfaces
This commit is contained in:
claire bontempo
2024-11-05 16:52:29 -08:00
committed by GitHub
parent 752bb08664
commit 30d4e21e88
36 changed files with 664 additions and 225 deletions

View File

@@ -53,7 +53,10 @@
class="is-flex-half"
/>
<div>
<OverviewCard @cardTitle="Generate credentials" @subText="Quickly generate credentials by typing the role name.">
<OverviewCard
@cardTitle="Generate credentials"
@subText="Quickly generate credentials by typing the role name. Only the engine's top-level roles are listed here."
>
<:content>
<div class="has-top-margin-m is-flex">
<SearchSelect
@@ -61,11 +64,14 @@
@ariaLabel="Role"
@placeholder="Select a role"
@disallowNewItems={{true}}
@options={{@roles}}
@options={{this.roleOptions}}
@selectLimit="1"
@fallbackComponent="input-search"
@onChange={{this.selectRole}}
@renderInPlace={{true}}
@passObject={{true}}
@objectKeys={{array "id" "name" "type"}}
@shouldRenderName={{true}}
/>
<div>
<Hds::Button

View File

@@ -24,15 +24,35 @@ interface Args {
breadcrumbs: Array<Breadcrumb>;
}
interface Option {
id: string;
name: string;
type: string;
}
export default class LdapLibrariesPageComponent extends Component<Args> {
@service('app-router') declare readonly router: RouterService;
@tracked selectedRole: LdapRoleModel | undefined;
get roleOptions() {
const options = this.args.roles
// hierarchical roles are not selectable
.filter((r: LdapRoleModel) => !r.name.endsWith('/'))
// *hack alert* - type is set as id so it renders beside name in search select
// this is to avoid more changes to search select and is okay here because
// we use the type and name to select the item below, not the id
.map((r: LdapRoleModel) => ({ id: r.type, name: r.name, type: r.type }));
return options;
}
@action
selectRole([roleName]: Array<string>) {
const model = this.args.roles.find((role) => role.name === roleName);
this.selectedRole = model;
async selectRole([option]: Array<Option>) {
if (option) {
const { name, type } = option;
const model = this.args.roles.find((role) => role.name === name && role.type === type);
this.selectedRole = model;
}
}
@action

View File

@@ -43,46 +43,59 @@
{{else}}
<div class="has-bottom-margin-s">
{{#each @roles as |role|}}
<ListItem @linkPrefix={{this.mountPoint}} @linkParams={{array "roles.role.details" role.type role.name}} as |Item|>
<ListItem @linkPrefix={{this.mountPoint}} @linkParams={{this.linkParams role}} as |Item|>
<Item.content>
<Icon @name="user" />
<span data-test-role={{role.name}}>{{role.name}}</span>
<span data-test-role="{{role.type}} {{role.name}}">{{role.name}}</span>
<Hds::Badge @text={{role.type}} data-test-role-type-badge={{role.name}} />
</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 />
{{#if role.canEdit}}
<dd.ToggleIcon
@icon="more-horizontal"
@text="More options"
@hasChevron={{false}}
data-test-popup-menu-trigger="{{role.type}} {{role.name}}"
/>
{{#if (this.isHierarchical role.name)}}
<dd.Interactive
data-test-edit
@route="roles.role.edit"
data-test-subdirectory
@route="roles.subdirectory"
@models={{array role.type (concat role.path_to_role role.name)}}
>Content</dd.Interactive>
{{else}}
{{#if role.canEdit}}
<dd.Interactive
data-test-edit
@route="roles.role.edit"
@models={{array role.type role.name}}
>Edit</dd.Interactive>
{{/if}}
{{#if role.canReadCreds}}
<dd.Interactive data-test-get-creds @route="roles.role.credentials" @models={{array role.type role.name}}>
Get credentials
</dd.Interactive>
{{/if}}
{{#if role.canRotateStaticCreds}}
<dd.Interactive
data-test-rotate-creds
@color="critical"
{{on "click" (fn (mut this.credsToRotate) role)}}
>Rotate credentials</dd.Interactive>
{{/if}}
<dd.Interactive
data-test-details
@route="roles.role.details"
{{! this will force the roles.role model hook to fire since we may only have a partial model loaded in the list view }}
@models={{array role.type role.name}}
>Edit</dd.Interactive>
{{/if}}
{{#if role.canReadCreds}}
<dd.Interactive data-test-get-creds @route="roles.role.credentials" @models={{array role.type role.name}}>
Get credentials
</dd.Interactive>
{{/if}}
{{#if role.canRotateStaticCreds}}
<dd.Interactive
data-test-rotate-creds
@color="critical"
{{on "click" (fn (mut this.credsToRotate) role)}}
>Rotate credentials</dd.Interactive>
{{/if}}
<dd.Interactive
data-test-details
@route="roles.role.details"
{{! this will force the roles.role model hook to fire since we may only have a partial model loaded in the list view }}
@models={{array role.type role.name}}
>Details</dd.Interactive>
{{#if role.canDelete}}
<dd.Interactive
data-test-delete
@color="critical"
{{on "click" (fn (mut this.roleToDelete) role)}}
>Delete</dd.Interactive>
>Details</dd.Interactive>
{{#if role.canDelete}}
<dd.Interactive
data-test-delete
@color="critical"
{{on "click" (fn (mut this.roleToDelete) role)}}
>Delete</dd.Interactive>
{{/if}}
{{/if}}
</Hds::Dropdown>
</Item.menu>
@@ -108,7 +121,9 @@
<Hds::Pagination::Numbered
@currentPage={{@roles.meta.currentPage}}
@currentPageSize={{@roles.meta.pageSize}}
@route="roles"
{{! localName will be either "index" or "subdirectory" }}
@route="roles.{{this.router.currentRoute.localName}}"
@models={{@currentRouteParams}}
@showSizeSelector={{false}}
@totalItems={{@roles.meta.filteredTotal}}
@queryFunction={{this.paginationQueryParams}}

View File

@@ -8,6 +8,7 @@ import { service } from '@ember/service';
import { action } from '@ember/object';
import { getOwner } from '@ember/owner';
import errorMessage from 'vault/utils/error-message';
import { tracked } from '@glimmer/tracking';
import type LdapRoleModel from 'vault/models/ldap/role';
import type SecretEngineModel from 'vault/models/secret-engine';
@@ -15,7 +16,6 @@ import type FlashMessageService from 'vault/services/flash-messages';
import type { Breadcrumb, EngineOwner } from 'vault/vault/app-types';
import type RouterService from '@ember/routing/router-service';
import type PaginationService from 'vault/services/pagination';
import { tracked } from '@glimmer/tracking';
interface Args {
roles: Array<LdapRoleModel>;
@@ -29,9 +29,20 @@ export default class LdapRolesPageComponent extends Component<Args> {
@service declare readonly flashMessages: FlashMessageService;
@service('app-router') declare readonly router: RouterService;
@service declare readonly pagination: PaginationService;
@tracked credsToRotate: LdapRoleModel | null = null;
@tracked roleToDelete: LdapRoleModel | null = null;
isHierarchical = (name: string) => name.endsWith('/');
linkParams = (role: LdapRoleModel) => {
const route = this.isHierarchical(role.name) ? 'roles.subdirectory' : 'roles.role.details';
// if there is a path_to_role we're in a subdirectory
// and must concat the ancestors with the leaf name to get the full role path
const roleName = role.path_to_role ? role.path_to_role + role.name : role.name;
return [route, role.type, roleName];
};
get mountPoint(): string {
const owner = getOwner(this) as EngineOwner;
return owner.mountPoint;
@@ -43,7 +54,8 @@ export default class LdapRolesPageComponent extends Component<Args> {
@action
onFilterChange(pageFilter: string) {
this.router.transitionTo('vault.cluster.secrets.backend.ldap.roles', { queryParams: { pageFilter } });
// refresh route, which fires off lazyPaginatedQuery to re-request and filter response
this.router.transitionTo(this.router?.currentRoute?.name, { queryParams: { pageFilter } });
}
@action

View File

@@ -0,0 +1,10 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import Controller from '@ember/controller';
export default class LdapRolesSubdirectoryController extends Controller {
queryParams = ['pageFilter', 'page'];
}

View File

@@ -9,6 +9,8 @@ export default buildRoutes(function () {
this.route('overview');
this.route('roles', function () {
this.route('create');
// wildcard route so we can traverse hierarchical roles i.e. prod/admin/my-role
this.route('subdirectory', { path: '/:type/subdirectory/*path_to_role' });
this.route('role', { path: '/:type/:name' }, function () {
this.route('details');
this.route('edit');

View File

@@ -16,14 +16,14 @@ import type Controller from '@ember/controller';
import type { Breadcrumb } from 'vault/vault/app-types';
import type AdapterError from 'ember-data/adapter'; // eslint-disable-line ember/use-ember-data-rfc-395-imports
interface LdapConfigurationRouteModel {
interface RouteModel {
backendModel: SecretEngineModel;
configModel: LdapConfigModel;
configError: AdapterError;
}
interface LdapConfigurationController extends Controller {
interface RouteController extends Controller {
breadcrumbs: Array<Breadcrumb>;
model: LdapConfigurationRouteModel;
model: RouteModel;
}
@withConfig('ldap/config')
@@ -42,11 +42,7 @@ export default class LdapConfigurationRoute extends Route {
};
}
setupController(
controller: LdapConfigurationController,
resolvedModel: LdapConfigurationRouteModel,
transition: Transition
) {
setupController(controller: RouteController, resolvedModel: RouteModel, transition: Transition) {
super.setupController(controller, resolvedModel, transition);
controller.breadcrumbs = [

View File

@@ -14,7 +14,7 @@ import type LdapConfigModel from 'vault/models/ldap/config';
import type Controller from '@ember/controller';
import type { Breadcrumb } from 'vault/vault/app-types';
interface LdapConfigureController extends Controller {
interface RouteController extends Controller {
breadcrumbs: Array<Breadcrumb>;
}
@@ -30,11 +30,7 @@ export default class LdapConfigureRoute extends Route {
return this.configModel || this.store.createRecord('ldap/config', { backend });
}
setupController(
controller: LdapConfigureController,
resolvedModel: LdapConfigModel,
transition: Transition
) {
setupController(controller: RouteController, resolvedModel: LdapConfigModel, transition: Transition) {
super.setupController(controller, resolvedModel, transition);
controller.breadcrumbs = [

View File

@@ -18,10 +18,10 @@ import type Controller from '@ember/controller';
import type { Breadcrumb } from 'vault/vault/app-types';
import { LdapLibraryAccountStatus } from 'vault/vault/adapters/ldap/library';
interface LdapOverviewController extends Controller {
interface RouteController extends Controller {
breadcrumbs: Array<Breadcrumb>;
}
interface LdapOverviewRouteModel {
interface RouteModel {
backendModel: SecretEngineModel;
promptConfig: boolean;
roles: Array<LdapRoleModel>;
@@ -66,11 +66,7 @@ export default class LdapOverviewRoute extends Route {
});
}
setupController(
controller: LdapOverviewController,
resolvedModel: LdapOverviewRouteModel,
transition: Transition
) {
setupController(controller: RouteController, resolvedModel: RouteModel, transition: Transition) {
super.setupController(controller, resolvedModel, transition);
controller.breadcrumbs = [

View File

@@ -0,0 +1,31 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import Route from '@ember/routing/route';
import { service } from '@ember/service';
import type PaginationService from 'vault/services/pagination';
import type SecretMountPath from 'vault/services/secret-mount-path';
// Base class for roles/index and roles/subdirectory routes
export default class LdapRolesRoute extends Route {
@service declare readonly pagination: PaginationService;
@service declare readonly secretMountPath: SecretMountPath;
lazyQuery(backendId: string, params: { page?: string; pageFilter: string }, adapterOptions: object) {
const page = Number(params.page) || 1;
return this.pagination.lazyPaginatedQuery(
'ldap/role',
{
backend: backendId,
page,
pageFilter: params.pageFilter,
responsePath: 'data.keys',
skipCache: page === 1,
},
{ adapterOptions }
);
}
}

View File

@@ -13,7 +13,7 @@ import type Controller from '@ember/controller';
import type Transition from '@ember/routing/transition';
import type { Breadcrumb } from 'vault/vault/app-types';
interface LdapRolesCreateController extends Controller {
interface RouteController extends Controller {
breadcrumbs: Array<Breadcrumb>;
model: LdapRoleModel;
}
@@ -27,11 +27,7 @@ export default class LdapRolesCreateRoute extends Route {
return this.store.createRecord('ldap/role', { backend });
}
setupController(
controller: LdapRolesCreateController,
resolvedModel: LdapRoleModel,
transition: Transition
) {
setupController(controller: RouteController, resolvedModel: LdapRoleModel, transition: Transition) {
super.setupController(controller, resolvedModel, transition);
controller.breadcrumbs = [

View File

@@ -3,42 +3,32 @@
* SPDX-License-Identifier: BUSL-1.1
*/
import Route from '@ember/routing/route';
import LdapRolesRoute from '../roles';
import { service } from '@ember/service';
import { withConfig } from 'core/decorators/fetch-secrets-engine-config';
import { hash } from 'rsvp';
import type StoreService from 'vault/services/store';
import type PaginationService from 'vault/services/pagination';
import type SecretMountPath from 'vault/services/secret-mount-path';
import type Transition from '@ember/routing/transition';
import type LdapRoleModel from 'vault/models/ldap/role';
import type SecretEngineModel from 'vault/models/secret-engine';
import type Controller from '@ember/controller';
import type { Breadcrumb } from 'vault/vault/app-types';
interface LdapRolesRouteModel {
interface RouteModel {
backendModel: SecretEngineModel;
promptConfig: boolean;
roles: Array<LdapRoleModel>;
}
interface LdapRolesController extends Controller {
breadcrumbs: Array<Breadcrumb>;
model: LdapRolesRouteModel;
pageFilter: string | undefined;
page: number | undefined;
}
interface LdapRolesRouteParams {
page?: string;
pageFilter: string;
interface RouteController extends Controller {
breadcrumbs: Array<Breadcrumb>;
model: RouteModel;
}
@withConfig('ldap/config')
export default class LdapRolesRoute extends Route {
@service declare readonly store: StoreService;
@service declare readonly pagination: PaginationService;
@service declare readonly secretMountPath: SecretMountPath;
export default class LdapRolesIndexRoute extends LdapRolesRoute {
@service declare readonly store: StoreService; // necessary for @withConfig decorator
declare promptConfig: boolean;
@@ -51,39 +41,26 @@ export default class LdapRolesRoute extends Route {
},
};
model(params: LdapRolesRouteParams) {
model(params: { page?: string; pageFilter: string }) {
const backendModel = this.modelFor('application') as SecretEngineModel;
return hash({
backendModel,
promptConfig: this.promptConfig,
roles: this.pagination.lazyPaginatedQuery(
'ldap/role',
{
backend: backendModel.id,
page: Number(params.page) || 1,
pageFilter: params.pageFilter,
responsePath: 'data.keys',
},
{ adapterOptions: { showPartialError: true } }
),
roles: this.lazyQuery(backendModel.id, params, { showPartialError: true }),
});
}
setupController(
controller: LdapRolesController,
resolvedModel: LdapRolesRouteModel,
transition: Transition
) {
setupController(controller: RouteController, resolvedModel: RouteModel, transition: Transition) {
super.setupController(controller, resolvedModel, transition);
controller.breadcrumbs = [
{ label: 'Secrets', route: 'secrets', linkExternal: true },
{ label: resolvedModel.backendModel.id, route: 'overview', model: resolvedModel.backend },
{ label: resolvedModel.backendModel.id, route: 'overview' },
{ label: 'Roles' },
];
}
resetController(controller: LdapRolesController, isExiting: boolean) {
resetController(controller: RouteController, isExiting: boolean) {
if (isExiting) {
controller.set('pageFilter', undefined);
controller.set('page', undefined);

View File

@@ -6,19 +6,17 @@
import Route from '@ember/routing/route';
import { service } from '@ember/service';
import { ModelFrom } from 'vault/vault/route';
import type Store from '@ember-data/store';
import type SecretMountPath from 'vault/services/secret-mount-path';
interface LdapRoleRouteParams {
name: string;
type: string;
}
export type LdapRolesRoleRouteModel = ModelFrom<LdapRolesRoleRoute>;
export default class LdapRoleRoute extends Route {
export default class LdapRolesRoleRoute extends Route {
@service declare readonly store: Store;
@service declare readonly secretMountPath: SecretMountPath;
model(params: LdapRoleRouteParams) {
model(params: { name: string; type: string }) {
const backend = this.secretMountPath.currentPath;
const { name, type } = params;
return this.store.queryRecord('ldap/role', { backend, name, type });

View File

@@ -5,6 +5,7 @@
import Route from '@ember/routing/route';
import { service } from '@ember/service';
import { ldapBreadcrumbs } from 'ldap/utils/ldap-breadcrumbs';
import type Store from '@ember-data/store';
import type LdapRoleModel from 'vault/models/ldap/role';
@@ -13,7 +14,7 @@ import type Transition from '@ember/routing/transition';
import type { Breadcrumb } from 'vault/vault/app-types';
import type AdapterError from 'ember-data/adapter'; // eslint-disable-line ember/use-ember-data-rfc-395-imports
export interface LdapStaticRoleCredentials {
export interface StaticCredentials {
dn: string;
last_vault_rotation: string;
password: string;
@@ -23,7 +24,7 @@ export interface LdapStaticRoleCredentials {
username: string;
type: string;
}
export interface LdapDynamicRoleCredentials {
export interface DynamicCredentials {
distinguished_names: Array<string>;
password: string;
username: string;
@@ -32,13 +33,13 @@ export interface LdapDynamicRoleCredentials {
renewable: boolean;
type: string;
}
interface LdapRoleCredentialsRouteModel {
credentials: undefined | LdapStaticRoleCredentials | LdapDynamicRoleCredentials;
interface RouteModel {
credentials: undefined | StaticCredentials | DynamicCredentials;
error: undefined | AdapterError;
}
interface LdapRoleCredentialsController extends Controller {
interface RouteController extends Controller {
breadcrumbs: Array<Breadcrumb>;
model: LdapRoleCredentialsRouteModel;
model: RouteModel;
}
export default class LdapRoleCredentialsRoute extends Route {
@@ -53,19 +54,16 @@ export default class LdapRoleCredentialsRoute extends Route {
return { error };
}
}
setupController(
controller: LdapRoleCredentialsController,
resolvedModel: LdapRoleCredentialsRouteModel,
transition: Transition
) {
setupController(controller: RouteController, resolvedModel: RouteModel, transition: Transition) {
super.setupController(controller, resolvedModel, transition);
const role = this.modelFor('roles.role') as LdapRoleModel;
controller.breadcrumbs = [
{ label: 'Secrets', route: 'secrets', linkExternal: true },
{ label: role.backend, route: 'overview' },
{ label: 'roles', route: 'roles' },
{ label: role.name, route: 'roles.role' },
{ label: 'credentials' },
{ label: 'Roles', route: 'roles' },
...ldapBreadcrumbs(role.name, role.type, role.backend),
{ label: 'Credentials' },
];
}
}

View File

@@ -4,29 +4,31 @@
*/
import Route from '@ember/routing/route';
import { service } from '@ember/service';
import { ldapBreadcrumbs } from 'ldap/utils/ldap-breadcrumbs';
import type LdapRoleModel from 'vault/models/ldap/role';
import type Controller from '@ember/controller';
import type Transition from '@ember/routing/transition';
import type { Breadcrumb } from 'vault/vault/app-types';
import type Controller from '@ember/controller';
import type LdapRoleModel from 'vault/models/ldap/role';
import type SecretMountPath from 'vault/services/secret-mount-path';
import type Transition from '@ember/routing/transition';
interface LdapRoleDetailsController extends Controller {
interface RouteController extends Controller {
breadcrumbs: Array<Breadcrumb>;
model: LdapRoleModel;
}
export default class LdapRoleEditRoute extends Route {
setupController(
controller: LdapRoleDetailsController,
resolvedModel: LdapRoleModel,
transition: Transition
) {
export default class LdapRolesRoleDetailsRoute extends Route {
@service declare readonly secretMountPath: SecretMountPath;
setupController(controller: RouteController, resolvedModel: LdapRoleModel, transition: Transition) {
super.setupController(controller, resolvedModel, transition);
controller.breadcrumbs = [
{ label: 'Secrets', route: 'secrets', linkExternal: true },
{ label: resolvedModel.backend, route: 'overview' },
{ label: 'roles', route: 'roles' },
{ label: resolvedModel.name },
{ label: 'Roles', route: 'roles' },
...ldapBreadcrumbs(resolvedModel.name, resolvedModel.type, this.secretMountPath.currentPath, true),
];
}
}

View File

@@ -4,26 +4,28 @@
*/
import Route from '@ember/routing/route';
import { ldapBreadcrumbs } from 'ldap/utils/ldap-breadcrumbs';
import type LdapRoleModel from 'vault/models/ldap/role';
import type Controller from '@ember/controller';
import type Transition from '@ember/routing/transition';
import type { Breadcrumb } from 'vault/vault/app-types';
interface LdapRoleEditController extends Controller {
interface RouteController extends Controller {
breadcrumbs: Array<Breadcrumb>;
model: LdapRoleModel;
}
export default class LdapRoleEditRoute extends Route {
setupController(controller: LdapRoleEditController, resolvedModel: LdapRoleModel, transition: Transition) {
setupController(controller: RouteController, resolvedModel: LdapRoleModel, transition: Transition) {
super.setupController(controller, resolvedModel, transition);
controller.breadcrumbs = [
{ label: 'Secrets', route: 'secrets', linkExternal: true },
{ label: resolvedModel.backend, route: 'overview' },
{ label: 'roles', route: 'roles' },
{ label: resolvedModel.name, route: 'roles.role' },
{ label: 'edit' },
{ label: 'Roles', route: 'roles' },
...ldapBreadcrumbs(resolvedModel.name, resolvedModel.type, resolvedModel.backend),
{ label: 'Edit' },
];
}
}

View File

@@ -0,0 +1,75 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import LdapRolesRoute from '../roles';
import { hash } from 'rsvp';
import { ldapBreadcrumbs } from 'ldap/utils/ldap-breadcrumbs';
import type { Breadcrumb } from 'vault/vault/app-types';
import type Controller from '@ember/controller';
import type LdapRoleModel from 'vault/models/ldap/role';
import type SecretEngineModel from 'vault/models/secret-engine';
import type Transition from '@ember/routing/transition';
interface RouteModel {
backendModel: SecretEngineModel;
roleAncestry: { path_to_role: string; type: string };
roles: Array<LdapRoleModel>;
}
interface RouteController extends Controller {
breadcrumbs: Array<Breadcrumb>;
model: RouteModel;
}
interface RouteParams {
page?: string;
pageFilter: string;
path_to_role: string;
type: string;
}
export default class LdapRolesSubdirectoryRoute extends LdapRolesRoute {
queryParams = {
pageFilter: {
refreshModel: true,
},
page: {
refreshModel: true,
},
};
model(params: RouteParams) {
const backendModel = this.modelFor('application') as SecretEngineModel;
const { path_to_role, type } = params;
const roleAncestry = { path_to_role, type };
return hash({
backendModel,
roleAncestry,
roles: this.lazyQuery(backendModel.id, params, { roleAncestry }),
});
}
setupController(controller: RouteController, resolvedModel: RouteModel, transition: Transition) {
super.setupController(controller, resolvedModel, transition);
const { backendModel, roleAncestry } = resolvedModel;
const crumbs = [
{ label: 'Secrets', route: 'secrets', linkExternal: true },
{ label: backendModel.id, route: 'overview' },
{ label: 'Roles', route: 'roles' },
...ldapBreadcrumbs(roleAncestry.path_to_role, roleAncestry.type, backendModel.id, true),
];
// must call 'set' so breadcrumbs update as we navigate through directories
controller.set('breadcrumbs', crumbs);
}
resetController(controller: RouteController, isExiting: boolean) {
if (isExiting) {
controller.set('pageFilter', undefined);
controller.set('page', undefined);
}
}
}

View File

@@ -0,0 +1,15 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
~}}
{{#let this.model.roleAncestry.type this.model.roleAncestry.path_to_role as |roleType path_to_role|}}
<Page::Roles
@roles={{this.model.roles}}
@promptConfig={{false}}
@backendModel={{this.model.backendModel}}
@breadcrumbs={{this.breadcrumbs}}
@pageFilter={{this.pageFilter}}
@currentRouteParams={{array this.model.backendModel.id roleType path_to_role}}
/>
{{/let}}

View File

@@ -0,0 +1,36 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import type { Breadcrumb } from 'vault/vault/app-types';
export const ldapBreadcrumbs = (
fullPath: string | undefined, // i.e. path/to/item
roleType: string,
mountPath: string,
lastItemCurrent = false // this array of objects can be spread anywhere within the crumbs array
): Breadcrumb[] => {
if (!fullPath) return [];
const ancestry = fullPath.split('/').filter((path) => path !== '');
const isDirectory = fullPath.endsWith('/');
return ancestry.map((name: string, idx: number) => {
const isLast = ancestry.length === idx + 1;
// if the end of the path is the current route, don't return a route link
if (isLast && lastItemCurrent) return { label: name };
// each segment is a continued concatenation of ancestral paths.
// for example, if the full path to an item is "prod/admin/west"
// the segments will be: prod/, prod/admin/, prod/admin/west.
// LIST or GET requests can then be made for each crumb accordingly.
const segment = ancestry.slice(0, idx + 1).join('/');
const itemPath = isLast && !isDirectory ? segment : `${segment}/`;
const routeParams = [mountPath, roleType, itemPath];
return {
label: name,
route: isLast && !isDirectory ? 'roles.role.details' : 'roles.subdirectory',
models: routeParams,
};
});
};