diff --git a/addons/api/addon/models/host-catalog.js b/addons/api/addon/models/host-catalog.js index d5cb6d494..4c2eff078 100644 --- a/addons/api/addon/models/host-catalog.js +++ b/addons/api/addon/models/host-catalog.js @@ -5,8 +5,19 @@ import GeneratedHostCatalogModel from '../generated/models/host-catalog'; -export const types = ['static', 'plugin']; -export const pluginTypes = ['aws', 'azure']; +export const TYPE_HOST_CATALOG_STATIC = 'static'; +export const TYPE_HOST_CATALOG_DYNAMIC = 'plugin'; +export const TYPES_HOST_CATALOG = Object.freeze([ + TYPE_HOST_CATALOG_STATIC, + TYPE_HOST_CATALOG_DYNAMIC, +]); + +export const TYPE_HOST_CATALOG_PLUGIN_AWS = 'aws'; +export const TYPE_HOST_CATALOG_PLUGIN_AZURE = 'azure'; +export const TYPES_HOST_CATALOG_PLUGIN = [ + TYPE_HOST_CATALOG_PLUGIN_AWS, + TYPE_HOST_CATALOG_PLUGIN_AZURE, +]; export default class HostCatalogModel extends GeneratedHostCatalogModel { // =attributes @@ -32,7 +43,9 @@ export default class HostCatalogModel extends GeneratedHostCatalogModel { * @type {boolean} */ get isUnknown() { - return this.isPlugin && !pluginTypes.includes(this.plugin?.name); + return ( + this.isPlugin && !TYPES_HOST_CATALOG_PLUGIN.includes(this.plugin?.name) + ); } /** diff --git a/addons/api/addon/models/host-set.js b/addons/api/addon/models/host-set.js index c1e17c79f..489be7e24 100644 --- a/addons/api/addon/models/host-set.js +++ b/addons/api/addon/models/host-set.js @@ -4,7 +4,7 @@ */ import GeneratedHostSetModel from '../generated/models/host-set'; -import { pluginTypes } from './host-catalog'; +import { TYPES_HOST_CATALOG_PLUGIN } from './host-catalog'; export default class HostSetModel extends GeneratedHostSetModel { // =attributes @@ -30,7 +30,9 @@ export default class HostSetModel extends GeneratedHostSetModel { * @type {boolean} */ get isUnknown() { - return this.isPlugin && !pluginTypes.includes(this.plugin?.name); + return ( + this.isPlugin && !TYPES_HOST_CATALOG_PLUGIN.includes(this.plugin?.name) + ); } /** diff --git a/addons/api/addon/models/host.js b/addons/api/addon/models/host.js index 7a96180da..056da52c1 100644 --- a/addons/api/addon/models/host.js +++ b/addons/api/addon/models/host.js @@ -5,7 +5,7 @@ import GeneratedHostModel from '../generated/models/host'; import { attr } from '@ember-data/model'; -import { pluginTypes } from './host-catalog'; +import { TYPES_HOST_CATALOG_PLUGIN } from './host-catalog'; export default class HostModel extends GeneratedHostModel { // =attributes @@ -64,7 +64,9 @@ export default class HostModel extends GeneratedHostModel { * @type {boolean} */ get isUnknown() { - return this.isPlugin && !pluginTypes.includes(this.plugin?.name); + return ( + this.isPlugin && !TYPES_HOST_CATALOG_PLUGIN.includes(this.plugin?.name) + ); } /** diff --git a/addons/api/addon/services/indexed-db.js b/addons/api/addon/services/indexed-db.js index 707ddf8bd..93d207b8c 100644 --- a/addons/api/addon/services/indexed-db.js +++ b/addons/api/addon/services/indexed-db.js @@ -18,6 +18,8 @@ export const modelIndexes = { role: '&id, attributes.created_time, 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': + '&id, attributes.created_time, attributes.type, attributes.name, attributes.description, attributes.scope.scope_id, attributes.plugin.name', }; export const formatDbName = (userId, clusterUrl) => diff --git a/addons/api/mirage/config.js b/addons/api/mirage/config.js index 61a9a1903..4834cf933 100644 --- a/addons/api/mirage/config.js +++ b/addons/api/mirage/config.js @@ -403,12 +403,7 @@ function routes() { // Other resources // host-catalog - this.get( - '/host-catalogs', - ({ hostCatalogs }, { queryParams: { scope_id: scopeId } }) => { - return hostCatalogs.where({ scopeId }); - }, - ); + this.get('/host-catalogs'); this.post( '/host-catalogs', function ({ hostCatalogs }, { queryParams: { plugin_name } }) { diff --git a/ui/admin/app/components/form/host-catalog/aws/index.js b/ui/admin/app/components/form/host-catalog/aws/index.js index 168daf774..658cd8363 100644 --- a/ui/admin/app/components/form/host-catalog/aws/index.js +++ b/ui/admin/app/components/form/host-catalog/aws/index.js @@ -5,20 +5,23 @@ import Component from '@glimmer/component'; import { action } from '@ember/object'; -import { types, pluginTypes } from 'api/models/host-catalog'; +import { + TYPES_HOST_CATALOG, + TYPES_HOST_CATALOG_PLUGIN, +} from 'api/models/host-catalog'; //Note: this is a temporary solution till we have resource type helper in place const icons = ['aws-color', 'azure-color']; export default class FormHostCatalogAwsComponent extends Component { // =properties - hostCatalogTypes = types; + hostCatalogTypes = TYPES_HOST_CATALOG; /** * maps resource type with icon * @type {object} */ get mapResourceTypeWithIcon() { - return pluginTypes.reduce( + return TYPES_HOST_CATALOG_PLUGIN.reduce( (obj, plugin, i) => ({ ...obj, [plugin]: icons[i] }), {}, ); diff --git a/ui/admin/app/components/form/host-catalog/azure/index.js b/ui/admin/app/components/form/host-catalog/azure/index.js index a3634f41f..f8eab0cd9 100644 --- a/ui/admin/app/components/form/host-catalog/azure/index.js +++ b/ui/admin/app/components/form/host-catalog/azure/index.js @@ -4,20 +4,23 @@ */ import Component from '@glimmer/component'; -import { types, pluginTypes } from 'api/models/host-catalog'; +import { + TYPES_HOST_CATALOG, + TYPES_HOST_CATALOG_PLUGIN, +} from 'api/models/host-catalog'; //Note: this is a temporary solution till we have resource type helper in place const icons = ['aws-color', 'azure-color']; export default class FormAzureHostCatalogAwsComponent extends Component { // =properties - hostCatalogTypes = types; + hostCatalogTypes = TYPES_HOST_CATALOG; /** * maps resource type with icon * @type {object} */ get mapResourceTypeWithIcon() { - return pluginTypes.reduce( + return TYPES_HOST_CATALOG_PLUGIN.reduce( (obj, plugin, i) => ({ ...obj, [plugin]: icons[i] }), {}, ); diff --git a/ui/admin/app/components/form/host-catalog/static/index.js b/ui/admin/app/components/form/host-catalog/static/index.js index d91754afb..8dc746724 100644 --- a/ui/admin/app/components/form/host-catalog/static/index.js +++ b/ui/admin/app/components/form/host-catalog/static/index.js @@ -4,20 +4,23 @@ */ import Component from '@glimmer/component'; -import { types, pluginTypes } from 'api/models/host-catalog'; +import { + TYPES_HOST_CATALOG, + TYPES_HOST_CATALOG_PLUGIN, +} from 'api/models/host-catalog'; //Note: this is a temporary solution till we have resource type helper in place const icons = ['aws-color', 'azure-color']; export default class FormStaticHostCatalogAwsComponent extends Component { // =properties - hostCatalogTypes = types; + hostCatalogTypes = TYPES_HOST_CATALOG; /** * maps resource type with icon * @type {object} */ get mapResourceTypeWithIcon() { - return pluginTypes.reduce( + return TYPES_HOST_CATALOG_PLUGIN.reduce( (obj, plugin, i) => ({ ...obj, [plugin]: icons[i] }), {}, ); diff --git a/ui/admin/app/controllers/scopes/scope/host-catalogs/index.js b/ui/admin/app/controllers/scopes/scope/host-catalogs/index.js new file mode 100644 index 000000000..38cf690d4 --- /dev/null +++ b/ui/admin/app/controllers/scopes/scope/host-catalogs/index.js @@ -0,0 +1,33 @@ +import Controller from '@ember/controller'; +import { tracked } from '@glimmer/tracking'; +import { action } from '@ember/object'; +import { debounce } from 'core/decorators/debounce'; +import { inject as service } from '@ember/service'; + +export default class ScopesScopeHostCatalogsIndexController extends Controller { + // =services + + @service intl; + + // =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; + } +} diff --git a/ui/admin/app/controllers/scopes/scope/targets/index.js b/ui/admin/app/controllers/scopes/scope/targets/index.js index f135b9841..ab25f4b6e 100644 --- a/ui/admin/app/controllers/scopes/scope/targets/index.js +++ b/ui/admin/app/controllers/scopes/scope/targets/index.js @@ -70,9 +70,10 @@ export default class ScopesScopeTargetsIndexController extends Controller { } /** - * Sets the scopes query param to value of selectedScopes - * to trigger a query and closes the dropdown - * @param {function} close + * Sets a query param to the value of selectedItems + * and resets the page to 1. + * @param {string} paramKey + * @param {[string]} selectedItems */ @action applyFilter(paramKey, selectedItems) { diff --git a/ui/admin/app/routes/scopes/scope/host-catalogs.js b/ui/admin/app/routes/scopes/scope/host-catalogs.js index b392c3734..965dd5908 100644 --- a/ui/admin/app/routes/scopes/scope/host-catalogs.js +++ b/ui/admin/app/routes/scopes/scope/host-catalogs.js @@ -13,10 +13,7 @@ import { notifySuccess, notifyError } from 'core/decorators/notify'; export default class ScopesScopeHostCatalogsRoute extends Route { // =services - @service store; - @service intl; @service session; - @service can; @service router; // =methods @@ -28,18 +25,6 @@ export default class ScopesScopeHostCatalogsRoute extends Route { if (!this.session.isAuthenticated) this.router.transitionTo('index'); } - /** - * Loads all host catalogs under the current scope. - * @return {Promise{[HostCatalogModel]}} - */ - async model() { - const scope = this.modelFor('scopes.scope'); - const { id: scope_id } = scope; - if (this.can.can('list model', scope, { collection: 'host-catalogs' })) { - return this.store.query('host-catalog', { scope_id }); - } - } - // =actions /** @@ -84,7 +69,7 @@ export default class ScopesScopeHostCatalogsRoute extends Route { @notifySuccess('notifications.delete-success') async delete(hostCatalog) { await hostCatalog.destroyRecord(); - await this.router.replaceWith('scopes.scope.host-catalogs'); + this.router.replaceWith('scopes.scope.host-catalogs'); this.refresh(); } } diff --git a/ui/admin/app/routes/scopes/scope/host-catalogs/index.js b/ui/admin/app/routes/scopes/scope/host-catalogs/index.js index fa7d9463f..b7d302889 100644 --- a/ui/admin/app/routes/scopes/scope/host-catalogs/index.js +++ b/ui/admin/app/routes/scopes/scope/host-catalogs/index.js @@ -4,8 +4,89 @@ */ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; export default class ScopesScopeHostCatalogsIndexRoute extends Route { + // =services + + @service store; + @service can; + + // =attributes + + queryParams = { + search: { + refreshModel: true, + replace: true, + }, + page: { + refreshModel: true, + }, + pageSize: { + refreshModel: true, + }, + }; + + // =methods + + /** + * Loads all host catalogs under the current scope. + * @returns {Promise<{hostCatalogsExist: boolean, totalItems: number, hostCatalogs: [HostCatalogModel]}>} + */ + async model({ search, page, pageSize }) { + const scope = this.modelFor('scopes.scope'); + const { id: scope_id } = scope; + + const filters = { + scope_id: [{ equals: scope_id }], + type: [], + 'plugin.name': [], + }; + + let hostCatalogs; + let totalItems = 0; + if (this.can.can('list model', scope, { collection: 'host-catalogs' })) { + hostCatalogs = await this.store.query('host-catalog', { + query: { search, filters }, + page, + pageSize, + }); + totalItems = hostCatalogs.meta?.totalItems; + } + + const hostCatalogsExist = await this.getHostCatalogsExist( + scope_id, + totalItems, + ); + + return { hostCatalogs, totalItems, hostCatalogsExist }; + } + + /** + * Returns true if any host catalogs exist. + * @param {string} scope_id + * @param {number} totalItems + * @returns {boolean} + */ + async getHostCatalogsExist(scope_id, totalItems) { + if (totalItems > 0) { + return true; + } + + const options = { pushToStore: false }; + const hostCatalogs = await this.store.query( + 'host-catalog', + { + query: { filters: { scope_id: [{ equals: scope_id }] } }, + page: 1, + pageSize: 1, + }, + options, + ); + + return hostCatalogs.length > 0; + } + setupController(controller) { const scope = this.modelFor('scopes.scope'); super.setupController(...arguments); diff --git a/ui/admin/app/routes/scopes/scope/targets/target/add-host-sources.js b/ui/admin/app/routes/scopes/scope/targets/target/add-host-sources.js index 4490bbae1..836c0082c 100644 --- a/ui/admin/app/routes/scopes/scope/targets/target/add-host-sources.js +++ b/ui/admin/app/routes/scopes/scope/targets/target/add-host-sources.js @@ -33,7 +33,9 @@ export default class ScopesScopeTargetsTargetAddHostSourcesRoute extends Route { async model() { const target = this.modelFor('scopes.scope.targets.target'); const { id: scope_id } = this.modelFor('scopes.scope'); - const hostCatalogs = await this.store.query('host-catalog', { scope_id }); + const hostCatalogs = await this.store.query('host-catalog', { + query: { filters: { scope_id: [{ equals: scope_id }] } }, + }); await all( hostCatalogs.map(({ id: host_catalog_id }) => this.store.query('host-set', { host_catalog_id }), diff --git a/ui/admin/app/templates/scopes/scope/host-catalogs/index.hbs b/ui/admin/app/templates/scopes/scope/host-catalogs/index.hbs index 88bef3d9f..f62147c4d 100644 --- a/ui/admin/app/templates/scopes/scope/host-catalogs/index.hbs +++ b/ui/admin/app/templates/scopes/scope/host-catalogs/index.hbs @@ -14,103 +14,125 @@ {{t 'resources.host-catalog.title_plural'}} - {{#if @model}} + {{#if @model.hostCatalogsExist}}

{{t 'resources.host-catalog.description'}}

{{/if}} {{#if (can 'create model' this.scope collection='host-catalogs')}} - {{#if @model}} - - {{t 'resources.host-catalog.titles.new'}} - + @text={{t 'resources.host-catalog.titles.new'}} + /> {{/if}} {{/if}} - + + {{#if @model.hostCatalogsExist}} + + + - {{#if @model}} - - - <:body as |B|> - - - {{#if (can 'read host-catalog' B.data)}} - - {{B.data.displayName}} - - {{else}} - - {{B.data.displayName}} - - {{/if}} - {{B.data.description}} - - - - - - - - - - - - {{else}} - - - - - {{#if (can 'list model' this.scope collection='host-catalogs')}} - {{! can list (at least): show default welcome message}} - {{t 'resources.host-catalog.description'}} - {{else if - (can 'create model' this.scope collection='host-catalogs') + <:body as |B|> + + + {{#if (can 'read host-catalog' B.data)}} + + {{B.data.displayName}} + + {{else}} + + {{B.data.displayName}} + + {{/if}} + {{B.data.description}} + + + + + + + + + + + + {{else}} + + + + + {{/if}} + {{else}} + + + - {{#if (can 'create model' this.scope collection='host-catalogs')}} - - - {{t 'titles.new'}} - - {{/if}} - - + ) + ) + resource=(t 'resources.host-catalog.title_plural') + }} + /> + {{#if (can 'create model' this.scope collection='host-catalogs')}} + + + + {{/if}} + {{/if}} diff --git a/ui/admin/tests/acceptance/host-catalogs/create-test.js b/ui/admin/tests/acceptance/host-catalogs/create-test.js index ef1b02c82..bcd0d1842 100644 --- a/ui/admin/tests/acceptance/host-catalogs/create-test.js +++ b/ui/admin/tests/acceptance/host-catalogs/create-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { visit, currentURL, find, click, fillIn } from '@ember/test-helpers'; +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 { Response } from 'miragejs'; @@ -14,12 +14,21 @@ import { //currentSession, //invalidateSession, } from 'ember-simple-auth/test-support'; +import { setupIndexedDb } from 'api/test-support/helpers/indexed-db'; module('Acceptance | host-catalogs | create', function (hooks) { setupApplicationTest(hooks); setupMirage(hooks); + setupIndexedDb(hooks); - let gethostCatalogCount; + let getHostCatalogCount; + + const NAME_INPUT_SELECTOR = '[name="name"]'; + const DESCRIPTION_INPUT_SELECTOR = '[name="description"]'; + const TYPE_INPUT_SELECTOR = '[name="type"]'; + const SAVE_BUTTON_SELECTOR = '[type="submit"]'; + const CANCEL_BUTTON_SELECTOR = '.rose-form-actions [type="button"]'; + const ALERT_TEXT_SELECTOR = '[role="alert"]'; const instances = { scopes: { @@ -75,68 +84,68 @@ module('Acceptance | host-catalogs | create', function (hooks) { urls.newStaticHostCatalog = `${urls.newHostCatalog}?type=static`; urls.newAWSDynamicHostCatalog = `${urls.newHostCatalog}?type=aws`; urls.newAzureDynamicHostCatalog = `${urls.newHostCatalog}?type=azure`; - // Generate resource couner - gethostCatalogCount = () => + // Generate resource counter + getHostCatalogCount = () => this.server.schema.hostCatalogs.all().models.length; authenticateSession({}); }); test('Users can create new static host catalogs', async function (assert) { - const count = gethostCatalogCount(); + const count = getHostCatalogCount(); await visit(urls.newStaticHostCatalog); - await fillIn('[name="name"]', 'random string'); - await fillIn('[name="description"]', 'random string'); - await fillIn('[name="type"]', 'static'); - await click('[type="submit"]'); - assert.strictEqual(gethostCatalogCount(), count + 1); + await fillIn(NAME_INPUT_SELECTOR, 'random string'); + await fillIn(DESCRIPTION_INPUT_SELECTOR, 'random string'); + await fillIn(TYPE_INPUT_SELECTOR, 'static'); + await click(SAVE_BUTTON_SELECTOR); + assert.strictEqual(getHostCatalogCount(), count + 1); }); test('Users can create new dynamic aws host catalogs with aws provider', async function (assert) { - const count = gethostCatalogCount(); + const count = getHostCatalogCount(); await visit(urls.newAWSDynamicHostCatalog); - await fillIn('[name="name"]', 'random string'); - await fillIn('[name="description"]', 'random string'); - await fillIn('[name="type"]', 'aws'); - await click('[type="submit"]'); - assert.strictEqual(gethostCatalogCount(), count + 1); + await fillIn(NAME_INPUT_SELECTOR, 'random string'); + await fillIn(DESCRIPTION_INPUT_SELECTOR, 'random string'); + await fillIn(TYPE_INPUT_SELECTOR, 'aws'); + await click(SAVE_BUTTON_SELECTOR); + assert.strictEqual(getHostCatalogCount(), count + 1); }); test('Users can create new dynamic aws host catalogs with azure provider', async function (assert) { - const count = gethostCatalogCount(); + const count = getHostCatalogCount(); await visit(urls.newAzureDynamicHostCatalog); - await fillIn('[name="name"]', 'random string'); - await fillIn('[name="description"]', 'random string'); - await fillIn('[name="type"]', 'azure'); - await click('[type="submit"]'); - assert.strictEqual(gethostCatalogCount(), count + 1); + await fillIn(NAME_INPUT_SELECTOR, 'random string'); + await fillIn(DESCRIPTION_INPUT_SELECTOR, 'random string'); + await fillIn(TYPE_INPUT_SELECTOR, 'azure'); + await click(SAVE_BUTTON_SELECTOR); + assert.strictEqual(getHostCatalogCount(), count + 1); }); test('Users can cancel creation of new static host catalogs', async function (assert) { - const count = gethostCatalogCount(); + const count = getHostCatalogCount(); await visit(urls.newStaticHostCatalog); - await fillIn('[name="name"]', 'random string'); - await click('.rose-form-actions [type="button"]'); + await fillIn(NAME_INPUT_SELECTOR, 'random string'); + await click(CANCEL_BUTTON_SELECTOR); assert.strictEqual(currentURL(), urls.hostCatalogs); - assert.strictEqual(gethostCatalogCount(), count); + assert.strictEqual(getHostCatalogCount(), count); }); test('Users can cancel creation of new dynamic host catalogs with AWS provider', async function (assert) { - const count = gethostCatalogCount(); + const count = getHostCatalogCount(); await visit(urls.newAWSDynamicHostCatalog); - await fillIn('[name="name"]', 'random string'); - await click('.rose-form-actions [type="button"]'); + await fillIn(NAME_INPUT_SELECTOR, 'random string'); + await click(CANCEL_BUTTON_SELECTOR); assert.strictEqual(currentURL(), urls.hostCatalogs); - assert.strictEqual(gethostCatalogCount(), count); + assert.strictEqual(getHostCatalogCount(), count); }); test('Users can cancel creation of new dynamic host catalogs with Azure provider', async function (assert) { - const count = gethostCatalogCount(); + const count = getHostCatalogCount(); await visit(urls.newAzureDynamicHostCatalog); - await fillIn('[name="name"]', 'random string'); - await click('.rose-form-actions [type="button"]'); + await fillIn(NAME_INPUT_SELECTOR, 'random string'); + await click(CANCEL_BUTTON_SELECTOR); assert.strictEqual(currentURL(), urls.hostCatalogs); - assert.strictEqual(gethostCatalogCount(), count); + assert.strictEqual(getHostCatalogCount(), count); }); test('Users can navigate to new static host catalogs route with proper authorization', async function (assert) { @@ -146,7 +155,7 @@ module('Acceptance | host-catalogs | create', function (hooks) { 'host-catalogs' ].includes('create'), ); - assert.ok(find(`[href="${urls.newHostCatalog}"]`)); + assert.dom(`[href="${urls.newHostCatalog}"]`).exists(); }); test('Users cannot navigate to new static host catalogs route without proper authorization', async function (assert) { @@ -158,7 +167,7 @@ module('Acceptance | host-catalogs | create', function (hooks) { 'host-catalogs' ].includes('create'), ); - assert.notOk(find(`[href="${urls.newStaticHostCatalog}"]`)); + assert.dom(`[href="${urls.newStaticHostCatalog}"]`).doesNotExist(); }); test('saving a new static host catalog with invalid fields displays error messages', async function (assert) { @@ -182,15 +191,9 @@ module('Acceptance | host-catalogs | create', function (hooks) { ); }); await visit(urls.newStaticHostCatalog); - await click('[type="submit"]'); - assert.ok( - find('[role="alert"]').textContent.trim(), - 'The request was invalid.', - ); - assert.ok( - find('[data-test-error-message-name]').textContent.trim(), - 'Name is required.', - ); + await click(SAVE_BUTTON_SELECTOR); + assert.dom(ALERT_TEXT_SELECTOR).includesText('The request was invalid.'); + assert.dom('[data-test-error-message-name]').hasText('Name is required.'); }); test('users cannot directly navigate to new host catalog route without proper authorization', async function (assert) { diff --git a/ui/admin/tests/acceptance/host-catalogs/delete-test.js b/ui/admin/tests/acceptance/host-catalogs/delete-test.js index 6680ea991..f794bad64 100644 --- a/ui/admin/tests/acceptance/host-catalogs/delete-test.js +++ b/ui/admin/tests/acceptance/host-catalogs/delete-test.js @@ -14,12 +14,14 @@ import { //currentSession, //invalidateSession, } from 'ember-simple-auth/test-support'; +import { setupIndexedDb } from 'api/test-support/helpers/indexed-db'; module('Acceptance | host-catalogs | delete', function (hooks) { setupApplicationTest(hooks); setupMirage(hooks); + setupIndexedDb(hooks); - let gethostCatalogCount; + let getHostCatalogCount; const instances = { scopes: { @@ -57,20 +59,21 @@ module('Acceptance | host-catalogs | delete', function (hooks) { urls.projectScope = `/scopes/${instances.scopes.project.id}`; urls.hostCatalogs = `${urls.projectScope}/host-catalogs`; urls.hostCatalog = `${urls.hostCatalogs}/${instances.hostCatalog.id}`; - // Generate resource couner - gethostCatalogCount = () => + // Generate resource counter + getHostCatalogCount = () => this.server.schema.hostCatalogs.all().models.length; authenticateSession({}); }); test('can delete host catalog', async function (assert) { - const hostCatalogCount = gethostCatalogCount(); + const hostCatalogCount = getHostCatalogCount(); + console.log(hostCatalogCount); await visit(urls.hostCatalogs); await click(`[href="${urls.hostCatalog}"]`); await click('.rose-layout-page-actions .rose-dropdown-button-danger'); - assert.strictEqual(gethostCatalogCount(), hostCatalogCount - 1); + assert.strictEqual(getHostCatalogCount(), hostCatalogCount - 1); }); test('cannot delete host catalog without proper authorization', async function (assert) { @@ -90,7 +93,7 @@ module('Acceptance | host-catalogs | delete', function (hooks) { test('can accept delete host catalog via dialog', async function (assert) { const confirmService = this.owner.lookup('service:confirm'); confirmService.enabled = true; - const hostCatalogCount = gethostCatalogCount(); + const hostCatalogCount = getHostCatalogCount(); await visit(urls.hostCatalogs); await click(`[href="${urls.hostCatalog}"]`); @@ -98,21 +101,21 @@ module('Acceptance | host-catalogs | delete', function (hooks) { await click('.rose-dialog .rose-button-primary'); assert.dom('.rose-notification-body').hasText('Deleted successfully.'); - assert.strictEqual(gethostCatalogCount(), hostCatalogCount - 1); + assert.strictEqual(getHostCatalogCount(), hostCatalogCount - 1); assert.strictEqual(currentURL(), urls.hostCatalogs); }); test('can cancel delete host catalog via dialog', async function (assert) { const confirmService = this.owner.lookup('service:confirm'); confirmService.enabled = true; - const hostCatalogCount = gethostCatalogCount(); + const hostCatalogCount = getHostCatalogCount(); await visit(urls.hostCatalogs); await click(`[href="${urls.hostCatalog}"]`); await click('.rose-layout-page-actions .rose-dropdown-button-danger'); await click('.rose-dialog .rose-button-secondary'); - assert.strictEqual(gethostCatalogCount(), hostCatalogCount); + assert.strictEqual(getHostCatalogCount(), hostCatalogCount); assert.strictEqual(currentURL(), urls.hostCatalog); }); diff --git a/ui/admin/tests/acceptance/host-catalogs/list-test.js b/ui/admin/tests/acceptance/host-catalogs/list-test.js index cd0f18f18..0792dc9ae 100644 --- a/ui/admin/tests/acceptance/host-catalogs/list-test.js +++ b/ui/admin/tests/acceptance/host-catalogs/list-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { visit, click } from '@ember/test-helpers'; +import { visit, click, fillIn, waitFor } from '@ember/test-helpers'; import { setupApplicationTest } from 'ember-qunit'; import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; import { @@ -13,10 +13,18 @@ import { //currentSession, //invalidateSession, } from 'ember-simple-auth/test-support'; +import { setupIndexedDb } from 'api/test-support/helpers/indexed-db'; +import { + TYPE_HOST_CATALOG_DYNAMIC, + TYPE_HOST_CATALOG_STATIC, + TYPE_HOST_CATALOG_PLUGIN_AWS, + TYPE_HOST_CATALOG_PLUGIN_AZURE, +} from 'api/models/host-catalog'; module('Acceptance | host-catalogs | list', function (hooks) { setupApplicationTest(hooks); setupMirage(hooks); + setupIndexedDb(hooks); const instances = { scopes: { @@ -24,7 +32,7 @@ module('Acceptance | host-catalogs | list', function (hooks) { org: null, project: null, }, - hostCatalog: null, + staticHostCatalog: null, }; const urls = { @@ -32,9 +40,12 @@ module('Acceptance | host-catalogs | list', function (hooks) { orgScope: null, projectScope: null, hostCatalogs: null, - hostCatalog: null, + staticHostCatalog: null, }; + const SEARCH_INPUT_SELECTOR = '.search-filtering [type="search"]'; + const NO_RESULTS_MSG_SELECTOR = '[data-test-no-host-catalog-results]'; + hooks.beforeEach(function () { instances.scopes.global = this.server.create('scope', { id: 'global' }); instances.scopes.org = this.server.create('scope', { @@ -45,21 +56,33 @@ module('Acceptance | host-catalogs | list', function (hooks) { type: 'project', scope: { id: instances.scopes.org.id, type: 'org' }, }); - instances.hostCatalog = this.server.create('host-catalog', { + instances.staticHostCatalog = this.server.create('host-catalog', { scope: instances.scopes.project, + type: TYPE_HOST_CATALOG_STATIC, + }); + instances.awsHostCatalog = this.server.create('host-catalog', { + scope: instances.scopes.project, + type: TYPE_HOST_CATALOG_DYNAMIC, + plugin: { name: TYPE_HOST_CATALOG_PLUGIN_AWS }, + }); + instances.azureHostCatalog = this.server.create('host-catalog', { + scope: instances.scopes.project, + type: TYPE_HOST_CATALOG_DYNAMIC, + plugin: { name: TYPE_HOST_CATALOG_PLUGIN_AZURE }, }); urls.globalScope = `/scopes/global/scopes`; urls.orgScope = `/scopes/${instances.scopes.org.id}/scopes`; urls.projectScope = `/scopes/${instances.scopes.project.id}`; urls.hostCatalogs = `${urls.projectScope}/host-catalogs`; - urls.hostCatalog = `${urls.hostCatalogs}/${instances.hostCatalog.id}`; + urls.staticHostCatalog = `${urls.hostCatalogs}/${instances.staticHostCatalog.id}`; + urls.awsHostCatalog = `${urls.hostCatalogs}/${instances.awsHostCatalog.id}`; + urls.azureHostCatalog = `${urls.hostCatalogs}/${instances.azureHostCatalog.id}`; authenticateSession({}); }); test('user can navigate to host catalogs with proper authorization', async function (assert) { await visit(urls.orgScope); - await click(`[href="${urls.projectScope}"]`); assert.true( @@ -146,6 +169,65 @@ module('Acceptance | host-catalogs | list', function (hooks) { await click(`[href="${urls.hostCatalogs}"]`); - assert.dom(`[href="${urls.hostCatalog}"]`).exists(); + assert.dom(`[href="${urls.staticHostCatalog}"]`).exists(); + }); + + test('user can search for a specific host catalog by id', async function (assert) { + await visit(urls.projectScope); + + await click(`[href="${urls.hostCatalogs}"]`); + assert.dom(`[href="${urls.staticHostCatalog}"]`).exists(); + assert.dom(`[href="${urls.awsHostCatalog}"]`).exists(); + assert.dom(`[href="${urls.azureHostCatalog}"]`).exists(); + + await fillIn(SEARCH_INPUT_SELECTOR, instances.staticHostCatalog.id); + await waitFor(`[href="${urls.awsHostCatalog}"]`, { count: 0 }); + + assert.dom(`[href="${urls.staticHostCatalog}"]`).exists(); + }); + + test('user can search for aws host catalog', async function (assert) { + await visit(urls.projectScope); + + await click(`[href="${urls.hostCatalogs}"]`); + assert.dom(`[href="${urls.staticHostCatalog}"]`).exists(); + assert.dom(`[href="${urls.awsHostCatalog}"]`).exists(); + assert.dom(`[href="${urls.azureHostCatalog}"]`).exists(); + + await fillIn(SEARCH_INPUT_SELECTOR, TYPE_HOST_CATALOG_PLUGIN_AWS); + await waitFor(`[href="${urls.staticHostCatalog}"]`, { count: 0 }); + + assert.dom(`[href="${urls.awsHostCatalog}"]`).exists(); + }); + + test('user can search for azure host catalog', async function (assert) { + await visit(urls.projectScope); + + await click(`[href="${urls.hostCatalogs}"]`); + assert.dom(`[href="${urls.staticHostCatalog}"]`).exists(); + assert.dom(`[href="${urls.awsHostCatalog}"]`).exists(); + assert.dom(`[href="${urls.azureHostCatalog}"]`).exists(); + + await fillIn(SEARCH_INPUT_SELECTOR, TYPE_HOST_CATALOG_PLUGIN_AZURE); + await waitFor(`[href="${urls.staticHostCatalog}"]`, { count: 0 }); + + assert.dom(`[href="${urls.azureHostCatalog}"]`).exists(); + }); + + test('user can search for host catalogs and get no results', async function (assert) { + await visit(urls.projectScope); + + await click(`[href="${urls.hostCatalogs}"]`); + assert.dom(`[href="${urls.staticHostCatalog}"]`).exists(); + assert.dom(`[href="${urls.awsHostCatalog}"]`).exists(); + assert.dom(`[href="${urls.azureHostCatalog}"]`).exists(); + + await fillIn(SEARCH_INPUT_SELECTOR, 'wow look at me I am a search query'); + await waitFor(NO_RESULTS_MSG_SELECTOR, { count: 1 }); + + assert.dom(`[href="${urls.staticHostCatalog}"]`).doesNotExist(); + assert.dom(`[href="${urls.awsHostCatalog}"]`).doesNotExist(); + assert.dom(`[href="${urls.azureHostCatalog}"]`).doesNotExist(); + assert.dom(NO_RESULTS_MSG_SELECTOR).includesText('No results found'); }); }); diff --git a/ui/admin/tests/acceptance/host-catalogs/read-test.js b/ui/admin/tests/acceptance/host-catalogs/read-test.js index 79f1c66dd..e948f67b7 100644 --- a/ui/admin/tests/acceptance/host-catalogs/read-test.js +++ b/ui/admin/tests/acceptance/host-catalogs/read-test.js @@ -14,10 +14,12 @@ import { //currentSession, //invalidateSession, } from 'ember-simple-auth/test-support'; +import { setupIndexedDb } from 'api/test-support/helpers/indexed-db'; module('Acceptance | host-catalogs | read', function (hooks) { setupApplicationTest(hooks); setupMirage(hooks); + setupIndexedDb(hooks); const instances = { scopes: { diff --git a/ui/admin/tests/acceptance/host-catalogs/update-test.js b/ui/admin/tests/acceptance/host-catalogs/update-test.js index 7603c2c93..f91c3c4db 100644 --- a/ui/admin/tests/acceptance/host-catalogs/update-test.js +++ b/ui/admin/tests/acceptance/host-catalogs/update-test.js @@ -14,10 +14,12 @@ import { //currentSession, //invalidateSession, } from 'ember-simple-auth/test-support'; +import { setupIndexedDb } from 'api/test-support/helpers/indexed-db'; module('Acceptance | host-catalogs | update', function (hooks) { setupApplicationTest(hooks); setupMirage(hooks); + setupIndexedDb(hooks); const instances = { scopes: { @@ -35,6 +37,14 @@ module('Acceptance | host-catalogs | update', function (hooks) { hostCatalog: null, }; + const NAME_INPUT_SELECTOR = '[name="name"]'; + const EDIT_BUTTON_SELECTOR = 'form [type="button"]'; + const SAVE_BUTTON_SELECTOR = '.rose-form-actions [type="submit"]'; + const CANCEL_BUTTON_SELECTOR = '.rose-form-actions [type="button"]'; + const MODAL_DISCARD_BUTTON_SELECTOR = + '.rose-dialog-footer button:first-child'; + const MODAL_CANCEL_BUTTON_SELECTOR = '.rose-dialog-footer button:last-child'; + hooks.beforeEach(function () { // Generate resources instances.scopes.global = this.server.create('scope', { id: 'global' }); @@ -62,9 +72,9 @@ module('Acceptance | host-catalogs | update', function (hooks) { assert.notEqual(instances.hostCatalog.name, 'random string'); await visit(urls.hostCatalog); - await click('form [type="button"]', 'Activate edit mode'); - await fillIn('[name="name"]', 'random string'); - await click('.rose-form-actions [type="submit"]'); + await click(EDIT_BUTTON_SELECTOR, 'Activate edit mode'); + await fillIn(NAME_INPUT_SELECTOR, 'random string'); + await click(SAVE_BUTTON_SELECTOR); assert.strictEqual(currentURL(), urls.hostCatalog); assert.strictEqual( @@ -82,18 +92,18 @@ module('Acceptance | host-catalogs | update', function (hooks) { await click(`[href="${urls.hostCatalog}"]`); - assert.dom('form [type="button"]').doesNotExist(); + assert.dom(EDIT_BUTTON_SELECTOR).doesNotExist(); }); test('clicking cancel in edit mode does not save changes', async function (assert) { await visit(urls.hostCatalog); - await click('form [type="button"]', 'Activate edit mode'); - await fillIn('[name="name"]', 'random string'); - await click('.rose-form-actions [type="button"]', 'Click Cancel'); + await click(EDIT_BUTTON_SELECTOR, 'Activate edit mode'); + await fillIn(NAME_INPUT_SELECTOR, 'random string'); + await click(CANCEL_BUTTON_SELECTOR, 'Click Cancel'); assert.notEqual(instances.hostCatalog.name, 'random string'); - assert.dom('[name="name"]').hasValue(instances.hostCatalog.name); + assert.dom(NAME_INPUT_SELECTOR).hasValue(instances.hostCatalog.name); }); test('saving an existing host catalog with invalid fields displays error messages', async function (assert) { @@ -118,8 +128,8 @@ module('Acceptance | host-catalogs | update', function (hooks) { }); await visit(urls.hostCatalog); - await click('form [type="button"]', 'Activate edit mode'); - await fillIn('[name="name"]', 'random string'); + await click(EDIT_BUTTON_SELECTOR, 'Activate edit mode'); + await fillIn(NAME_INPUT_SELECTOR, 'random string'); await click('[type="submit"]'); assert.dom('[role="alert"] div').hasText('The request was invalid.'); @@ -132,12 +142,12 @@ module('Acceptance | host-catalogs | update', function (hooks) { assert.notEqual(instances.hostCatalog.name, 'random string'); await visit(urls.hostCatalog); - await click('form [type="button"]', 'Activate edit mode'); - await fillIn('[name="name"]', 'random string'); + await click(EDIT_BUTTON_SELECTOR, 'Activate edit mode'); + await fillIn(NAME_INPUT_SELECTOR, 'random string'); assert.strictEqual(currentURL(), urls.hostCatalog); await click(`[href="${urls.hostCatalogs}"]`); assert.dom('.rose-dialog').exists(); - await click('.rose-dialog-footer button:first-child', 'Click Discard'); + await click(MODAL_DISCARD_BUTTON_SELECTOR, 'Click Discard'); assert.strictEqual(currentURL(), urls.hostCatalogs); assert.notEqual( @@ -152,12 +162,12 @@ module('Acceptance | host-catalogs | update', function (hooks) { assert.notEqual(instances.hostCatalog.name, 'random string'); await visit(urls.hostCatalog); - await click('form [type="button"]', 'Activate edit mode'); - await fillIn('[name="name"]', 'random string'); + await click(EDIT_BUTTON_SELECTOR, 'Activate edit mode'); + await fillIn(NAME_INPUT_SELECTOR, 'random string'); assert.strictEqual(currentURL(), urls.hostCatalog); await click(`[href="${urls.hostCatalogs}"]`); assert.dom('.rose-dialog').exists(); - await click('.rose-dialog-footer button:last-child', 'Click Cancel'); + await click(MODAL_CANCEL_BUTTON_SELECTOR, 'Click Cancel'); assert.strictEqual(currentURL(), urls.hostCatalog); assert.notEqual( diff --git a/ui/admin/tests/unit/controllers/scopes/scope/host-catalogs/index-test.js b/ui/admin/tests/unit/controllers/scopes/scope/host-catalogs/index-test.js new file mode 100644 index 000000000..9b22ac11e --- /dev/null +++ b/ui/admin/tests/unit/controllers/scopes/scope/host-catalogs/index-test.js @@ -0,0 +1,31 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'admin/tests/helpers'; +import { waitUntil } from '@ember/test-helpers'; + +module( + 'Unit | Controller | scopes/scope/host-catalogs/index', + function (hooks) { + setupTest(hooks); + + let controller; + + hooks.beforeEach(function () { + controller = this.owner.lookup( + 'controller:scopes/scope/host-catalogs/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); + }); + }, +);