Icu 13302 add filtering pagination support for scopes (#2216)

* feat: 🎸 add filtering and pagination to scopes

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13302
This commit is contained in:
Lisbet Alvarez authored and Zhihe Li committed 2024-04-12 14:55:34 -04:00
1 parent ece9c9b0c8
commit 1d7892230c
36 files changed
+521 -157

No files matched your search

+13 -14
View File
@@ -4,13 +4,15 @@
*/
import GeneratedScopeModel from '../generated/models/scope';
import { computed } from '@ember/object';
export const scopeTypes = {
global: 'global',
org: 'org',
project: 'project',
};
export const TYPE_SCOPE_GLOBAL = 'global';
export const TYPE_SCOPE_ORG = 'org';
export const TYPE_SCOPE_PROJECT = 'project';
export const TYPES_SCOPE = Object.freeze([
TYPE_SCOPE_GLOBAL,
TYPE_SCOPE_ORG,
TYPE_SCOPE_PROJECT,
]);
export default class ScopeModel extends GeneratedScopeModel {
// =attributes
@@ -18,9 +20,8 @@ export default class ScopeModel extends GeneratedScopeModel {
/**
* @type {boolean}
*/
@computed('type')
get isGlobal() {
return this.type === scopeTypes.global;
return this.type === TYPE_SCOPE_GLOBAL;
}
// There is only one global scope and it cannot be created by clients,
// thus no set.
@@ -28,23 +29,21 @@ export default class ScopeModel extends GeneratedScopeModel {
/**
* @type {boolean}
*/
@computed('type')
get isOrg() {
return this.type === scopeTypes.org;
return this.type === TYPE_SCOPE_ORG;
}
set isOrg(value) {
if (value) this.type = scopeTypes.org;
if (value) this.type = TYPE_SCOPE_ORG;
}
/**
* @type {boolean}
*/
@computed('type')
get isProject() {
return this.type === scopeTypes.project;
return this.type === TYPE_SCOPE_PROJECT;
}
set isProject(value) {
if (value) this.type = scopeTypes.project;
if (value) this.type = TYPE_SCOPE_PROJECT;
}
/**
+2
View File
@@ -16,6 +16,8 @@ export const modelIndexes = {
group:
'&id, attributes.created_time, attributes.name, attributes.description, attributes.scope.scope_id',
role: '&id, attributes.created_time, attributes.name, attributes.description, attributes.scope.scope_id',
scope:
'&id, attributes.created_time, attributes.type, attributes.name, attributes.description, attributes.scope.scope_id',
'credential-store':
'&id, attributes.created_time, attributes.name, attributes.description, attributes.type, attributes.scope.scope_id',
'host-catalog':
@@ -16,6 +16,7 @@
{{#if (can 'addBrokeredCredentialSources target' @model)}}
<dropdown.link
@route='scopes.scope.targets.target.add-brokered-credential-sources'
data-test-add-brokered-cred-sources-action
>
{{t 'resources.target.actions.add-brokered-credential-sources'}}
</dropdown.link>
@@ -24,6 +25,7 @@
{{#if (can 'addInjectedApplicationCredentialSources target' @model)}}
<dropdown.link
@route='scopes.scope.targets.target.add-injected-application-credential-sources'
data-test-add-injected-cred-sources-action
>
{{t
'resources.target.actions.add-injected-application-credential-sources'
@@ -0,0 +1,28 @@
import Controller from '@ember/controller';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';
import { debounce } from 'core/decorators/debounce';
export default class ScopesScopeScopesIndexController extends Controller {
// =attributes
queryParams = ['search', 'page', 'pageSize'];
@tracked search = '';
@tracked page = 1;
@tracked pageSize = 10;
// =methods
/**
* 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;
}
}
+2
View File
@@ -9,6 +9,7 @@ import { action } from '@ember/object';
import { loading } from 'ember-loading';
import { notifyError } from 'core/decorators/notify';
import { TYPE_TARGET_TCP } from 'api/models/target';
export default class OnboardingRoute extends Route {
// =services
@@ -25,6 +26,7 @@ export default class OnboardingRoute extends Route {
async beforeModel() {
const orgs = await this.store.query('scope', {
scope_id: 'global',
query: { filters: { scope_id: [{ equals: 'global' }] } },
});
if (!this.session.isAuthenticated || orgs.length)
this.router.transitionTo('index');
+7 -3
View File
@@ -33,9 +33,13 @@ export default class ScopesRoute extends Route {
await this.store.findRecord('scope', 'global').catch(() => {
/* no op */
});
// NOTE: In the absence of a `scope_id` query parameter, this endpoint is
// expected to default to the global scope, thus returning org scopes.
return this.store.query('scope', {}).catch(() => A([]));
// Return all org scopes.
return this.store
.query('scope', {
scope_id: 'global',
query: { filters: { scope_id: [{ equals: 'global' }] } },
})
.catch(() => A([]));
}
/**
+11 -5
View File
@@ -11,6 +11,7 @@ import { next } from '@ember/runloop';
import { loading } from 'ember-loading';
import { confirm } from 'core/decorators/confirm';
import { notifySuccess, notifyError } from 'core/decorators/notify';
import { TYPE_SCOPE_GLOBAL, TYPE_SCOPE_ORG } from 'api/models/scope';
export default class ScopesScopeRoute extends Route {
// =services
@@ -36,7 +37,7 @@ export default class ScopesScopeRoute extends Route {
async model({ scope_id: id }) {
// Since only global and org scopes are authenticatable, we can infer type
// from ID because global has a fixed ID.
const type = id === 'global' ? 'global' : 'org';
const type = id === 'global' ? TYPE_SCOPE_GLOBAL : TYPE_SCOPE_ORG;
return this.store.findRecord('scope', id).catch(() => {
const maybeExistingScope = this.store.peekRecord('scope', id);
const scopeOptions = { id, type };
@@ -55,11 +56,17 @@ export default class ScopesScopeRoute extends Route {
// First, load orgs and, if necessary, projects
let orgs, projects;
orgs = await this.store
.query('scope', { scope_id: 'global' })
.query('scope', {
scope_id: 'global',
query: { filters: { scope_id: [{ equals: 'global' }] } },
})
.catch(() => A([]));
if (model.isProject) {
projects = await this.store.query('scope', { scope_id: model.scopeID });
projects = await this.store.query('scope', {
scope_id: model.scopeID,
query: { filters: { scope_id: [{ equals: model.scopeID }] } },
});
}
// Then pull out the "selected" scopes, if relevant
let selectedOrg, selectedProject;
@@ -144,7 +151,6 @@ export default class ScopesScopeRoute extends Route {
async delete(scope) {
const { scopeID } = scope;
await scope.destroyRecord();
await this.router.replaceWith('scopes.scope.scopes', scopeID);
//this.refresh();
this.router.replaceWith('scopes.scope.scopes', scopeID);
}
}
@@ -22,12 +22,16 @@ export default class ScopesScopeRolesRoleIndexRoute extends Route {
if (!currentScope.isProject) {
const scopes = await this.store.query('scope', {
scope_id: currentScope.id,
query: { filters: { scope_id: [{ equals: currentScope.id }] } },
});
subScopes = await Promise.all(
scopes.map(async (scope) => ({
model: scope,
subScopes: !scope.isProject
? await this.store.query('scope', { scope_id: scope.id })
? await this.store.query('scope', {
scope_id: scope.id,
query: { filters: { scope_id: [{ equals: scope.id }] } },
})
: [],
})),
);
@@ -14,7 +14,6 @@ import { inject as service } from '@ember/service';
export default class ScopesScopeScopesRoute extends Route {
// =services
@service store;
@service session;
@service router;
@@ -26,23 +25,4 @@ export default class ScopesScopeScopesRoute extends Route {
beforeModel() {
if (!this.session.isAuthenticated) this.router.transitionTo('index');
}
/**
* Loads sub scopes for the current scope.
* @return {Promise}
*/
async model() {
const currentScope = this.modelFor('scopes.scope');
const parentScope = !currentScope.isGlobal
? await this.store.findRecord('scope', currentScope.scopeID)
: null;
const subScopes = await this.store.query('scope', {
scope_id: currentScope.id,
});
return {
currentScope,
parentScope,
subScopes,
};
}
}
@@ -4,5 +4,84 @@
*/
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
export default class ScopesScopeScopesIndexRoute extends Route {}
export default class ScopesScopeScopesIndexRoute extends Route {
// =attributes
queryParams = {
search: {
refreshModel: true,
replace: true,
},
page: {
refreshModel: true,
},
pageSize: {
refreshModel: true,
},
};
// =services
@service can;
@service store;
// =methods
/**
* Loads sub scopes for the current scope.
* @returns {Promise<{totalItems: number, currentScope: ScopeModel, subScopes: [ScopeModel], scopesExist: boolean }> }
*/
async model({ search, page, pageSize }) {
const currentScope = this.modelFor('scopes.scope');
const { id: scope_id } = currentScope;
const filters = {
scope_id: [{ equals: scope_id }],
};
const subScopes = await this.store.query('scope', {
scope_id,
query: { search, filters },
page,
pageSize,
});
const totalItems = subScopes.meta?.totalItems;
const scopesExist = await this.getScopesExist(scope_id, totalItems);
return {
currentScope,
subScopes,
scopesExist,
totalItems,
};
}
/**
* Sets scopesExist to true if there exists any scopes.
* @param {string} scope_id
* @param {number} totalItems
* @returns {Promise<boolean>}
*/
async getScopesExist(scope_id, totalItems) {
if (totalItems > 0) {
return true;
}
const options = { pushToStore: false };
const scopes = await this.store.query(
'scope',
{
scope_id,
query: {
filters: {
scope_id: [{ equals: scope_id }],
},
},
page: 1,
pageSize: 1,
},
options,
);
return scopes.length > 0;
}
}
@@ -78,7 +78,10 @@ export default class ScopesScopeStorageBucketsNewRoute extends Route {
async afterModel() {
let scopes;
const orgScopes = (
await this.store.query('scope', { scope_id: 'global' })
await this.store.query('scope', {
scope_id: 'global',
query: { filters: { scope_id: [{ equals: 'global' }] } },
})
).map((scope) => ({ model: scope }));
scopes = [
{ model: this.store.peekRecord('scope', 'global') },
@@ -16,7 +16,10 @@ export default class ScopesScopeStorageBucketsStorageBucketIndexRoute extends Ro
async afterModel() {
let scopes;
const orgScopes = (
await this.store.query('scope', { scope_id: 'global' })
await this.store.query('scope', {
scope_id: 'global',
query: { filters: { scope_id: [{ equals: 'global' }] } },
})
).map((scope) => ({ model: scope }));
scopes = [
{ model: this.store.peekRecord('scope', 'global') },
@@ -5,13 +5,13 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import { notifySuccess, notifyError } from 'core/decorators/notify';
import { loading } from 'ember-loading';
import {
TYPE_STORAGE_BUCKET_PLUGIN,
TYPE_STORAGE_BUCKET_PLUGIN_AWS_S3,
} from 'api/models/storage-bucket';
import { action } from '@ember/object';
import { notifySuccess, notifyError } from 'core/decorators/notify';
import { loading } from 'ember-loading';
export default class ScopesScopeTargetsTargetEnableSessionRecordingCreateStorageBucketRoute extends Route {
// =services
@@ -37,7 +37,10 @@ export default class ScopesScopeTargetsTargetEnableSessionRecordingCreateStorage
async afterModel() {
let scopes;
const orgScopes = (
await this.store.query('scope', { scope_id: 'global' })
await this.store.query('scope', {
scope_id: 'global',
query: { filters: { scope_id: [{ equals: 'global' }] } },
})
).map((scope) => ({ model: scope }));
scopes = [
{ model: this.store.peekRecord('scope', 'global') },
@@ -15,7 +15,7 @@
{{t 'resources.org.title_plural'}}
<DocLink @doc='org' @iconSize='large' />
</h2>
{{#if @model.subScopes}}
{{#if @model.scopesExist}}
<p>{{t 'resources.org.description'}}</p>
{{/if}}
{{else if @model.currentScope.isOrg}}
@@ -23,7 +23,7 @@
{{t 'resources.project.title_plural'}}
<DocLink @doc='project' @iconSize='large' />
</h2>
{{#if @model.subScopes}}
{{#if @model.scopesExist}}
<p>{{t 'resources.project.description'}}</p>
{{/if}}
{{/if}}
@@ -31,79 +31,115 @@
<page.actions>
{{#if (can 'create model' @model.currentScope collection='scopes')}}
{{#if @model.subScopes}}
<Rose::LinkButton @route='scopes.scope.scopes.new' @style='primary'>
{{#if @model.currentScope.isGlobal}}
{{t 'resources.org.titles.new'}}
{{else if @model.currentScope.isOrg}}
{{t 'resources.project.titles.new'}}
{{/if}}
</Rose::LinkButton>
{{#if @model.scopesExist}}
<Hds::Button
@route='scopes.scope.scopes.new'
@text={{t
(if
@model.currentScope.isGlobal
'resources.org.titles.new'
'resources.project.titles.new'
)
}}
/>
{{/if}}
{{/if}}
</page.actions>
<page.body>
<page.body class='search-filtering'>
{{#if @model.subScopes}}
<Rose::Cards as |cards|>
{{#each @model.subScopes as |scope|}}
<cards.link
@title={{scope.name}}
@description={{scope.description}}
@id={{scope.id}}
@icon={{if
scope.isOrg
'flight-icons/svg/org-16'
'flight-icons/svg/grid-16'
{{#if @model.scopesExist}}
<Hds::SegmentedGroup as |S|>
<S.TextInput
@value={{this.search}}
@type='search'
placeholder={{t 'actions.search'}}
aria-label={{t 'actions.search'}}
{{on 'input' this.handleSearchInput}}
/>
</Hds::SegmentedGroup>
{{#if @model.subScopes}}
<Hds::Table
@columns={{array
(hash label=(t 'form.name.label'))
(hash label=(t 'form.id.label'))
}}
@model={{@model.subScopes}}
@valign='middle'
>
<:body as |B|>
<B.Tr data-test-scopes-table-row={{B.data.id}}>
<B.Td>
{{#if (can 'read model' B.data)}}
<LinkTo @route='scopes.scope' @model={{B.data.id}}>
{{B.data.displayName}}
</LinkTo>
{{else}}
<Hds::Text::Body @tag='p'>
{{B.data.displayName}}
</Hds::Text::Body>
{{/if}}
<Hds::Text::Body @tag='p'>
{{B.data.description}}
</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
@totalItems={{@model.totalItems}}
@currentPage={{this.page}}
@currentPageSize={{this.pageSize}}
/>
{{else}}
<Hds::ApplicationState data-test-no-scope-results as |A|>
<A.Header @title={{t 'titles.no-results-found'}} />
<A.Body
@text={{t
'descriptions.no-search-results'
query=this.search
resource=(t 'resources.scope.title_plural')
}}
@route='scopes.scope'
@model={{scope.id}}
/>
{{/each}}
</Rose::Cards>
</Hds::ApplicationState>
{{/if}}
{{else}}
{{#if @model.currentScope.isGlobal}}
<Rose::Layout::Centered>
<Rose::Message
@title={{t 'resources.org.messages.welcome.title'}}
as |message|
>
<message.description>
{{t 'resources.org.description'}}
</message.description>
{{#if (can 'create model' @model.currentScope collection='scopes')}}
<message.link @route='scopes.scope.scopes.new'>
<Rose::Icon @name='flight-icons/svg/plus-circle-16' />
{{t 'titles.new'}}
</message.link>
{{/if}}
</Rose::Message>
</Rose::Layout::Centered>
{{/if}}
{{#if @model.currentScope.isOrg}}
<Rose::Layout::Centered>
<Rose::Message
@title={{t 'resources.project.messages.welcome.title'}}
as |message|
>
<message.description>
{{t 'resources.project.description'}}
</message.description>
{{#if (can 'create model' @model.currentScope collection='scopes')}}
<message.link @route='scopes.scope.scopes.new'>
<Rose::Icon @name='flight-icons/svg/plus-circle-16' />
{{t 'titles.new'}}
</message.link>
{{/if}}
</Rose::Message>
</Rose::Layout::Centered>
{{/if}}
<Hds::ApplicationState as |A|>
<A.Header
@title={{t
(if
@model.currentScope.isGlobal
'resources.org.messages.welcome.title'
'resources.project.messages.welcome.title'
)
}}
/>
<A.Body
@text={{t
(if
@model.currentScope.isGlobal
'resources.org.description'
'resources.project.description'
)
}}
/>
{{#if (can 'create model' @model.currentScope collection='scopes')}}
<A.Footer as |F|>
<F.Link::Standalone
@icon='plus-circle'
@text={{t 'titles.new'}}
@route='scopes.scope.scopes.new'
/>
</A.Footer>
{{/if}}
</Hds::ApplicationState>
{{/if}}
</page.body>
@@ -7,6 +7,7 @@ import { module, test } from 'qunit';
import { visit, currentURL, click, find, fillIn } 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 { Response } from 'miragejs';
import {
@@ -19,6 +20,7 @@ import {
module('Acceptance | accounts | change password', function (hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
setupIndexedDb(hooks);
const instances = {
scopes: {
@@ -7,6 +7,7 @@ import { module, test } from 'qunit';
import { visit, currentURL, click, fillIn, select } 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 { Response } from 'miragejs';
import {
authenticateSession,
@@ -18,6 +19,8 @@ import {
module('Acceptance | auth-methods | create', function (hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
setupIndexedDb(hooks);
const DROPDOWN_SELECTOR_ICON =
'tbody .hds-table__tr:nth-child(1) .hds-table__td:last-child .hds-dropdown-toggle-icon';
const DROPDOWN_SELECTOR_OPTION =
@@ -7,6 +7,7 @@ import { module, test } from 'qunit';
import { visit, find } 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 {
authenticateSession,
// These are left here intentionally for future reference.
@@ -17,6 +18,7 @@ import {
module('Acceptance | auth-methods | list', function (hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
setupIndexedDb(hooks);
const instances = {
scopes: {
@@ -7,6 +7,7 @@ import { module, test } from 'qunit';
import { visit, currentURL, click } 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 {
@@ -18,6 +19,8 @@ import {
module('Acceptance | auth-methods | read', function (hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
setupIndexedDb(hooks);
const AUTH_LINK_SELECTOR = 'main tbody .hds-table__td:nth-child(1) a';
const AUTH_TYPE_SELECTOR =
'tbody .hds-table__tr:nth-child(1) .hds-table__td:nth-child(2)';
@@ -15,6 +15,7 @@ import {
} 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 { Response } from 'miragejs';
import { runAllJobs } from 'ember-pollster/test-support';
import a11yAudit from 'ember-a11y-testing/test-support/audit';
@@ -32,6 +33,7 @@ import {
module('Acceptance | authentication', function (hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
setupIndexedDb(hooks);
let indexURL;
let globalScope;
@@ -7,11 +7,13 @@ import { module, test } from 'qunit';
import { visit, click } from '@ember/test-helpers';
import { setupApplicationTest } from 'admin/tests/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('Acceptance | policies | delete', function (hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
setupIndexedDb(hooks);
let featuresService;
let policyCount;
@@ -7,11 +7,13 @@ import { module, test } from 'qunit';
import { visit, click, currentURL, find } from '@ember/test-helpers';
import { setupApplicationTest } from 'admin/tests/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('Acceptance | policies | list', function (hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
setupIndexedDb(hooks);
let featuresService;
let intl;
@@ -7,12 +7,14 @@ import { module, test } from 'qunit';
import { visit, currentURL, click } from '@ember/test-helpers';
import { setupApplicationTest } from 'admin/tests/helpers';
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';
module('Acceptance | policies | read', function (hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
setupIndexedDb(hooks);
let features;
+70 -39
View File
@@ -7,6 +7,7 @@ import { module, test } from 'qunit';
import { visit, currentURL, click, fillIn } 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 { Response } from 'miragejs';
import {
@@ -19,6 +20,15 @@ import {
module('Acceptance | scopes', function (hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
setupIndexedDb(hooks);
const ORG_SCOPES_DROPDOWN_SELECTOR = (url) =>
`.rose-header-nav .rose-dropdown [href="${url}"]`;
const PROJECT_SCOPES_DROPDOWN_SELECTOR = (url) =>
`.rose-header-nav .rose-dropdown + .rose-dropdown [href="${url}"]`;
const SAVE_BUTTON_SELECTOR = '[type="submit"]';
const CANCEL_BUTTON_SELECTOR = '.rose-form-actions [type="button"]';
const NAME_FIELD_SELECTOR = '[name="name"]';
let getScopeCount;
@@ -34,10 +44,14 @@ module('Acceptance | scopes', function (hooks) {
const urls = {
globalScope: null,
orgScope: null,
orgScopes: null,
newOrgScope: null,
org2Scope: null,
org2Scopes: null,
orgScopeEdit: null,
org2ScopeEdit: null,
projectScope: null,
newProjectScope: null,
project2Scope: null,
projectScopeEdit: null,
project2ScopeEdit: null,
@@ -64,19 +78,21 @@ module('Acceptance | scopes', function (hooks) {
});
// Generate route URLs for resources
urls.globalScope = `/scopes/global/scopes`;
urls.newOrgScope = `/scopes/global/scopes/new`;
urls.orgScope = `/scopes/${instances.scopes.org.id}/scopes`;
urls.org2Scope = `/scopes/${instances.scopes.org2.id}/scopes`;
urls.orgScopeEdit = `/scopes/${instances.scopes.org.id}/edit`;
urls.org2ScopeEdit = `/scopes/${instances.scopes.org2.id}/edit`;
urls.newProjectScope = `/scopes/${instances.scopes.org.id}/scopes/new`;
urls.newOrgScope = `${urls.globalScope}/new`;
urls.orgScope = `/scopes/${instances.scopes.org.id}`;
urls.orgScopes = `${urls.orgScope}/scopes`;
urls.org2Scope = `/scopes/${instances.scopes.org2.id}`;
urls.org2Scopes = `${urls.org2Scope}/scopes`;
urls.orgScopeEdit = `${urls.orgScope}/edit`;
urls.org2ScopeEdit = `${urls.org2Scope}/edit`;
urls.newProjectScope = `${urls.orgScopes}/new`;
urls.projectScope = `/scopes/${instances.scopes.project.id}`;
urls.project2Scope = `/scopes/${instances.scopes.project2.id}`;
urls.projectScopeEdit = `/scopes/${instances.scopes.project.id}/edit`;
urls.project2ScopeEdit = `/scopes/${instances.scopes.project2.id}/edit`;
// Generate resource couner
urls.projectScopeEdit = `${urls.projectScope}/edit`;
urls.project2ScopeEdit = `${urls.project2Scope}/edit`;
// Generate resource counter
getScopeCount = (type) => this.server.schema.scopes.where({ type }).length;
authenticateSession({});
authenticateSession({ isGlobal: true });
});
test('visiting global scope', async function (assert) {
@@ -100,67 +116,79 @@ module('Acceptance | scopes', function (hooks) {
});
test('visiting org scope', async function (assert) {
await visit(urls.orgScope);
await visit(urls.orgScopes);
await a11yAudit();
assert.strictEqual(currentURL(), urls.orgScope);
assert.strictEqual(currentURL(), urls.orgScopes);
});
// NOTE: In reality, we'd have a third "Global" item listed in the org
// dropdown. But since the mock test authenticator bypasses the auth
// normalization step, the UI doesn't know it's authenticated with global
// and thus doesn't display the "Global" item in the org nav dropdown.
test('can navigate among org scopes via header navigation', async function (assert) {
await visit(urls.globalScope);
await a11yAudit();
assert.strictEqual(currentURL(), urls.globalScope);
await click('.rose-header-nav .rose-dropdown a:nth-child(1)');
assert.strictEqual(currentURL(), urls.orgScope);
// In reality, there would be a third item in the list
await click('.rose-header-nav .rose-dropdown a:nth-child(2)');
assert.strictEqual(currentURL(), urls.org2Scope);
await click(ORG_SCOPES_DROPDOWN_SELECTOR(urls.orgScope));
assert.strictEqual(currentURL(), urls.orgScopes);
await click(ORG_SCOPES_DROPDOWN_SELECTOR(urls.org2Scope));
assert.strictEqual(currentURL(), urls.org2Scopes);
});
test('can navigate among project scopes via header navigation', async function (assert) {
await visit(urls.projectScope);
await a11yAudit();
assert.strictEqual(currentURL(), urls.projectScopeEdit);
await click(
'.rose-header-nav .rose-dropdown + .rose-dropdown a:nth-child(2)',
);
await click(PROJECT_SCOPES_DROPDOWN_SELECTOR(urls.project2Scope));
assert.strictEqual(currentURL(), urls.project2ScopeEdit);
});
test('can create new org scopes', async function (assert) {
const orgScopeCount = getScopeCount('org');
await visit(urls.newOrgScope);
await fillIn('[name="name"]', 'random string');
await click('[type="submit"]');
await visit(urls.globalScope);
await click(`[href="${urls.newOrgScope}"]`);
await fillIn(NAME_FIELD_SELECTOR, 'random string');
await click(SAVE_BUTTON_SELECTOR);
assert.strictEqual(getScopeCount('org'), orgScopeCount + 1);
});
test('can create new project scopes', async function (assert) {
const orgScopeCount = getScopeCount('project');
await visit(urls.newProjectScope);
await fillIn('[name="name"]', 'random string');
await click('[type="submit"]');
await visit(urls.orgScopes);
await click(`[href="${urls.newProjectScope}"]`);
await fillIn(NAME_FIELD_SELECTOR, 'random string');
await click(SAVE_BUTTON_SELECTOR);
assert.strictEqual(getScopeCount('project'), orgScopeCount + 1);
});
test('can cancel create new org scopes', async function (assert) {
const orgScopeCount = getScopeCount('org');
await visit(urls.newOrgScope);
await fillIn('[name="name"]', 'random string');
await click('.rose-form-actions [type="button"]');
await visit(urls.globalScope);
await click(`[href="${urls.newOrgScope}"]`);
await fillIn(NAME_FIELD_SELECTOR, 'random string');
await click(CANCEL_BUTTON_SELECTOR);
assert.strictEqual(currentURL(), urls.globalScope);
assert.strictEqual(getScopeCount('org'), orgScopeCount);
});
test('can cancel create new project scopes', async function (assert) {
const projectScopeCount = getScopeCount('project');
await visit(urls.newProjectScope);
await fillIn('[name="name"]', 'random string');
await click('.rose-form-actions [type="button"]');
assert.strictEqual(currentURL(), urls.orgScope);
await visit(urls.orgScopes);
await click(`[href="${urls.newProjectScope}"]`);
await fillIn(NAME_FIELD_SELECTOR, 'random string');
await click(CANCEL_BUTTON_SELECTOR);
assert.strictEqual(currentURL(), urls.orgScopes);
assert.strictEqual(getScopeCount('project'), projectScopeCount);
});
@@ -184,8 +212,11 @@ module('Acceptance | scopes', function (hooks) {
},
);
});
await visit(urls.newOrgScope);
await click('[type="submit"]');
await visit(urls.globalScope);
await click(`[href="${urls.newOrgScope}"]`);
await click(SAVE_BUTTON_SELECTOR);
assert.dom('.rose-notification-body').hasText('The request was invalid.');
assert.dom('.hds-form-error__message').hasText('Name is required.');
});
@@ -7,6 +7,7 @@ import { module, test } from 'qunit';
import { visit, click, currentURL } 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 { Response } from 'miragejs';
import {
authenticateSession,
@@ -18,6 +19,7 @@ import {
module('Acceptance | scopes | delete', function (hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
setupIndexedDb(hooks);
let getScopeCount;
@@ -0,0 +1,120 @@
import { module, test } from 'qunit';
import { visit, fillIn, waitFor } 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 {
authenticateSession,
// These are left here intentionally for future reference.
//currentSession,
//invalidateSession,
} from 'ember-simple-auth/test-support';
module('Acceptance | scopes | list', function (hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
setupIndexedDb(hooks);
const SEARCH_INPUT_SELECTOR = '.search-filtering [type="search"]';
const NO_RESULTS_MSG_SELECTOR = '[data-test-no-scope-results]';
const SCOPE_LINK_SELECTOR = (id) =>
`tbody [data-test-scopes-table-row="${id}"] a`;
const instances = {
scopes: {
global: null,
org1: null,
org2: null,
project1: null,
project2: null,
},
};
const urls = {
globalScope: null,
orgScope: null,
};
hooks.beforeEach(function () {
instances.scopes.global = this.server.create('scope', { id: 'global' });
instances.scopes.org1 = this.server.create('scope', {
type: 'org',
scope: { id: 'global', type: 'global' },
});
instances.scopes.org2 = this.server.create('scope', {
type: 'org',
scope: { id: 'global', type: 'global' },
});
instances.scopes.project1 = this.server.create('scope', {
type: 'project',
scope: { id: instances.scopes.org1.id, type: 'org' },
});
instances.scopes.project2 = this.server.create('scope', {
type: 'project',
scope: { id: instances.scopes.org1.id, type: 'org' },
});
urls.globalScope = `/scopes/global/scopes`;
urls.orgScope = `/scopes/${instances.scopes.org1.id}/scopes`;
authenticateSession({});
});
test('user can search for a specifc org scope by id', async function (assert) {
await visit(urls.globalScope);
assert.dom(SCOPE_LINK_SELECTOR(instances.scopes.org1.id)).exists();
assert.dom(SCOPE_LINK_SELECTOR(instances.scopes.org2.id)).exists();
await fillIn(SEARCH_INPUT_SELECTOR, instances.scopes.org1.id);
await waitFor(SCOPE_LINK_SELECTOR(instances.scopes.org2.id), { count: 0 });
assert.dom(SCOPE_LINK_SELECTOR(instances.scopes.org1.id)).exists();
assert.dom(SCOPE_LINK_SELECTOR(instances.scopes.org2.id)).doesNotExist();
});
test('user can search for org scopes and get no results', async function (assert) {
await visit(urls.globalScope);
assert.dom(SCOPE_LINK_SELECTOR(instances.scopes.org1.id)).exists();
assert.dom(SCOPE_LINK_SELECTOR(instances.scopes.org2.id)).exists();
await fillIn(SEARCH_INPUT_SELECTOR, 'fake org scope that does not exist');
await waitFor(NO_RESULTS_MSG_SELECTOR, { count: 1 });
assert.dom(SCOPE_LINK_SELECTOR(instances.scopes.org1.id)).doesNotExist();
assert.dom(SCOPE_LINK_SELECTOR(instances.scopes.org2.id)).doesNotExist();
});
test('user can search for a specifc project scope by id', async function (assert) {
await visit(urls.orgScope);
assert.dom(SCOPE_LINK_SELECTOR(instances.scopes.project1.id)).exists();
assert.dom(SCOPE_LINK_SELECTOR(instances.scopes.project2.id)).exists();
await fillIn(SEARCH_INPUT_SELECTOR, instances.scopes.project1.id);
await waitFor(SCOPE_LINK_SELECTOR(instances.scopes.project2.id), {
count: 0,
});
assert.dom(SCOPE_LINK_SELECTOR(instances.scopes.project1.id)).exists();
assert
.dom(SCOPE_LINK_SELECTOR(instances.scopes.project2.id))
.doesNotExist();
});
test('user can search for project scopes and get no results', async function (assert) {
await visit(urls.orgScope);
assert.dom(SCOPE_LINK_SELECTOR(instances.scopes.project1.id)).exists();
assert.dom(SCOPE_LINK_SELECTOR(instances.scopes.project2.id)).exists();
await fillIn(SEARCH_INPUT_SELECTOR, 'fake org scope that does not exist');
await waitFor(NO_RESULTS_MSG_SELECTOR, { count: 1 });
assert
.dom(SCOPE_LINK_SELECTOR(instances.scopes.project1.id))
.doesNotExist();
assert
.dom(SCOPE_LINK_SELECTOR(instances.scopes.project2.id))
.doesNotExist();
});
});
@@ -7,6 +7,7 @@ import { module, test } from 'qunit';
import { visit, currentURL, click } 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,
@@ -18,6 +19,7 @@ import {
module('Acceptance | scopes | read', function (hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
setupIndexedDb(hooks);
let features;
let featureEdition;
@@ -7,6 +7,7 @@ import { module, test } from 'qunit';
import { visit, currentURL, click, fillIn } 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 { Response } from 'miragejs';
import {
authenticateSession,
@@ -18,6 +19,7 @@ import {
module('Acceptance | scopes | update', function (hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
setupIndexedDb(hooks);
const instances = {
scopes: {
@@ -7,11 +7,13 @@ import { module, test } from 'qunit';
import { visit, currentURL, click } from '@ember/test-helpers';
import { setupApplicationTest } from 'admin/tests/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('Acceptance | session recordings | list', function (hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
setupIndexedDb(hooks);
let featuresService;
@@ -7,11 +7,13 @@ import { module, test } from 'qunit';
import { visit, click, currentURL } from '@ember/test-helpers';
import { setupApplicationTest } from 'admin/tests/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('Acceptance | storage-buckets | list', function (hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
setupIndexedDb(hooks);
let featuresService;
let intl;
@@ -7,12 +7,14 @@ import { module, test } from 'qunit';
import { visit, currentURL, click } from '@ember/test-helpers';
import { setupApplicationTest } from 'admin/tests/helpers';
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';
module('Acceptance | storage-buckets | read', function (hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
setupIndexedDb(hooks);
let features;
@@ -246,7 +246,7 @@ module('Acceptance | targets | brokered credential sources', function (hooks) {
(item) => item !== 'add-credential-sources',
);
await visit(urls.brokeredCredentialSources);
assert.dom('.rose-dropdown-content a:nth-child(2)').doesNotExist();
assert.dom('[data-test-add-brokered-cred-sources-action]').doesNotExist();
});
test('can select and cancel credential sources to add', async function (assert) {
@@ -261,7 +261,7 @@ module(
);
await visit(urls.injectedApplicationCredentialSources);
assert.dom('.rose-dropdown-content a:nth-child(2)').doesNotExist();
assert.dom('[data-test-add-injected-cred-sources-action]').doesNotExist();
});
test('can select and cancel credential sources to add', async function (assert) {
@@ -7,6 +7,7 @@ import { module, test } from 'qunit';
import { visit, currentURL, click, fillIn } 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 { Response } from 'miragejs';
import {
authenticateSession,
@@ -18,6 +19,7 @@ import {
module('Acceptance | users | create', function (hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
setupIndexedDb(hooks);
let getUsersCount;
@@ -7,6 +7,7 @@ import { module, test } from 'qunit';
import { visit } 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 {
authenticateSession,
// These are left here intentionally for future reference.
@@ -17,6 +18,7 @@ import {
module('Acceptance | workers | list', function (hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
setupIndexedDb(hooks);
let featuresService;
@@ -7,6 +7,7 @@ import { module, test } from 'qunit';
import { visit, currentURL, click } 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,
@@ -18,6 +19,7 @@ import {
module('Acceptance | workers | read', function (hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
setupIndexedDb(hooks);
let featuresService;
@@ -0,0 +1,26 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
import { waitUntil } from '@ember/test-helpers';
module('Unit | Controller | scopes/scope/scopes/index', function (hooks) {
setupTest(hooks);
let controller;
hooks.beforeEach(function () {
controller = this.owner.lookup('controller:scopes/scope/scopes/index');
});
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);
});
});