feat: 🎸 create manage projects in org route and view (#2365)
* feat: 🎸 create manage projects in org route and view ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13882 --------- Co-authored-by: Cameron Perera <cameron.perera@hashicorp.com> Co-authored-by: Zhihe Li <zhihe.li@hashicorp.com>
This commit is contained in:
1 parent
e83bb425af
commit
3064e51bc1
20 files changed
+1541
-381
No files matched your search
@@ -586,6 +586,9 @@ role:
|
||||
manage-custom-scopes:
|
||||
title: Manage Custom Scopes
|
||||
description: Select which specific orgs you want to apply to this role's grant sets.
|
||||
manage-org-projects:
|
||||
title: 'Add Projects in {orgDisplayName}'
|
||||
description: Select which projects in this org you want to apply to this role's grant sets.
|
||||
no-org-scopes:
|
||||
title: No Org Scopes Available
|
||||
description: Org scopes are not available for assignment.
|
||||
@@ -603,6 +606,7 @@ role:
|
||||
add-custom-scopes:
|
||||
text: Add custom scopes
|
||||
help: Customize which scopes are added to this role.
|
||||
view-projects: '{selected}/{total} total projects'
|
||||
form:
|
||||
this:
|
||||
label: Add this scope
|
||||
|
||||
@@ -7,8 +7,7 @@
|
||||
...attributes
|
||||
class='rose-header-brand'
|
||||
title={{@text}}
|
||||
@route={{if @route @route 'scopes.scope'}}
|
||||
@model={{if @model @model 'global'}}
|
||||
@route={{if @route @route 'index'}}
|
||||
>
|
||||
|
||||
{{#if @logo}}
|
||||
|
||||
@@ -8,5 +8,6 @@
|
||||
@currentPage={{@currentPage}}
|
||||
@currentPageSize={{@currentPageSize}}
|
||||
@route={{this.router.currentRouteName}}
|
||||
@model={{@model}}
|
||||
@queryFunction={{this.paginationQueryParams}}
|
||||
/>
|
||||
@@ -20,7 +20,7 @@
|
||||
{{on 'input' @handleSearchInput}}
|
||||
/>
|
||||
</Hds::SegmentedGroup>
|
||||
{{#if @model.subScopes}}
|
||||
{{#if @model.orgScopes}}
|
||||
{{#if this.selectedItems.length}}
|
||||
<div class='table-selected-items-text'>
|
||||
<Hds::Text::Body @tag='p' @color='faint'>
|
||||
@@ -38,7 +38,7 @@
|
||||
(hash label=(t 'form.id.label'))
|
||||
(hash label=(t 'resources.role.scope.titles.projects_selected'))
|
||||
}}
|
||||
@model={{@model.subScopes}}
|
||||
@model={{@model.orgScopes}}
|
||||
@isSelectable={{true}}
|
||||
@onSelectionChange={{this.selectionChange}}
|
||||
@valign='middle'
|
||||
@@ -62,12 +62,27 @@
|
||||
/>
|
||||
</B.Td>
|
||||
<B.Td>
|
||||
{{! TODO: Add link to projects selected page}}
|
||||
{{#let (get @model.projectTotals B.data.id) as |projectTotals|}}
|
||||
{{#if projectTotals}}
|
||||
<Hds::Link::Standalone
|
||||
@icon='arrow-right'
|
||||
@iconPosition='trailing'
|
||||
@text={{t
|
||||
'resources.role.scope.actions.view-projects'
|
||||
selected=projectTotals.selected
|
||||
total=projectTotals.total
|
||||
}}
|
||||
@route='scopes.scope.roles.role.manage-scopes.manage-org-projects'
|
||||
@model={{B.data.id}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{/let}}
|
||||
</B.Td>
|
||||
</B.Tr>
|
||||
</:body>
|
||||
</Hds::Table>
|
||||
<Rose::Pagination
|
||||
@model={{@model.role.id}}
|
||||
@totalItems={{@model.totalItems}}
|
||||
@currentPage={{@page}}
|
||||
@currentPageSize={{@pageSize}}
|
||||
@@ -78,7 +93,7 @@
|
||||
@cancelText={{t 'actions.cancel'}}
|
||||
/>
|
||||
{{else}}
|
||||
<Hds::ApplicationState data-test-no-scope-results as |A|>
|
||||
<Hds::ApplicationState data-test-no-grant-scope-results as |A|>
|
||||
<A.Header @title={{t 'titles.no-results-found'}} />
|
||||
<A.Body
|
||||
@text={{t
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
{{!
|
||||
Copyright (c) HashiCorp, Inc.
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
|
||||
{{#if @model.totalItemsCount}}
|
||||
<Rose::Form
|
||||
class='full-width role-manage-custom-scopes-form'
|
||||
@onSubmit={{fn @submit this.allGrantScopes}}
|
||||
@cancel={{@cancel}}
|
||||
@disabled={{@model.role.isSaving}}
|
||||
as |form|
|
||||
>
|
||||
<Hds::SegmentedGroup as |S|>
|
||||
<S.TextInput
|
||||
@value={{@search}}
|
||||
@type='search'
|
||||
placeholder={{t 'actions.search'}}
|
||||
aria-label={{t 'actions.search'}}
|
||||
{{on 'input' @handleSearchInput}}
|
||||
/>
|
||||
</Hds::SegmentedGroup>
|
||||
{{#if @model.projectScopes}}
|
||||
{{#if this.selectedItems.length}}
|
||||
<div class='table-selected-items-text'>
|
||||
<Hds::Text::Body @tag='p' @color='faint'>
|
||||
{{t
|
||||
'resources.role.scope.titles.selected'
|
||||
items=this.selectedItems.length
|
||||
total=@model.totalItemsCount
|
||||
}}
|
||||
</Hds::Text::Body>
|
||||
</div>
|
||||
{{/if}}
|
||||
<Hds::Table
|
||||
@columns={{array
|
||||
(hash label=(t 'resources.project.title'))
|
||||
(hash label=(t 'form.id.label'))
|
||||
}}
|
||||
@model={{@model.projectScopes}}
|
||||
@isSelectable={{true}}
|
||||
@onSelectionChange={{this.selectionChange}}
|
||||
@valign='middle'
|
||||
>
|
||||
<:body as |B|>
|
||||
<B.Tr
|
||||
@selectionKey={{B.data.id}}
|
||||
@isSelected={{includes B.data.id this.selectedItems}}
|
||||
@selectionAriaLabelSuffix='row {{B.data.id}}'
|
||||
data-test-project-scopes-table-row={{B.data.id}}
|
||||
>
|
||||
<B.Td>
|
||||
<Hds::Text::Body @tag='p'>
|
||||
{{B.data.displayName}}
|
||||
</Hds::Text::Body>
|
||||
</B.Td>
|
||||
<B.Td>
|
||||
<Hds::Copy::Snippet
|
||||
@textToCopy={{B.data.id}}
|
||||
@color='secondary'
|
||||
/>
|
||||
</B.Td>
|
||||
</B.Tr>
|
||||
</:body>
|
||||
</Hds::Table>
|
||||
<Rose::Pagination
|
||||
@model={{array @model.role.id @model.orgScope.id}}
|
||||
@totalItems={{@model.totalItems}}
|
||||
@currentPage={{@page}}
|
||||
@currentPageSize={{@pageSize}}
|
||||
/>
|
||||
|
||||
<form.actions
|
||||
@submitText={{t 'actions.save'}}
|
||||
@cancelText={{t 'actions.cancel'}}
|
||||
/>
|
||||
{{else}}
|
||||
<Hds::ApplicationState data-test-no-grant-scope-results as |A|>
|
||||
<A.Header @title={{t 'titles.no-results-found'}} />
|
||||
<A.Body
|
||||
@text={{t
|
||||
'descriptions.no-search-results'
|
||||
query=@search
|
||||
resource=(t 'resources.scope.title_plural')
|
||||
}}
|
||||
/>
|
||||
</Hds::ApplicationState>
|
||||
{{/if}}
|
||||
</Rose::Form>
|
||||
{{else}}
|
||||
<Hds::ApplicationState as |A|>
|
||||
<A.Header
|
||||
@title={{t 'resources.role.scope.messages.no-org-scopes.title'}}
|
||||
/>
|
||||
<A.Body
|
||||
@text={{t 'resources.role.scope.messages.no-org-scopes.description'}}
|
||||
/>
|
||||
<A.Footer as |F|>
|
||||
<F.LinkStandalone
|
||||
@icon='arrow-left'
|
||||
@text={{t 'actions.back'}}
|
||||
@route='scopes.scope.roles.role.manage-scopes'
|
||||
/>
|
||||
</A.Footer>
|
||||
</Hds::ApplicationState>
|
||||
{{/if}}
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
import { action } from '@ember/object';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
|
||||
export default class FormRoleManageOrgProjectsIndexComponent extends Component {
|
||||
// =attributes
|
||||
|
||||
@tracked selectedItems = [...this.args.model.selectedProjectIDs];
|
||||
|
||||
get allGrantScopes() {
|
||||
return [
|
||||
...this.args.model.role.grantScopeKeywords,
|
||||
...this.args.model.role.grantScopeOrgIDs,
|
||||
...this.args.model.remainingProjectIDs,
|
||||
...this.selectedItems,
|
||||
];
|
||||
}
|
||||
|
||||
// =actions
|
||||
|
||||
/**
|
||||
* Handles the selection changes for the paginated table.
|
||||
* @param {object} selectableRowsStates
|
||||
*/
|
||||
@action
|
||||
selectionChange({ selectableRowsStates }) {
|
||||
selectableRowsStates.forEach((row) => {
|
||||
const { isSelected, selectionKey: key } = row;
|
||||
const includesId = this.selectedItems.includes(key);
|
||||
if (isSelected) {
|
||||
if (!includesId) this.selectedItems = [...this.selectedItems, key];
|
||||
} else {
|
||||
if (includesId)
|
||||
this.selectedItems = this.selectedItems.filter(
|
||||
(item) => item !== key,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -75,8 +75,9 @@
|
||||
@route={{if
|
||||
@model.scope.isGlobal
|
||||
'scopes.scope.roles.role.manage-scopes.manage-custom-scopes'
|
||||
'scopes.scope.roles.role.manage-scopes'
|
||||
'scopes.scope.roles.role.manage-scopes.manage-org-projects'
|
||||
}}
|
||||
@model={{if @model.scope.isOrg @model.scope.id}}
|
||||
/>
|
||||
|
||||
<Hds::Form::HelperText
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Controller from '@ember/controller';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import { loading } from 'ember-loading';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { debounce } from 'core/decorators/debounce';
|
||||
import { notifySuccess, notifyError } from 'core/decorators/notify';
|
||||
|
||||
export default class ScopesScopeRolesRoleManageScopesManageOrgProjectsController extends Controller {
|
||||
// =services
|
||||
|
||||
@service router;
|
||||
|
||||
// =attributes
|
||||
|
||||
queryParams = ['search', 'page', 'pageSize'];
|
||||
|
||||
@tracked search = '';
|
||||
@tracked page = 1;
|
||||
@tracked pageSize = 10;
|
||||
|
||||
// =actions
|
||||
|
||||
/**
|
||||
* Handles input on each keystroke and the search queryParam
|
||||
* @param {object} event
|
||||
*/
|
||||
@action
|
||||
@debounce(250)
|
||||
handleSearchInput(event) {
|
||||
const { value } = event.target;
|
||||
this.search = value;
|
||||
this.page = 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save grant scope IDs to current role via the API.
|
||||
* @param {RoleModel} role
|
||||
* @param {[string]} grantScopeIDs
|
||||
*/
|
||||
@action
|
||||
@loading
|
||||
@notifyError(({ message }) => message, { catch: true })
|
||||
@notifySuccess('notifications.add-success')
|
||||
async setGrantScopes(role, grantScopeIDs) {
|
||||
await role.setGrantScopes(grantScopeIDs);
|
||||
if (role.scope.isGlobal) {
|
||||
this.router.replaceWith(
|
||||
'scopes.scope.roles.role.manage-scopes.manage-custom-scopes',
|
||||
);
|
||||
} else {
|
||||
this.router.replaceWith('scopes.scope.roles.role.manage-scopes');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect to previous as if nothing ever happened.
|
||||
*/
|
||||
@action
|
||||
cancel(role) {
|
||||
if (role.scope.isGlobal) {
|
||||
this.router.replaceWith(
|
||||
'scopes.scope.roles.role.manage-scopes.manage-custom-scopes',
|
||||
);
|
||||
} else {
|
||||
this.router.replaceWith('scopes.scope.roles.role.manage-scopes');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -55,6 +55,7 @@ Router.map(function () {
|
||||
this.route('scopes');
|
||||
this.route('manage-scopes', function () {
|
||||
this.route('manage-custom-scopes');
|
||||
this.route('manage-org-projects', { path: ':org_id' });
|
||||
});
|
||||
});
|
||||
this.route('new');
|
||||
|
||||
@@ -4,5 +4,25 @@
|
||||
*/
|
||||
|
||||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class ScopesScopeRolesRoleIndexRoute extends Route {}
|
||||
export default class ScopesScopeRolesRoleIndexRoute extends Route {
|
||||
// =services
|
||||
|
||||
@service store;
|
||||
|
||||
// =methods
|
||||
|
||||
/**
|
||||
* Pre-loads all scopes since they will be necessary in sub-routes.
|
||||
* @param {RoleModel} role
|
||||
*/
|
||||
async afterModel(role) {
|
||||
if (!role.scope.isProject) {
|
||||
await this.store.query('scope', {
|
||||
scope_id: role.scope.id,
|
||||
recursive: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { TYPE_SCOPE_PROJECT } from 'api/models/scope';
|
||||
|
||||
export default class ScopesScopeRolesRoleManageScopesManageCustomScopesRoute extends Route {
|
||||
// =attributes
|
||||
@@ -30,7 +31,7 @@ export default class ScopesScopeRolesRoleManageScopesManageCustomScopesRoute ext
|
||||
|
||||
/**
|
||||
* Loads sub scopes for the current scope.
|
||||
* @returns {Promise<{role: RoleModel, subScopes: [ScopeModel], totalItems: number, totalItemsCount: number}> }
|
||||
* @returns {Promise<{role: RoleModel, orgScopes: [ScopeModel], projectTotals: object, totalItems: number, totalItemsCount: number}> }
|
||||
*/
|
||||
async model({ search, page, pageSize }) {
|
||||
const role = this.modelFor('scopes.scope.roles.role');
|
||||
@@ -40,27 +41,63 @@ export default class ScopesScopeRolesRoleManageScopesManageCustomScopesRoute ext
|
||||
scope_id: [{ equals: scope_id }],
|
||||
};
|
||||
|
||||
const subScopes = await this.store.query('scope', {
|
||||
const orgScopes = await this.store.query('scope', {
|
||||
scope_id,
|
||||
query: { search, filters },
|
||||
page,
|
||||
pageSize,
|
||||
});
|
||||
const totalItems = subScopes.meta?.totalItems;
|
||||
const totalItems = orgScopes.meta?.totalItems;
|
||||
const totalItemsCount = await this.getTotalItemsCount(
|
||||
scope_id,
|
||||
search,
|
||||
totalItems,
|
||||
);
|
||||
|
||||
const projectTotals = await this.getProjectTotals(
|
||||
role.grantScopeProjectIDs,
|
||||
);
|
||||
|
||||
return {
|
||||
role,
|
||||
subScopes,
|
||||
orgScopes,
|
||||
projectTotals,
|
||||
totalItems,
|
||||
totalItemsCount,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an object that contains the number of selected projects
|
||||
* and the total number of projects for each org scope.
|
||||
* @param {[string]} projectIDs
|
||||
* @returns {object}
|
||||
*/
|
||||
async getProjectTotals(projectIDs) {
|
||||
const options = { pushToStore: false, peekIndexedDB: true };
|
||||
const projects = await this.store.query(
|
||||
'scope',
|
||||
{
|
||||
scope_id: 'global',
|
||||
query: { filters: { type: [{ equals: TYPE_SCOPE_PROJECT }] } },
|
||||
},
|
||||
options,
|
||||
);
|
||||
|
||||
const projectTotals = {};
|
||||
projects.forEach(({ id, scope }) => {
|
||||
if (!projectTotals[scope.id]) {
|
||||
projectTotals[scope.id] = { selected: 0, total: 0 };
|
||||
}
|
||||
if (projectIDs.includes(id)) {
|
||||
projectTotals[scope.id].selected++;
|
||||
}
|
||||
projectTotals[scope.id].total++;
|
||||
});
|
||||
|
||||
return projectTotals;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets scopesExist to true if there exists any scopes and
|
||||
* sets totalItemsCount to the number of scopes that exist.
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class ScopesScopeRolesRoleManageScopesManageOrgProjectsRoute extends Route {
|
||||
// =attributes
|
||||
|
||||
queryParams = {
|
||||
search: {
|
||||
refreshModel: true,
|
||||
replace: true,
|
||||
},
|
||||
page: {
|
||||
refreshModel: true,
|
||||
},
|
||||
pageSize: {
|
||||
refreshModel: true,
|
||||
},
|
||||
};
|
||||
|
||||
// =services
|
||||
|
||||
@service store;
|
||||
|
||||
// =methods
|
||||
|
||||
/**
|
||||
* Loads projects for current org scope.
|
||||
* @return {Promise<{role: RoleModel, orgScope: ScopeModel, projectScopes: [ScopeModel], totalItems: number, totalItemsCount: number, selectedProjectIDs: [string], remainingProjectIDs: [string]}> }
|
||||
*/
|
||||
async model({ org_id, search, page, pageSize }) {
|
||||
const role = this.modelFor('scopes.scope.roles.role');
|
||||
const orgScope = this.store.peekRecord('scope', org_id);
|
||||
const filters = {
|
||||
scope_id: [{ equals: org_id }],
|
||||
};
|
||||
|
||||
const projectScopes = await this.store.query('scope', {
|
||||
scope_id: org_id,
|
||||
query: { search, filters },
|
||||
page,
|
||||
pageSize,
|
||||
});
|
||||
const totalItems = projectScopes.meta?.totalItems;
|
||||
const totalItemsCount = await this.getTotalItemsCount(
|
||||
org_id,
|
||||
search,
|
||||
totalItems,
|
||||
);
|
||||
|
||||
const { selectedProjectIDs, remainingProjectIDs } =
|
||||
await this.getSelectedProjects(role, org_id);
|
||||
|
||||
return {
|
||||
role,
|
||||
orgScope,
|
||||
projectScopes,
|
||||
totalItems,
|
||||
totalItemsCount,
|
||||
selectedProjectIDs,
|
||||
remainingProjectIDs,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets scopesExist to true if there exists any scopes and
|
||||
* sets totalItemsCount to the number of scopes that exist.
|
||||
* @param {string} scope_id
|
||||
* @param {string} search
|
||||
* @param {number} totalItems
|
||||
* @returns {Promise<number>}
|
||||
*/
|
||||
async getTotalItemsCount(scope_id, search, totalItems) {
|
||||
if (!search) {
|
||||
return totalItems;
|
||||
}
|
||||
const options = { pushToStore: false, peekIndexedDB: true };
|
||||
const scopes = await this.store.query(
|
||||
'scope',
|
||||
{
|
||||
scope_id,
|
||||
query: {
|
||||
filters: {
|
||||
scope_id: [{ equals: scope_id }],
|
||||
},
|
||||
},
|
||||
page: 1,
|
||||
pageSize: 1,
|
||||
},
|
||||
options,
|
||||
);
|
||||
return scopes.meta?.totalItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract project type grant scope ids that belong to this org scope.
|
||||
* @param {RoleModel} role
|
||||
* @param {string} org_id
|
||||
* @returns {object}
|
||||
*/
|
||||
async getSelectedProjects(role, org_id) {
|
||||
let selectedProjectIDs = role.grantScopeProjectIDs;
|
||||
let remainingProjectIDs = [];
|
||||
if (role.scope.isGlobal && selectedProjectIDs.length) {
|
||||
const id = [];
|
||||
selectedProjectIDs.forEach((projectID) => id.push({ equals: projectID }));
|
||||
const options = { pushToStore: false, peekIndexedDB: true };
|
||||
const projects = await this.store.query(
|
||||
'scope',
|
||||
{ scope_id: 'global', query: { filters: { id } } },
|
||||
options,
|
||||
);
|
||||
selectedProjectIDs = projects.reduce((selectedIDs, project) => {
|
||||
if (project.scope.id === org_id) {
|
||||
selectedIDs.push(project.id);
|
||||
} else {
|
||||
remainingProjectIDs.push(project.id);
|
||||
}
|
||||
return selectedIDs;
|
||||
}, []);
|
||||
}
|
||||
return { selectedProjectIDs, remainingProjectIDs };
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,9 @@
|
||||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { GRANT_SCOPE_KEYWORDS } from 'api/models/role';
|
||||
|
||||
export default class ScopesScopeRolesRoleScopesRoute extends Route {
|
||||
// =services
|
||||
// =methods
|
||||
|
||||
@service store;
|
||||
|
||||
async model() {
|
||||
const role = this.modelFor('scopes.scope.roles.role');
|
||||
if (
|
||||
role.grant_scope_ids &&
|
||||
!GRANT_SCOPE_KEYWORDS.some((id) => role.grant_scope_ids.includes(id))
|
||||
) {
|
||||
await this.store.query('scope', {
|
||||
scope_id: role.scope.id,
|
||||
recursive: true,
|
||||
});
|
||||
}
|
||||
return role;
|
||||
model() {
|
||||
return this.modelFor('scopes.scope.roles.role');
|
||||
}
|
||||
}
|
||||
@@ -1059,7 +1059,10 @@
|
||||
.table-selected-items-text {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.hds-table {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.hds-pagination {
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
{{!
|
||||
Copyright (c) HashiCorp, Inc.
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
|
||||
{{page-title
|
||||
(t
|
||||
'resources.role.scope.messages.manage-org-projects.title'
|
||||
orgDisplayName=@model.orgScope.displayName
|
||||
)
|
||||
}}
|
||||
{{#if @model.role.scope.isGlobal}}
|
||||
<Breadcrumbs::Item
|
||||
@text={{t 'resources.role.scope.messages.manage-custom-scopes.title'}}
|
||||
@route='scopes.scope.roles.role.manage-scopes.manage-custom-scopes'
|
||||
/>
|
||||
{{/if}}
|
||||
<Breadcrumbs::Item
|
||||
@text={{t
|
||||
'resources.role.scope.messages.manage-org-projects.title'
|
||||
orgDisplayName=@model.orgScope.displayName
|
||||
}}
|
||||
@route='scopes.scope.roles.role.manage-scopes.manage-org-projects'
|
||||
@model={{@model}}
|
||||
/>
|
||||
|
||||
<Rose::Layout::Page as |page|>
|
||||
|
||||
<page.breadcrumbs>
|
||||
<Breadcrumbs::Container />
|
||||
</page.breadcrumbs>
|
||||
|
||||
<page.header>
|
||||
<h2>
|
||||
{{t
|
||||
'resources.role.scope.messages.manage-org-projects.title'
|
||||
orgDisplayName=@model.orgScope.displayName
|
||||
}}
|
||||
{{! TODO: Add doclink }}
|
||||
</h2>
|
||||
<p>{{t 'resources.role.scope.messages.manage-org-projects.description'}}</p>
|
||||
</page.header>
|
||||
|
||||
<page.body class='search-filtering'>
|
||||
<Form::Role::ManageOrgProjects
|
||||
@model={{@model}}
|
||||
@search={{this.search}}
|
||||
@page={{this.page}}
|
||||
@pageSize={{this.pageSize}}
|
||||
@handleSearchInput={{this.handleSearchInput}}
|
||||
@submit={{fn this.setGrantScopes @model.role}}
|
||||
@cancel={{fn this.cancel @model.role}}
|
||||
/>
|
||||
</page.body>
|
||||
|
||||
</Rose::Layout::Page>
|
||||
@@ -0,0 +1,533 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import {
|
||||
visit,
|
||||
currentURL,
|
||||
click,
|
||||
findAll,
|
||||
fillIn,
|
||||
waitUntil,
|
||||
} from '@ember/test-helpers';
|
||||
import { setupApplicationTest } from 'ember-qunit';
|
||||
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
|
||||
import { setupIndexedDb } from 'api/test-support/helpers/indexed-db';
|
||||
import a11yAudit from 'ember-a11y-testing/test-support/audit';
|
||||
import { authenticateSession } from 'ember-simple-auth/test-support';
|
||||
import {
|
||||
GRANT_SCOPE_THIS,
|
||||
GRANT_SCOPE_CHILDREN,
|
||||
GRANT_SCOPE_DESCENDANTS,
|
||||
} from 'api/models/role';
|
||||
|
||||
module('Acceptance | roles | global-scope', function (hooks) {
|
||||
setupApplicationTest(hooks);
|
||||
setupMirage(hooks);
|
||||
setupIndexedDb(hooks);
|
||||
|
||||
const SCOPE_TOGGLE_SELECTOR = (name) =>
|
||||
`.hds-form-toggle input[name="${name}"]`;
|
||||
const SCOPE_CHECKBOX_SELECTOR = (type, id) =>
|
||||
`tbody [data-test-${type}-scopes-table-row="${id}"] input`;
|
||||
const TABLE_ROW_SELECTOR = 'tbody tr';
|
||||
const TABLE_SCOPE_SELECTOR = 'tbody tr:nth-child(2) a';
|
||||
const TABLE_PARENT_SCOPE_SELECTOR = 'tbody tr:nth-child(2) td:nth-child(3) a';
|
||||
const SAVE_BTN_SELECTOR = 'form [type="submit"]';
|
||||
const CANCEL_BTN_SELECTOR = '.rose-form-actions [type="button"]';
|
||||
const MANAGE_SCOPES_SELECTOR = '[data-test-manage-dropdown-scopes]';
|
||||
const TOAST_SELECTOR = '[role="alert"]';
|
||||
const SEARCH_INPUT_SELECTOR = '.search-filtering [type="search"]';
|
||||
const NO_RESULTS_MSG_SELECTOR = '[data-test-no-grant-scope-results]';
|
||||
const NO_SCOPES_MSG_SELECTOR = '.role-grant-scopes div';
|
||||
const NO_SCOPES_MSG_LINK_SELECTOR =
|
||||
'.role-grant-scopes div div:nth-child(3) a';
|
||||
|
||||
const instances = {
|
||||
scopes: {
|
||||
global: null,
|
||||
org: null,
|
||||
project: null,
|
||||
},
|
||||
role: null,
|
||||
};
|
||||
const urls = {
|
||||
orgScope: null,
|
||||
roles: null,
|
||||
role: null,
|
||||
roleScopes: null,
|
||||
manageScopes: null,
|
||||
manageCustomScopes: null,
|
||||
};
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
authenticateSession({});
|
||||
instances.scopes.global = this.server.create('scope', { id: 'global' });
|
||||
instances.scopes.org = this.server.create('scope', {
|
||||
type: 'org',
|
||||
scope: { id: 'global', type: 'global' },
|
||||
});
|
||||
instances.scopes.project = this.server.create('scope', {
|
||||
type: 'project',
|
||||
scope: { id: instances.scopes.org.id, type: 'org' },
|
||||
});
|
||||
instances.role = this.server.create(
|
||||
'role',
|
||||
{
|
||||
scope: instances.scopes.global,
|
||||
},
|
||||
'withScopes',
|
||||
);
|
||||
|
||||
urls.roles = `/scopes/global/roles`;
|
||||
urls.role = `${urls.roles}/${instances.role.id}`;
|
||||
urls.roleScopes = `${urls.role}/scopes`;
|
||||
urls.manageScopes = `${urls.role}/manage-scopes`;
|
||||
urls.manageCustomScopes = `${urls.manageScopes}/manage-custom-scopes`;
|
||||
});
|
||||
|
||||
test('visiting role scopes', async function (assert) {
|
||||
await visit(urls.role);
|
||||
|
||||
await click(`[href="${urls.roleScopes}"]`);
|
||||
await a11yAudit();
|
||||
|
||||
assert.strictEqual(currentURL(), urls.roleScopes);
|
||||
assert
|
||||
.dom(TABLE_ROW_SELECTOR)
|
||||
.exists({ count: instances.role.grant_scope_ids.length });
|
||||
});
|
||||
|
||||
test('user can naviage to scope from role grant scopes', async function (assert) {
|
||||
await visit(urls.role);
|
||||
|
||||
await click(`[href="${urls.roleScopes}"]`);
|
||||
await click(TABLE_SCOPE_SELECTOR);
|
||||
|
||||
assert.strictEqual(
|
||||
currentURL(),
|
||||
`/scopes/${instances.scopes.global.id}/scopes`,
|
||||
);
|
||||
});
|
||||
|
||||
test('user can naviage to parent scope from role grant scopes', async function (assert) {
|
||||
instances.role.update({
|
||||
grant_scope_ids: ['this', instances.scopes.org.id],
|
||||
});
|
||||
await visit(urls.role);
|
||||
|
||||
await click(`[href="${urls.roleScopes}"]`);
|
||||
await click(TABLE_PARENT_SCOPE_SELECTOR);
|
||||
|
||||
assert.strictEqual(
|
||||
currentURL(),
|
||||
`/scopes/${instances.scopes.global.id}/scopes`,
|
||||
);
|
||||
});
|
||||
|
||||
test('user sees no scopes message and action when role has no grant scopes', async function (assert) {
|
||||
instances.role.update({ grant_scope_ids: [] });
|
||||
await visit(urls.role);
|
||||
|
||||
await click(`[href="${urls.roleScopes}"]`);
|
||||
|
||||
assert.dom(NO_SCOPES_MSG_SELECTOR).includesText('No scopes added');
|
||||
assert.dom(NO_SCOPES_MSG_LINK_SELECTOR).isVisible();
|
||||
});
|
||||
|
||||
test('user does not see action to add scopes when role has no grant scopes without proper permissions', async function (assert) {
|
||||
instances.role.update({
|
||||
grant_scope_ids: [],
|
||||
authorized_actions: instances.role.authorized_actions.filter(
|
||||
(action) => action !== 'set-grant-scopes',
|
||||
),
|
||||
});
|
||||
await visit(urls.role);
|
||||
|
||||
await click(`[href="${urls.roleScopes}"]`);
|
||||
|
||||
assert.dom(NO_SCOPES_MSG_LINK_SELECTOR).doesNotExist();
|
||||
});
|
||||
|
||||
test('correct toggles are visible for global level role on manage scopes page', async function (assert) {
|
||||
await visit(urls.role);
|
||||
|
||||
await click(MANAGE_SCOPES_SELECTOR);
|
||||
|
||||
assert.strictEqual(currentURL(), urls.manageScopes);
|
||||
assert.dom(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_THIS)).isVisible();
|
||||
assert.dom(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_CHILDREN)).isVisible();
|
||||
assert.dom(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_DESCENDANTS)).isVisible();
|
||||
});
|
||||
|
||||
test('user can save scope keywords to add on manage scopes page', async function (assert) {
|
||||
instances.role.update({ grant_scope_ids: [] });
|
||||
await visit(urls.role);
|
||||
|
||||
await click(`[href="${urls.roleScopes}"]`);
|
||||
|
||||
assert.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 0);
|
||||
|
||||
await click(MANAGE_SCOPES_SELECTOR);
|
||||
await a11yAudit();
|
||||
|
||||
assert.strictEqual(currentURL(), urls.manageScopes);
|
||||
|
||||
// Click three times to select, unselect, then reselect (for coverage)
|
||||
await click(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_THIS));
|
||||
await click(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_THIS));
|
||||
await click(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_THIS));
|
||||
await click(SAVE_BTN_SELECTOR);
|
||||
|
||||
assert.strictEqual(currentURL(), urls.roleScopes);
|
||||
assert.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 1);
|
||||
});
|
||||
|
||||
test('user can cancel scope keywords to add on manage scopes page', async function (assert) {
|
||||
instances.role.update({ grant_scope_ids: [] });
|
||||
await visit(urls.role);
|
||||
|
||||
await click(`[href="${urls.roleScopes}"]`);
|
||||
|
||||
assert.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 0);
|
||||
|
||||
await click(MANAGE_SCOPES_SELECTOR);
|
||||
|
||||
assert.strictEqual(currentURL(), urls.manageScopes);
|
||||
|
||||
// Click three times to select, unselect, then reselect (for coverage)
|
||||
await click(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_THIS));
|
||||
await click(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_THIS));
|
||||
await click(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_THIS));
|
||||
await click(CANCEL_BTN_SELECTOR);
|
||||
|
||||
assert.strictEqual(currentURL(), urls.roleScopes);
|
||||
assert.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 0);
|
||||
});
|
||||
|
||||
test('shows error message on scope save on manage scopes page', async function (assert) {
|
||||
this.server.post('/roles/:idMethod', () => {
|
||||
return new Response(
|
||||
400,
|
||||
{},
|
||||
{
|
||||
status: 400,
|
||||
code: 'invalid_argument',
|
||||
message: 'The request was invalid.',
|
||||
details: {},
|
||||
},
|
||||
);
|
||||
});
|
||||
instances.role.update({ grant_scope_ids: [] });
|
||||
await visit(urls.role);
|
||||
|
||||
await click(MANAGE_SCOPES_SELECTOR);
|
||||
await click(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_THIS));
|
||||
await click(SAVE_BTN_SELECTOR);
|
||||
|
||||
assert.dom(TOAST_SELECTOR).isVisible();
|
||||
});
|
||||
|
||||
test('user can save custom scopes to add on manage custom scopes page', async function (assert) {
|
||||
instances.role.update({ grant_scope_ids: [] });
|
||||
await visit(urls.role);
|
||||
|
||||
await click(`[href="${urls.roleScopes}"]`);
|
||||
|
||||
assert.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 0);
|
||||
|
||||
await click(MANAGE_SCOPES_SELECTOR);
|
||||
|
||||
assert.strictEqual(currentURL(), urls.manageScopes);
|
||||
|
||||
await click(`[href="${urls.manageCustomScopes}"]`);
|
||||
await a11yAudit();
|
||||
|
||||
assert.strictEqual(currentURL(), urls.manageCustomScopes);
|
||||
|
||||
// Click three times to select, unselect, then reselect (for coverage)
|
||||
await click(SCOPE_CHECKBOX_SELECTOR('org', instances.scopes.org.id));
|
||||
await click(SCOPE_CHECKBOX_SELECTOR('org', instances.scopes.org.id));
|
||||
await click(SCOPE_CHECKBOX_SELECTOR('org', instances.scopes.org.id));
|
||||
await click(SAVE_BTN_SELECTOR);
|
||||
|
||||
assert.strictEqual(currentURL(), urls.manageScopes);
|
||||
|
||||
await click(SAVE_BTN_SELECTOR);
|
||||
|
||||
assert.strictEqual(currentURL(), urls.roleScopes);
|
||||
assert.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 1);
|
||||
});
|
||||
|
||||
test('user can cancel custom scopes to add on manage custom scopes page', async function (assert) {
|
||||
instances.role.update({ grant_scope_ids: [] });
|
||||
await visit(urls.role);
|
||||
|
||||
await click(`[href="${urls.roleScopes}"]`);
|
||||
|
||||
assert.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 0);
|
||||
|
||||
await click(MANAGE_SCOPES_SELECTOR);
|
||||
|
||||
assert.strictEqual(currentURL(), urls.manageScopes);
|
||||
|
||||
await click(`[href="${urls.manageCustomScopes}"]`);
|
||||
|
||||
// Click three times to select, unselect, then reselect (for coverage)
|
||||
await click(SCOPE_CHECKBOX_SELECTOR('org', instances.scopes.org.id));
|
||||
await click(SCOPE_CHECKBOX_SELECTOR('org', instances.scopes.org.id));
|
||||
await click(SCOPE_CHECKBOX_SELECTOR('org', instances.scopes.org.id));
|
||||
await click(CANCEL_BTN_SELECTOR);
|
||||
|
||||
assert.strictEqual(currentURL(), urls.manageScopes);
|
||||
|
||||
await click(CANCEL_BTN_SELECTOR);
|
||||
|
||||
assert.strictEqual(currentURL(), urls.roleScopes);
|
||||
assert.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 0);
|
||||
});
|
||||
|
||||
test('shows error message on custom scope save on manage custom scopes page', async function (assert) {
|
||||
this.server.post('/roles/:idMethod', () => {
|
||||
return new Response(
|
||||
400,
|
||||
{},
|
||||
{
|
||||
status: 400,
|
||||
code: 'invalid_argument',
|
||||
message: 'The request was invalid.',
|
||||
details: {},
|
||||
},
|
||||
);
|
||||
});
|
||||
instances.role.update({ grant_scope_ids: [] });
|
||||
await visit(urls.role);
|
||||
|
||||
await click(MANAGE_SCOPES_SELECTOR);
|
||||
await click(`[href="${urls.manageCustomScopes}"]`);
|
||||
await click(SCOPE_CHECKBOX_SELECTOR('org', instances.scopes.org.id));
|
||||
await click(SAVE_BTN_SELECTOR);
|
||||
|
||||
assert.dom(TOAST_SELECTOR).isVisible();
|
||||
});
|
||||
|
||||
test('user can search for a specifc org scope by id on manage custom scopes page', async function (assert) {
|
||||
const anotherOrg = this.server.create('scope', {
|
||||
type: 'org',
|
||||
scope: { id: 'global', type: 'global' },
|
||||
});
|
||||
await visit(urls.manageScopes);
|
||||
|
||||
await click(`[href="${urls.manageCustomScopes}"]`);
|
||||
|
||||
assert
|
||||
.dom(SCOPE_CHECKBOX_SELECTOR('org', instances.scopes.org.id))
|
||||
.exists();
|
||||
assert.dom(SCOPE_CHECKBOX_SELECTOR('org', anotherOrg.id)).exists();
|
||||
|
||||
await fillIn(SEARCH_INPUT_SELECTOR, instances.scopes.org.id);
|
||||
await waitUntil(
|
||||
() => findAll(SCOPE_CHECKBOX_SELECTOR('org', anotherOrg.id)).length === 0,
|
||||
);
|
||||
|
||||
assert
|
||||
.dom(SCOPE_CHECKBOX_SELECTOR('org', instances.scopes.org.id))
|
||||
.exists();
|
||||
assert.dom(SCOPE_CHECKBOX_SELECTOR('org', anotherOrg.id)).doesNotExist();
|
||||
});
|
||||
|
||||
test('user can search for org scopes and get no results on manage custom scopes page', async function (assert) {
|
||||
const anotherOrg = this.server.create('scope', {
|
||||
type: 'org',
|
||||
scope: { id: 'global', type: 'global' },
|
||||
});
|
||||
await visit(urls.manageScopes);
|
||||
|
||||
await click(`[href="${urls.manageCustomScopes}"]`);
|
||||
|
||||
assert
|
||||
.dom(SCOPE_CHECKBOX_SELECTOR('org', instances.scopes.org.id))
|
||||
.exists();
|
||||
assert.dom(SCOPE_CHECKBOX_SELECTOR('org', anotherOrg.id)).exists();
|
||||
|
||||
await fillIn(SEARCH_INPUT_SELECTOR, 'fake scope that does not exist');
|
||||
await waitUntil(() => findAll(NO_RESULTS_MSG_SELECTOR).length === 1);
|
||||
|
||||
assert
|
||||
.dom(SCOPE_CHECKBOX_SELECTOR('org', instances.scopes.org.id))
|
||||
.doesNotExist();
|
||||
assert.dom(SCOPE_CHECKBOX_SELECTOR('org', anotherOrg.id)).doesNotExist();
|
||||
assert.dom(NO_RESULTS_MSG_SELECTOR).includesText('No results found');
|
||||
});
|
||||
|
||||
test('user can save custom scopes to add on manage org projects page', async function (assert) {
|
||||
instances.role.update({ grant_scope_ids: [] });
|
||||
await visit(urls.role);
|
||||
|
||||
await click(`[href="${urls.roleScopes}"]`);
|
||||
|
||||
assert.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 0);
|
||||
|
||||
await click(MANAGE_SCOPES_SELECTOR);
|
||||
await click(`[href="${urls.manageCustomScopes}"]`);
|
||||
await click(
|
||||
`tbody [href="${urls.manageScopes}/${instances.scopes.org.id}"]`,
|
||||
);
|
||||
await a11yAudit();
|
||||
|
||||
// Click three times to select, unselect, then reselect (for coverage)
|
||||
await click(
|
||||
SCOPE_CHECKBOX_SELECTOR('project', instances.scopes.project.id),
|
||||
);
|
||||
await click(
|
||||
SCOPE_CHECKBOX_SELECTOR('project', instances.scopes.project.id),
|
||||
);
|
||||
await click(
|
||||
SCOPE_CHECKBOX_SELECTOR('project', instances.scopes.project.id),
|
||||
);
|
||||
await click(SAVE_BTN_SELECTOR);
|
||||
|
||||
assert.strictEqual(currentURL(), urls.manageCustomScopes);
|
||||
|
||||
await click(SAVE_BTN_SELECTOR);
|
||||
|
||||
assert.strictEqual(currentURL(), urls.manageScopes);
|
||||
|
||||
await click(SAVE_BTN_SELECTOR);
|
||||
|
||||
assert.strictEqual(currentURL(), urls.roleScopes);
|
||||
assert.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 1);
|
||||
});
|
||||
|
||||
test('user can cancel custom scopes to add on manage org projects page', async function (assert) {
|
||||
instances.role.update({ grant_scope_ids: [] });
|
||||
await visit(urls.role);
|
||||
|
||||
await click(`[href="${urls.roleScopes}"]`);
|
||||
|
||||
assert.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 0);
|
||||
|
||||
await click(MANAGE_SCOPES_SELECTOR);
|
||||
await click(`[href="${urls.manageCustomScopes}"]`);
|
||||
await click(
|
||||
`tbody [href="${urls.manageScopes}/${instances.scopes.org.id}"]`,
|
||||
);
|
||||
|
||||
// Click three times to select, unselect, then reselect (for coverage)
|
||||
await click(
|
||||
SCOPE_CHECKBOX_SELECTOR('project', instances.scopes.project.id),
|
||||
);
|
||||
await click(
|
||||
SCOPE_CHECKBOX_SELECTOR('project', instances.scopes.project.id),
|
||||
);
|
||||
await click(
|
||||
SCOPE_CHECKBOX_SELECTOR('project', instances.scopes.project.id),
|
||||
);
|
||||
await click(CANCEL_BTN_SELECTOR);
|
||||
|
||||
assert.strictEqual(currentURL(), urls.manageCustomScopes);
|
||||
|
||||
await click(CANCEL_BTN_SELECTOR);
|
||||
|
||||
assert.strictEqual(currentURL(), urls.manageScopes);
|
||||
|
||||
await click(CANCEL_BTN_SELECTOR);
|
||||
|
||||
assert.strictEqual(currentURL(), urls.roleScopes);
|
||||
assert.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 0);
|
||||
});
|
||||
|
||||
test('shows error message on custom scope save on manage org projects page', async function (assert) {
|
||||
this.server.post('/roles/:idMethod', () => {
|
||||
return new Response(
|
||||
400,
|
||||
{},
|
||||
{
|
||||
status: 400,
|
||||
code: 'invalid_argument',
|
||||
message: 'The request was invalid.',
|
||||
details: {},
|
||||
},
|
||||
);
|
||||
});
|
||||
instances.role.update({ grant_scope_ids: [] });
|
||||
await visit(urls.role);
|
||||
|
||||
await click(MANAGE_SCOPES_SELECTOR);
|
||||
await click(`[href="${urls.manageCustomScopes}"]`);
|
||||
await click(
|
||||
`tbody [href="${urls.manageScopes}/${instances.scopes.org.id}"]`,
|
||||
);
|
||||
await click(
|
||||
SCOPE_CHECKBOX_SELECTOR('project', instances.scopes.project.id),
|
||||
);
|
||||
await click(SAVE_BTN_SELECTOR);
|
||||
|
||||
assert.dom(TOAST_SELECTOR).isVisible();
|
||||
});
|
||||
|
||||
test('user can search for a specifc project scope by id on manage org projects page', async function (assert) {
|
||||
const anotherProject = this.server.create('scope', {
|
||||
type: 'project',
|
||||
scope: { id: instances.scopes.org.id, type: 'org' },
|
||||
});
|
||||
await visit(urls.role);
|
||||
|
||||
await click(MANAGE_SCOPES_SELECTOR);
|
||||
await click(`[href="${urls.manageCustomScopes}"]`);
|
||||
await click(
|
||||
`tbody [href="${urls.manageScopes}/${instances.scopes.org.id}"]`,
|
||||
);
|
||||
|
||||
assert
|
||||
.dom(SCOPE_CHECKBOX_SELECTOR('project', instances.scopes.project.id))
|
||||
.exists();
|
||||
assert.dom(SCOPE_CHECKBOX_SELECTOR('project', anotherProject.id)).exists();
|
||||
|
||||
await fillIn(SEARCH_INPUT_SELECTOR, instances.scopes.project.id);
|
||||
await waitUntil(
|
||||
() =>
|
||||
findAll(SCOPE_CHECKBOX_SELECTOR('project', anotherProject.id))
|
||||
.length === 0,
|
||||
);
|
||||
|
||||
assert
|
||||
.dom(SCOPE_CHECKBOX_SELECTOR('project', instances.scopes.project.id))
|
||||
.exists();
|
||||
assert
|
||||
.dom(SCOPE_CHECKBOX_SELECTOR('project', anotherProject.id))
|
||||
.doesNotExist();
|
||||
});
|
||||
|
||||
test('user can search for project scopes and get no results on manage org projects page', async function (assert) {
|
||||
const anotherProject = this.server.create('scope', {
|
||||
type: 'project',
|
||||
scope: { id: instances.scopes.org.id, type: 'org' },
|
||||
});
|
||||
await visit(urls.role);
|
||||
|
||||
await click(MANAGE_SCOPES_SELECTOR);
|
||||
await click(`[href="${urls.manageCustomScopes}"]`);
|
||||
await click(
|
||||
`tbody [href="${urls.manageScopes}/${instances.scopes.org.id}"]`,
|
||||
);
|
||||
|
||||
assert
|
||||
.dom(SCOPE_CHECKBOX_SELECTOR('project', instances.scopes.project.id))
|
||||
.exists();
|
||||
assert.dom(SCOPE_CHECKBOX_SELECTOR('project', anotherProject.id)).exists();
|
||||
|
||||
await fillIn(SEARCH_INPUT_SELECTOR, 'fake scope that does not exist');
|
||||
await waitUntil(() => findAll(NO_RESULTS_MSG_SELECTOR).length === 1);
|
||||
|
||||
assert
|
||||
.dom(SCOPE_CHECKBOX_SELECTOR('project', instances.scopes.project.id))
|
||||
.doesNotExist();
|
||||
assert
|
||||
.dom(SCOPE_CHECKBOX_SELECTOR('project', anotherProject.id))
|
||||
.doesNotExist();
|
||||
assert.dom(NO_RESULTS_MSG_SELECTOR).includesText('No results found');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,376 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import {
|
||||
visit,
|
||||
currentURL,
|
||||
click,
|
||||
findAll,
|
||||
fillIn,
|
||||
waitUntil,
|
||||
} from '@ember/test-helpers';
|
||||
import { setupApplicationTest } from 'ember-qunit';
|
||||
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
|
||||
import { setupIndexedDb } from 'api/test-support/helpers/indexed-db';
|
||||
import a11yAudit from 'ember-a11y-testing/test-support/audit';
|
||||
import { authenticateSession } from 'ember-simple-auth/test-support';
|
||||
import {
|
||||
GRANT_SCOPE_THIS,
|
||||
GRANT_SCOPE_CHILDREN,
|
||||
GRANT_SCOPE_DESCENDANTS,
|
||||
} from 'api/models/role';
|
||||
|
||||
module('Acceptance | roles | org-scope', function (hooks) {
|
||||
setupApplicationTest(hooks);
|
||||
setupMirage(hooks);
|
||||
setupIndexedDb(hooks);
|
||||
|
||||
const SCOPE_TOGGLE_SELECTOR = (name) =>
|
||||
`.hds-form-toggle input[name="${name}"]`;
|
||||
const SCOPE_CHECKBOX_SELECTOR = (type, id) =>
|
||||
`tbody [data-test-${type}-scopes-table-row="${id}"] input`;
|
||||
const TABLE_ROW_SELECTOR = 'tbody tr';
|
||||
const TABLE_SCOPE_SELECTOR = 'tbody tr:nth-child(2) a';
|
||||
const TABLE_PARENT_SCOPE_SELECTOR = 'tbody tr:nth-child(2) td:nth-child(3) a';
|
||||
const SAVE_BTN_SELECTOR = 'form [type="submit"]';
|
||||
const CANCEL_BTN_SELECTOR = '.rose-form-actions [type="button"]';
|
||||
const MANAGE_SCOPES_SELECTOR = '[data-test-manage-dropdown-scopes]';
|
||||
const TOAST_SELECTOR = '[role="alert"]';
|
||||
const SEARCH_INPUT_SELECTOR = '.search-filtering [type="search"]';
|
||||
const NO_RESULTS_MSG_SELECTOR = '[data-test-no-grant-scope-results]';
|
||||
const NO_SCOPES_MSG_SELECTOR = '.role-grant-scopes div';
|
||||
const NO_SCOPES_MSG_LINK_SELECTOR =
|
||||
'.role-grant-scopes div div:nth-child(3) a';
|
||||
|
||||
const instances = {
|
||||
scopes: {
|
||||
global: null,
|
||||
org: null,
|
||||
project: null,
|
||||
},
|
||||
role: null,
|
||||
};
|
||||
const urls = {
|
||||
orgScope: null,
|
||||
roles: null,
|
||||
role: null,
|
||||
roleScopes: null,
|
||||
manageScopes: null,
|
||||
manageOrgProjects: null,
|
||||
};
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
authenticateSession({});
|
||||
instances.scopes.global = this.server.create('scope', { id: 'global' });
|
||||
instances.scopes.org = this.server.create('scope', {
|
||||
type: 'org',
|
||||
scope: { id: 'global', type: 'global' },
|
||||
});
|
||||
instances.scopes.project = this.server.create('scope', {
|
||||
type: 'project',
|
||||
scope: { id: instances.scopes.org.id, type: 'org' },
|
||||
});
|
||||
instances.role = this.server.create(
|
||||
'role',
|
||||
{
|
||||
scope: instances.scopes.org,
|
||||
},
|
||||
'withScopes',
|
||||
);
|
||||
|
||||
urls.roles = `/scopes/${instances.scopes.org.id}/roles`;
|
||||
urls.role = `${urls.roles}/${instances.role.id}`;
|
||||
urls.roleScopes = `${urls.role}/scopes`;
|
||||
urls.manageScopes = `${urls.role}/manage-scopes`;
|
||||
urls.manageOrgProjects = `${urls.role}/manage-scopes/${instances.scopes.org.id}`;
|
||||
});
|
||||
|
||||
test('visiting role scopes', async function (assert) {
|
||||
await visit(urls.role);
|
||||
|
||||
await click(`[href="${urls.roleScopes}"]`);
|
||||
await a11yAudit();
|
||||
|
||||
assert.strictEqual(currentURL(), urls.roleScopes);
|
||||
assert
|
||||
.dom(TABLE_ROW_SELECTOR)
|
||||
.exists({ count: instances.role.grant_scope_ids.length });
|
||||
});
|
||||
|
||||
test('user can naviage to scope from role grant scopes', async function (assert) {
|
||||
await visit(urls.role);
|
||||
|
||||
await click(`[href="${urls.roleScopes}"]`);
|
||||
await click(TABLE_SCOPE_SELECTOR);
|
||||
|
||||
assert.strictEqual(
|
||||
currentURL(),
|
||||
`/scopes/${instances.scopes.org.id}/scopes`,
|
||||
);
|
||||
});
|
||||
|
||||
test('user can naviage to parent scope from role grant scopes', async function (assert) {
|
||||
await visit(urls.role);
|
||||
|
||||
await click(`[href="${urls.roleScopes}"]`);
|
||||
await click(TABLE_PARENT_SCOPE_SELECTOR);
|
||||
|
||||
assert.strictEqual(
|
||||
currentURL(),
|
||||
`/scopes/${instances.scopes.global.id}/scopes`,
|
||||
);
|
||||
});
|
||||
|
||||
test('user sees no scopes message and action when role has no grant scopes', async function (assert) {
|
||||
instances.role.update({ grant_scope_ids: [] });
|
||||
await visit(urls.role);
|
||||
|
||||
await click(`[href="${urls.roleScopes}"]`);
|
||||
|
||||
assert.dom(NO_SCOPES_MSG_SELECTOR).includesText('No scopes added');
|
||||
assert.dom(NO_SCOPES_MSG_LINK_SELECTOR).isVisible();
|
||||
});
|
||||
|
||||
test('user does not see action to add scopes when role has no grant scopes without proper permissions', async function (assert) {
|
||||
instances.role.update({
|
||||
grant_scope_ids: [],
|
||||
authorized_actions: instances.role.authorized_actions.filter(
|
||||
(action) => action !== 'set-grant-scopes',
|
||||
),
|
||||
});
|
||||
await visit(urls.role);
|
||||
|
||||
await click(`[href="${urls.roleScopes}"]`);
|
||||
|
||||
assert.dom(NO_SCOPES_MSG_LINK_SELECTOR).doesNotExist();
|
||||
});
|
||||
|
||||
test('correct toggles are visible for org level role on manage scopes page', async function (assert) {
|
||||
await visit(urls.role);
|
||||
|
||||
await click(MANAGE_SCOPES_SELECTOR);
|
||||
|
||||
assert.strictEqual(currentURL(), urls.manageScopes);
|
||||
assert.dom(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_THIS)).isVisible();
|
||||
assert.dom(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_CHILDREN)).isVisible();
|
||||
assert.dom(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_DESCENDANTS)).doesNotExist();
|
||||
});
|
||||
|
||||
test('user can save scope keywords to add on manage scopes page', async function (assert) {
|
||||
instances.role.update({ grant_scope_ids: [] });
|
||||
await visit(urls.role);
|
||||
|
||||
await click(`[href="${urls.roleScopes}"]`);
|
||||
|
||||
assert.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 0);
|
||||
|
||||
await click(MANAGE_SCOPES_SELECTOR);
|
||||
await a11yAudit();
|
||||
|
||||
assert.strictEqual(currentURL(), urls.manageScopes);
|
||||
|
||||
// Click three times to select, unselect, then reselect (for coverage)
|
||||
await click(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_THIS));
|
||||
await click(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_THIS));
|
||||
await click(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_THIS));
|
||||
await click(SAVE_BTN_SELECTOR);
|
||||
|
||||
assert.strictEqual(currentURL(), urls.roleScopes);
|
||||
assert.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 1);
|
||||
});
|
||||
|
||||
test('user can cancel scope keywords to add on manage scopes page', async function (assert) {
|
||||
instances.role.update({ grant_scope_ids: [] });
|
||||
await visit(urls.role);
|
||||
|
||||
await click(`[href="${urls.roleScopes}"]`);
|
||||
|
||||
assert.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 0);
|
||||
|
||||
await click(MANAGE_SCOPES_SELECTOR);
|
||||
|
||||
assert.strictEqual(currentURL(), urls.manageScopes);
|
||||
|
||||
// Click three times to select, unselect, then reselect (for coverage)
|
||||
await click(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_THIS));
|
||||
await click(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_THIS));
|
||||
await click(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_THIS));
|
||||
await click(CANCEL_BTN_SELECTOR);
|
||||
|
||||
assert.strictEqual(currentURL(), urls.roleScopes);
|
||||
assert.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 0);
|
||||
});
|
||||
|
||||
test('shows error message on scope save on manage scopes page', async function (assert) {
|
||||
this.server.post('/roles/:idMethod', () => {
|
||||
return new Response(
|
||||
400,
|
||||
{},
|
||||
{
|
||||
status: 400,
|
||||
code: 'invalid_argument',
|
||||
message: 'The request was invalid.',
|
||||
details: {},
|
||||
},
|
||||
);
|
||||
});
|
||||
instances.role.update({ grant_scope_ids: [] });
|
||||
await visit(urls.role);
|
||||
|
||||
await click(MANAGE_SCOPES_SELECTOR);
|
||||
await click(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_THIS));
|
||||
await click(SAVE_BTN_SELECTOR);
|
||||
|
||||
assert.dom(TOAST_SELECTOR).isVisible();
|
||||
});
|
||||
|
||||
test('user can save custom scopes to add on manage org projects page', async function (assert) {
|
||||
instances.role.update({ grant_scope_ids: [] });
|
||||
await visit(urls.role);
|
||||
|
||||
await click(`[href="${urls.roleScopes}"]`);
|
||||
|
||||
assert.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 0);
|
||||
|
||||
await click(MANAGE_SCOPES_SELECTOR);
|
||||
await click(`[href="${urls.manageOrgProjects}"]`);
|
||||
await a11yAudit();
|
||||
|
||||
// Click three times to select, unselect, then reselect (for coverage)
|
||||
await click(
|
||||
SCOPE_CHECKBOX_SELECTOR('project', instances.scopes.project.id),
|
||||
);
|
||||
await click(
|
||||
SCOPE_CHECKBOX_SELECTOR('project', instances.scopes.project.id),
|
||||
);
|
||||
await click(
|
||||
SCOPE_CHECKBOX_SELECTOR('project', instances.scopes.project.id),
|
||||
);
|
||||
await click(SAVE_BTN_SELECTOR);
|
||||
|
||||
assert.strictEqual(currentURL(), urls.manageScopes);
|
||||
|
||||
await click(SAVE_BTN_SELECTOR);
|
||||
|
||||
assert.strictEqual(currentURL(), urls.roleScopes);
|
||||
assert.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 1);
|
||||
});
|
||||
|
||||
test('user can cancel custom scopes to add on manage org projects page', async function (assert) {
|
||||
instances.role.update({ grant_scope_ids: [] });
|
||||
await visit(urls.role);
|
||||
|
||||
await click(`[href="${urls.roleScopes}"]`);
|
||||
|
||||
assert.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 0);
|
||||
|
||||
await click(MANAGE_SCOPES_SELECTOR);
|
||||
await click(`[href="${urls.manageOrgProjects}"]`);
|
||||
await a11yAudit();
|
||||
|
||||
// Click three times to select, unselect, then reselect (for coverage)
|
||||
await click(
|
||||
SCOPE_CHECKBOX_SELECTOR('project', instances.scopes.project.id),
|
||||
);
|
||||
await click(
|
||||
SCOPE_CHECKBOX_SELECTOR('project', instances.scopes.project.id),
|
||||
);
|
||||
await click(
|
||||
SCOPE_CHECKBOX_SELECTOR('project', instances.scopes.project.id),
|
||||
);
|
||||
await click(CANCEL_BTN_SELECTOR);
|
||||
|
||||
assert.strictEqual(currentURL(), urls.manageScopes);
|
||||
|
||||
await click(CANCEL_BTN_SELECTOR);
|
||||
|
||||
assert.strictEqual(currentURL(), urls.roleScopes);
|
||||
assert.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 0);
|
||||
});
|
||||
|
||||
test('shows error message on custom scope save on manage org projects page', async function (assert) {
|
||||
this.server.post('/roles/:idMethod', () => {
|
||||
return new Response(
|
||||
400,
|
||||
{},
|
||||
{
|
||||
status: 400,
|
||||
code: 'invalid_argument',
|
||||
message: 'The request was invalid.',
|
||||
details: {},
|
||||
},
|
||||
);
|
||||
});
|
||||
instances.role.update({ grant_scope_ids: [] });
|
||||
await visit(urls.role);
|
||||
|
||||
await click(MANAGE_SCOPES_SELECTOR);
|
||||
await click(`[href="${urls.manageOrgProjects}"]`);
|
||||
await click(
|
||||
SCOPE_CHECKBOX_SELECTOR('project', instances.scopes.project.id),
|
||||
);
|
||||
await click(SAVE_BTN_SELECTOR);
|
||||
|
||||
assert.dom(TOAST_SELECTOR).isVisible();
|
||||
});
|
||||
|
||||
test('user can search for a specifc project scope by id on manage org projects page', async function (assert) {
|
||||
const anotherProject = this.server.create('scope', {
|
||||
type: 'project',
|
||||
scope: { id: instances.scopes.org.id, type: 'org' },
|
||||
});
|
||||
await visit(urls.role);
|
||||
|
||||
await click(MANAGE_SCOPES_SELECTOR);
|
||||
await click(`[href="${urls.manageOrgProjects}"]`);
|
||||
|
||||
assert
|
||||
.dom(SCOPE_CHECKBOX_SELECTOR('project', instances.scopes.project.id))
|
||||
.exists();
|
||||
assert.dom(SCOPE_CHECKBOX_SELECTOR('project', anotherProject.id)).exists();
|
||||
|
||||
await fillIn(SEARCH_INPUT_SELECTOR, instances.scopes.project.id);
|
||||
await waitUntil(
|
||||
() =>
|
||||
findAll(SCOPE_CHECKBOX_SELECTOR('project', anotherProject.id))
|
||||
.length === 0,
|
||||
);
|
||||
|
||||
assert
|
||||
.dom(SCOPE_CHECKBOX_SELECTOR('project', instances.scopes.project.id))
|
||||
.exists();
|
||||
assert
|
||||
.dom(SCOPE_CHECKBOX_SELECTOR('project', anotherProject.id))
|
||||
.doesNotExist();
|
||||
});
|
||||
|
||||
test('user can search for project scopes and get no results on manage org projects page', async function (assert) {
|
||||
const anotherProject = this.server.create('scope', {
|
||||
type: 'project',
|
||||
scope: { id: instances.scopes.org.id, type: 'org' },
|
||||
});
|
||||
await visit(urls.role);
|
||||
|
||||
await click(MANAGE_SCOPES_SELECTOR);
|
||||
await click(`[href="${urls.manageOrgProjects}"]`);
|
||||
|
||||
assert
|
||||
.dom(SCOPE_CHECKBOX_SELECTOR('project', instances.scopes.project.id))
|
||||
.exists();
|
||||
assert.dom(SCOPE_CHECKBOX_SELECTOR('project', anotherProject.id)).exists();
|
||||
|
||||
await fillIn(SEARCH_INPUT_SELECTOR, 'fake scope that does not exist');
|
||||
await waitUntil(() => findAll(NO_RESULTS_MSG_SELECTOR).length === 1);
|
||||
|
||||
assert
|
||||
.dom(SCOPE_CHECKBOX_SELECTOR('project', instances.scopes.project.id))
|
||||
.doesNotExist();
|
||||
assert
|
||||
.dom(SCOPE_CHECKBOX_SELECTOR('project', anotherProject.id))
|
||||
.doesNotExist();
|
||||
assert.dom(NO_RESULTS_MSG_SELECTOR).includesText('No results found');
|
||||
});
|
||||
});
|
||||
@@ -1,351 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import {
|
||||
visit,
|
||||
currentURL,
|
||||
click,
|
||||
findAll,
|
||||
fillIn,
|
||||
waitUntil,
|
||||
} from '@ember/test-helpers';
|
||||
import { setupApplicationTest } from 'ember-qunit';
|
||||
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
|
||||
import { setupIndexedDb } from 'api/test-support/helpers/indexed-db';
|
||||
import a11yAudit from 'ember-a11y-testing/test-support/audit';
|
||||
import { authenticateSession } from 'ember-simple-auth/test-support';
|
||||
import {
|
||||
GRANT_SCOPE_THIS,
|
||||
GRANT_SCOPE_CHILDREN,
|
||||
GRANT_SCOPE_DESCENDANTS,
|
||||
} from 'api/models/role';
|
||||
|
||||
module('Acceptance | roles | scopes', function (hooks) {
|
||||
setupApplicationTest(hooks);
|
||||
setupMirage(hooks);
|
||||
setupIndexedDb(hooks);
|
||||
|
||||
const SCOPE_TOGGLE_SELECTOR = (name) =>
|
||||
`.hds-form-toggle input[name="${name}"]`;
|
||||
const SCOPE_CHECKBOX_SELECTOR = (id) =>
|
||||
`tbody [data-test-org-scopes-table-row="${id}"] input`;
|
||||
const TABLE_ROW_SELECTOR = 'tbody tr';
|
||||
const SAVE_BTN_SELECTOR = 'form [type="submit"]';
|
||||
const CANCEL_BTN_SELECTOR = '.rose-form-actions [type="button"]';
|
||||
const MANAGE_SCOPES_SELECTOR = '[data-test-manage-dropdown-scopes]';
|
||||
const TOAST_SELECTOR = '[role="alert"]';
|
||||
const SEARCH_INPUT_SELECTOR = '.search-filtering [type="search"]';
|
||||
const NO_RESULTS_MSG_SELECTOR = '[data-test-no-scope-results]';
|
||||
|
||||
const instances = {
|
||||
scopes: {
|
||||
global: null,
|
||||
org: null,
|
||||
},
|
||||
globalRole: null,
|
||||
orgRole: null,
|
||||
};
|
||||
const urls = {
|
||||
orgScope: null,
|
||||
globalRoles: null,
|
||||
globalRole: null,
|
||||
globalRoleScopes: null,
|
||||
globalManageScopes: null,
|
||||
orgRoles: null,
|
||||
orgRole: null,
|
||||
orgRoleScopes: null,
|
||||
orgManageScopes: null,
|
||||
manageCustomScopes: null,
|
||||
};
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
authenticateSession({});
|
||||
instances.scopes.global = this.server.create('scope', { id: 'global' });
|
||||
instances.scopes.org = this.server.create('scope', {
|
||||
type: 'org',
|
||||
scope: { id: 'global', type: 'global' },
|
||||
});
|
||||
instances.globalRole = this.server.create(
|
||||
'role',
|
||||
{
|
||||
scope: instances.scopes.global,
|
||||
},
|
||||
'withScopes',
|
||||
);
|
||||
instances.orgRole = this.server.create(
|
||||
'role',
|
||||
{
|
||||
scope: instances.scopes.org,
|
||||
},
|
||||
'withScopes',
|
||||
);
|
||||
|
||||
urls.globalRoles = `/scopes/global/roles`;
|
||||
urls.globalRole = `${urls.globalRoles}/${instances.globalRole.id}`;
|
||||
urls.globalRoleScopes = `${urls.globalRole}/scopes`;
|
||||
urls.globalManageScopes = `${urls.globalRole}/manage-scopes`;
|
||||
urls.manageCustomScopes = `${urls.globalManageScopes}/manage-custom-scopes`;
|
||||
urls.orgRoles = `/scopes/${instances.scopes.org.id}/roles`;
|
||||
urls.orgRole = `${urls.orgRoles}/${instances.orgRole.id}`;
|
||||
urls.orgRoleScopes = `${urls.orgRole}/scopes`;
|
||||
urls.orgManageScopes = `${urls.orgRole}/manage-scopes`;
|
||||
});
|
||||
|
||||
test('visiting role scopes', async function (assert) {
|
||||
await visit(urls.orgRoleScopes);
|
||||
await a11yAudit();
|
||||
assert.strictEqual(currentURL(), urls.orgRoleScopes);
|
||||
assert
|
||||
.dom(TABLE_ROW_SELECTOR)
|
||||
.exists({ count: instances.orgRole.grant_scope_ids.length });
|
||||
});
|
||||
|
||||
test('user can naviage to scope from role grant scopes', async function (assert) {
|
||||
await visit(urls.orgRoleScopes);
|
||||
await click('tbody tr:nth-child(2) a');
|
||||
assert.strictEqual(
|
||||
currentURL(),
|
||||
`/scopes/${instances.scopes.org.id}/scopes`,
|
||||
);
|
||||
});
|
||||
|
||||
test('user can naviage to parent scope from role grant scopes', async function (assert) {
|
||||
await visit(urls.orgRoleScopes);
|
||||
await click('tbody tr:nth-child(2) td:nth-child(3) a');
|
||||
assert.strictEqual(
|
||||
currentURL(),
|
||||
`/scopes/${instances.scopes.global.id}/scopes`,
|
||||
);
|
||||
});
|
||||
|
||||
test('user sees no scopes message and action when role has no grant scopes', async function (assert) {
|
||||
instances.orgRole.update({ grant_scope_ids: [] });
|
||||
await visit(urls.orgRoleScopes);
|
||||
assert.dom('.role-grant-scopes div').includesText('No scopes added');
|
||||
assert.dom('.role-grant-scopes div div:nth-child(3) a').isVisible();
|
||||
});
|
||||
|
||||
test('user does not see action to add scopes when role has no grant scopes without proper permissions', async function (assert) {
|
||||
instances.orgRole.update({
|
||||
grant_scope_ids: [],
|
||||
authorized_actions: instances.orgRole.authorized_actions.filter(
|
||||
(action) => action !== 'set-grant-scopes',
|
||||
),
|
||||
});
|
||||
await visit(urls.orgRoleScopes);
|
||||
assert.dom('.role-grant-scopes div div:nth-child(3) a').doesNotExist();
|
||||
});
|
||||
|
||||
test('user can toggle and save scope keywords to add on manage scopes page', async function (assert) {
|
||||
instances.orgRole.update({ grant_scope_ids: [] });
|
||||
await visit(urls.orgRole);
|
||||
|
||||
await click(`[href="${urls.orgRoleScopes}"]`);
|
||||
|
||||
assert.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 0);
|
||||
|
||||
await click(MANAGE_SCOPES_SELECTOR);
|
||||
|
||||
assert.strictEqual(currentURL(), urls.orgManageScopes);
|
||||
|
||||
// Click three times to select, unselect, then reselect (for coverage)
|
||||
await click(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_THIS));
|
||||
await click(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_THIS));
|
||||
await click(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_THIS));
|
||||
await click(SAVE_BTN_SELECTOR);
|
||||
|
||||
assert.strictEqual(currentURL(), urls.orgRoleScopes);
|
||||
assert.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 1);
|
||||
});
|
||||
|
||||
test('correct toggles are visible for global level role on manage scopes page', async function (assert) {
|
||||
await visit(urls.globalRole);
|
||||
|
||||
await click(MANAGE_SCOPES_SELECTOR);
|
||||
|
||||
assert.strictEqual(currentURL(), urls.globalManageScopes);
|
||||
assert.dom(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_THIS)).isVisible();
|
||||
assert.dom(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_CHILDREN)).isVisible();
|
||||
assert.dom(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_DESCENDANTS)).isVisible();
|
||||
});
|
||||
|
||||
test('correct toggles are visible for org level role on manage scopes page', async function (assert) {
|
||||
await visit(urls.orgRole);
|
||||
|
||||
await click(MANAGE_SCOPES_SELECTOR);
|
||||
|
||||
assert.strictEqual(currentURL(), urls.orgManageScopes);
|
||||
assert.dom(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_THIS)).isVisible();
|
||||
assert.dom(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_CHILDREN)).isVisible();
|
||||
assert.dom(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_DESCENDANTS)).doesNotExist();
|
||||
});
|
||||
|
||||
test('user can toggle and cancel scope keywords to add on manage scopes page', async function (assert) {
|
||||
instances.orgRole.update({ grant_scope_ids: [] });
|
||||
await visit(urls.orgRole);
|
||||
|
||||
await click(`[href="${urls.orgRoleScopes}"]`);
|
||||
|
||||
assert.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 0);
|
||||
|
||||
await click(MANAGE_SCOPES_SELECTOR);
|
||||
|
||||
assert.strictEqual(currentURL(), urls.orgManageScopes);
|
||||
|
||||
// Click three times to select, unselect, then reselect (for coverage)
|
||||
await click(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_THIS));
|
||||
await click(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_THIS));
|
||||
await click(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_THIS));
|
||||
await click(CANCEL_BTN_SELECTOR);
|
||||
|
||||
assert.strictEqual(currentURL(), urls.orgRoleScopes);
|
||||
assert.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 0);
|
||||
});
|
||||
|
||||
test('shows error message on scope save on manage scopes page', async function (assert) {
|
||||
this.server.post('/roles/:idMethod', () => {
|
||||
return new Response(
|
||||
400,
|
||||
{},
|
||||
{
|
||||
status: 400,
|
||||
code: 'invalid_argument',
|
||||
message: 'The request was invalid.',
|
||||
details: {},
|
||||
},
|
||||
);
|
||||
});
|
||||
instances.orgRole.update({ grant_scope_ids: [] });
|
||||
await visit(urls.orgRole);
|
||||
|
||||
await click(MANAGE_SCOPES_SELECTOR);
|
||||
await click(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_THIS));
|
||||
await click(SAVE_BTN_SELECTOR);
|
||||
|
||||
assert.dom(TOAST_SELECTOR).isVisible();
|
||||
});
|
||||
|
||||
test('user can select and save custom scopes to add on manage custom scopes page', async function (assert) {
|
||||
instances.globalRole.update({ grant_scope_ids: [] });
|
||||
await visit(urls.globalRole);
|
||||
|
||||
await click(`[href="${urls.globalRoleScopes}"]`);
|
||||
|
||||
assert.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 0);
|
||||
|
||||
await click(MANAGE_SCOPES_SELECTOR);
|
||||
|
||||
assert.strictEqual(currentURL(), urls.globalManageScopes);
|
||||
|
||||
await click(`[href="${urls.manageCustomScopes}"]`);
|
||||
|
||||
// Click three times to select, unselect, then reselect (for coverage)
|
||||
await click(SCOPE_CHECKBOX_SELECTOR(instances.scopes.org.id));
|
||||
await click(SCOPE_CHECKBOX_SELECTOR(instances.scopes.org.id));
|
||||
await click(SCOPE_CHECKBOX_SELECTOR(instances.scopes.org.id));
|
||||
await click(SAVE_BTN_SELECTOR);
|
||||
|
||||
assert.strictEqual(currentURL(), urls.globalManageScopes);
|
||||
|
||||
await click(SAVE_BTN_SELECTOR);
|
||||
|
||||
assert.strictEqual(currentURL(), urls.globalRoleScopes);
|
||||
assert.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 1);
|
||||
});
|
||||
|
||||
test('user can select and cancel custom scopes to add on manage custom scopes page', async function (assert) {
|
||||
instances.globalRole.update({ grant_scope_ids: [] });
|
||||
await visit(urls.globalRole);
|
||||
|
||||
await click(`[href="${urls.globalRoleScopes}"]`);
|
||||
|
||||
assert.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 0);
|
||||
|
||||
await click(MANAGE_SCOPES_SELECTOR);
|
||||
|
||||
assert.strictEqual(currentURL(), urls.globalManageScopes);
|
||||
|
||||
await click(`[href="${urls.manageCustomScopes}"]`);
|
||||
|
||||
// Click three times to select, unselect, then reselect (for coverage)
|
||||
await click(SCOPE_CHECKBOX_SELECTOR(instances.scopes.org.id));
|
||||
await click(SCOPE_CHECKBOX_SELECTOR(instances.scopes.org.id));
|
||||
await click(SCOPE_CHECKBOX_SELECTOR(instances.scopes.org.id));
|
||||
await click(CANCEL_BTN_SELECTOR);
|
||||
|
||||
assert.strictEqual(currentURL(), urls.globalManageScopes);
|
||||
|
||||
await click(CANCEL_BTN_SELECTOR);
|
||||
|
||||
assert.strictEqual(currentURL(), urls.globalRoleScopes);
|
||||
assert.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 0);
|
||||
});
|
||||
|
||||
test('shows error message on custom scope save on manage custom scopes page', async function (assert) {
|
||||
this.server.post('/roles/:idMethod', () => {
|
||||
return new Response(
|
||||
400,
|
||||
{},
|
||||
{
|
||||
status: 400,
|
||||
code: 'invalid_argument',
|
||||
message: 'The request was invalid.',
|
||||
details: {},
|
||||
},
|
||||
);
|
||||
});
|
||||
instances.globalRole.update({ grant_scope_ids: [] });
|
||||
await visit(urls.globalRole);
|
||||
|
||||
await click(MANAGE_SCOPES_SELECTOR);
|
||||
await click(`[href="${urls.manageCustomScopes}"]`);
|
||||
await click(SCOPE_CHECKBOX_SELECTOR(instances.scopes.org.id));
|
||||
await click(SAVE_BTN_SELECTOR);
|
||||
|
||||
assert.dom(TOAST_SELECTOR).isVisible();
|
||||
});
|
||||
|
||||
test('user can search for a specifc org scope by id on manage custom scopes page', async function (assert) {
|
||||
const anotherOrg = this.server.create('scope', {
|
||||
type: 'org',
|
||||
scope: { id: 'global', type: 'global' },
|
||||
});
|
||||
await visit(urls.globalManageScopes);
|
||||
|
||||
await click(`[href="${urls.manageCustomScopes}"]`);
|
||||
|
||||
assert.dom(SCOPE_CHECKBOX_SELECTOR(instances.scopes.org.id)).exists();
|
||||
assert.dom(SCOPE_CHECKBOX_SELECTOR(anotherOrg.id)).exists();
|
||||
|
||||
await fillIn(SEARCH_INPUT_SELECTOR, instances.scopes.org.id);
|
||||
await waitUntil(
|
||||
() => findAll(SCOPE_CHECKBOX_SELECTOR(anotherOrg.id)).length === 0,
|
||||
);
|
||||
|
||||
assert.dom(SCOPE_CHECKBOX_SELECTOR(instances.scopes.org.id)).exists();
|
||||
assert.dom(SCOPE_CHECKBOX_SELECTOR(anotherOrg.id)).doesNotExist();
|
||||
});
|
||||
|
||||
test('user can search for org scopes and get no results on manage custom scopes page', async function (assert) {
|
||||
const anotherOrg = this.server.create('scope', {
|
||||
type: 'org',
|
||||
scope: { id: 'global', type: 'global' },
|
||||
});
|
||||
await visit(urls.globalManageScopes);
|
||||
|
||||
await click(`[href="${urls.manageCustomScopes}"]`);
|
||||
|
||||
assert.dom(SCOPE_CHECKBOX_SELECTOR(instances.scopes.org.id)).exists();
|
||||
assert.dom(SCOPE_CHECKBOX_SELECTOR(anotherOrg.id)).exists();
|
||||
|
||||
await fillIn(SEARCH_INPUT_SELECTOR, 'fake scope that does not exist');
|
||||
await waitUntil(() => findAll(NO_RESULTS_MSG_SELECTOR).length === 1);
|
||||
|
||||
assert.dom(`[href="${urls.role1}"]`).doesNotExist();
|
||||
assert.dom(`[href="${urls.role2}"]`).doesNotExist();
|
||||
assert.dom(NO_RESULTS_MSG_SELECTOR).includesText('No results found');
|
||||
});
|
||||
});
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
import { currentURL, visit, waitUntil } from '@ember/test-helpers';
|
||||
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
|
||||
import { setupIndexedDb } from 'api/test-support/helpers/indexed-db';
|
||||
import { authenticateSession } from 'ember-simple-auth/test-support';
|
||||
|
||||
module(
|
||||
'Unit | Controller | scopes/scope/roles/role/manage-scopes/manage-org-projects',
|
||||
function (hooks) {
|
||||
setupTest(hooks);
|
||||
setupMirage(hooks);
|
||||
setupIndexedDb(hooks);
|
||||
|
||||
let store;
|
||||
let controller;
|
||||
|
||||
const instances = {
|
||||
scopes: {
|
||||
global: null,
|
||||
org: null,
|
||||
project: null,
|
||||
},
|
||||
role: null,
|
||||
};
|
||||
|
||||
const urls = {
|
||||
globalScope: null,
|
||||
manageOrgProjects: null,
|
||||
manageCustomScopes: null,
|
||||
};
|
||||
|
||||
hooks.beforeEach(async function () {
|
||||
authenticateSession({});
|
||||
store = this.owner.lookup('service:store');
|
||||
controller = this.owner.lookup(
|
||||
'controller:scopes/scope/roles/role/manage-scopes/manage-org-projects',
|
||||
);
|
||||
|
||||
instances.scopes.global = this.server.create('scope', {
|
||||
id: 'global',
|
||||
type: 'global',
|
||||
});
|
||||
instances.scopes.org = this.server.create('scope', {
|
||||
type: 'org',
|
||||
scope: { id: 'global', type: 'global' },
|
||||
});
|
||||
instances.scopes.project = this.server.create('scope', {
|
||||
type: 'project',
|
||||
scope: { id: instances.scopes.org.id, type: 'org' },
|
||||
});
|
||||
instances.role = this.server.create('role', {
|
||||
scope: instances.scopes.global,
|
||||
grant_scope_ids: [],
|
||||
});
|
||||
|
||||
urls.globalScope = `/scopes/global`;
|
||||
urls.manageOrgProjects = `${urls.globalScope}/roles/${instances.role.id}/manage-scopes/${instances.scopes.org.id}`;
|
||||
urls.manageCustomScopes = `${urls.globalScope}/roles/${instances.role.id}/manage-scopes/manage-custom-scopes`;
|
||||
});
|
||||
|
||||
test('it exists', function (assert) {
|
||||
assert.ok(controller);
|
||||
});
|
||||
|
||||
test('handleSearchInput action sets expected values correctly', async function (assert) {
|
||||
const searchValue = 'test';
|
||||
controller.handleSearchInput({ target: { value: searchValue } });
|
||||
await waitUntil(() => controller.search === searchValue);
|
||||
|
||||
assert.strictEqual(controller.page, 1);
|
||||
assert.strictEqual(controller.search, searchValue);
|
||||
});
|
||||
|
||||
test('setGrantScopes action adds scopes to specified model', async function (assert) {
|
||||
await visit(urls.manageOrgProjects);
|
||||
const role = await store.findRecord('role', instances.role.id);
|
||||
|
||||
assert.deepEqual(role.grant_scope_ids, []);
|
||||
|
||||
await controller.setGrantScopes(role, [instances.scopes.project.id]);
|
||||
|
||||
assert.deepEqual(role.grant_scope_ids, [instances.scopes.project.id]);
|
||||
|
||||
await waitUntil(() => currentURL() === urls.manageCustomScopes);
|
||||
|
||||
assert.strictEqual(currentURL(), urls.manageCustomScopes);
|
||||
});
|
||||
|
||||
test('cancel action causes transition to expected route', async function (assert) {
|
||||
await visit(urls.manageOrgProjects);
|
||||
const role = await store.findRecord('role', instances.role.id);
|
||||
|
||||
await controller.cancel(role);
|
||||
await waitUntil(() => currentURL() === urls.manageCustomScopes);
|
||||
|
||||
assert.strictEqual(currentURL(), urls.manageCustomScopes);
|
||||
});
|
||||
},
|
||||
);
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
|
||||
module(
|
||||
'Unit | Route | scopes/scope/roles/role/manage-scopes/manage-org-projects',
|
||||
function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test('it exists', function (assert) {
|
||||
let route = this.owner.lookup(
|
||||
'route:scopes/scope/roles/role/manage-scopes/manage-org-projects',
|
||||
);
|
||||
assert.ok(route);
|
||||
});
|
||||
},
|
||||
);
|
||||
Reference in new issue
Block a user