Merge pull request #940 from hashicorp/feat-typed-hc

Feat: typed host catalog form components for admin UI
This commit is contained in:
Dharini Jeeva authored and GitHub committed 2022-02-08 12:24:37 -08:00
commit 6634d868c9
14 files changed
+566 -75

No files matched your search

+1 -1
View File
@@ -26,7 +26,7 @@ export default class HostCatalogModel extends GeneratedHostCatalogModel {
* @type {boolean}
*/
get isUnknown() {
return this.isPlugin && !types.includes(this.plugin.name);
return this.isPlugin && !types.includes(this.plugin?.name);
}
/**
+10
View File
@@ -118,3 +118,13 @@ callback_url:
filter:
label: Filter
help: A boolean expression defining a filter run against the provided information.
tenant_id:
label: Tenant ID
subscription_id:
label: Subscription ID
access_key_id:
label: Access Key ID
secret_access_key:
label: Secret Access Key
region:
label: Region
+20 -4
View File
@@ -78,16 +78,32 @@ host-catalog:
title: Host Catalog
title_plural: Host Catalogs
description: A host catalog is a collection of hosts and host sets.
type: Type
messages:
welcome:
title: Welcome to Host Catalogs
actions:
delete: Delete Host Catalog
types:
static: Static
unknown: Unknown
aws: AWS
azure: Azure
static:
label: Static
help: Provide a list of hosts and host sets with static addresses
unknown:
label: Unknown
aws:
label: AWS
azure:
label: Azure
dynamic:
label: Dynamic
help: Automate discovery of target hosts and services on Azure or AWS
form:
provider:
label: Provider Details
description: This is the provider for this host catalog. The format of the host set filters are specific to each provider.
choose_a_provider:
label: Choose a provider
description: There are several built-in plugins for the most common providers. You can choose from on of the included plugins or use a custom plugin.
host-set:
title: Host Set
title_plural: Host Sets
@@ -61,7 +61,7 @@ $radio-svg: (
display: inline-flex;
align-items: center;
justify-content: center;
margin: $size-m $size-xs $size-xl 0;
margin: $size-m $size-xs $size-m 0;
.radio-card-overlay {
position: absolute;
@@ -0,0 +1,150 @@
<Rose::Form
@onSubmit={{@submit}}
@cancel={{@cancel}}
@disabled={{@model.isSaving}}
@error={{@model.errors.base}}
@showEditToggle={{if @model.isNew false true}}
as |form|
>
<form.input
@name='name'
@type='name'
@value={{@model.name}}
@label={{t 'form.name.label'}}
@helperText={{t 'form.name.help'}}
@error={{@model.errors.name}}
disabled={{@model.isSaving}}
readonly={{false}}
as |field|
>
{{#if @model.errors.name}}
<field.errors as |errors|>
{{#each @model.errors.name as |error|}}
<errors.message>{{error.message}}</errors.message>
{{/each}}
</field.errors>
{{/if}}
</form.input>
<form.textarea
@name='description'
@type='description'
@value={{@model.description}}
@label={{t 'form.description.label'}}
@helperText={{t 'form.description.help'}}
@error={{@model.errors.description}}
disabled={{@model.isSaving}}
readonly={{false}}
as |field|
>
{{#if @model.errors.description}}
<field.errors as |errors|>
{{#each @model.errors.description as |error|}}
<errors.message>{{error.message}}</errors.message>
{{/each}}
</field.errors>
{{/if}}
</form.textarea>
{{#if @model.isNew}}
<form.fieldset>
<:title>{{t 'form.type.label'}}</:title>
<:body>
<form.radioGroup
@name='types'
@selectedValue={{@model.type}}
@changed={{@changeType}}
as |radioGroup|
>
<radioGroup.card
@label={{t 'resources.host-catalog.types.static.label'}}
@value='static'
@helperText={{t 'resources.host-catalog.types.static.help'}}
@layout='wide'
/>
<radioGroup.card
@label={{t 'resources.host-catalog.types.dynamic.label'}}
@value='plugin'
@helperText={{t 'resources.host-catalog.types.dynamic.help'}}
@layout='wide'
/>
</form.radioGroup>
</:body>
</form.fieldset>
<Rose::Separator />
<form.fieldset>
<:title>{{t 'resources.host-catalog.form.provider.label'}}</:title>
</form.fieldset>
<form.fieldset>
<:title>{{t
'resources.host-catalog.form.provider.choose_a_provider.label'
}}</:title>
<:description>{{t
'resources.host-catalog.form.provider.choose_a_provider.description'
}}</:description>
<:body>
<form.radioGroup
@name='provider'
@selectedValue={{@model.compositeType}}
@changed={{@changeType}}
as |radioGroup|
>
<radioGroup.card
@label={{t 'resources.host-catalog.types.aws.label'}}
@value='aws'
@layout='tile'
@icon='flight-icons/svg/aws-color-16'
/>
<radioGroup.card
@label={{t 'resources.host-catalog.types.azure.label'}}
@value='azure'
@layout='tile'
@icon='flight-icons/svg/azure-color-16'
/>
</form.radioGroup>
</:body>
</form.fieldset>
{{else}}
<form.fieldset>
<:title>{{t 'resources.host-catalog.form.provider.label'}}</:title>
<:description>{{t
'resources.host-catalog.form.provider.description'
}}</:description>
<:body>
<form.radioGroup
@name='provider'
@selectedValue={{@model.compositeType}}
@disabled={{true}}
as |radioGroup|
>
<radioGroup.card
@label={{t 'resources.host-catalog.types.aws.label'}}
@value='aws'
@layout='tile'
@icon='flight-icons/svg/aws-color-16'
/>
</form.radioGroup>
</:body>
</form.fieldset>
{{/if}}
<form.input
@name='Region'
@type='region'
@value={{@model.region}}
@label={{t 'form.region.label'}}
readonly={{false}}
/>
{{#if (can 'save model' @model)}}
<form.actions
@disabled={{if @model.cannotSave @model.cannotSave}}
@enableEditText={{t 'actions.edit-form'}}
@submitText={{t 'actions.save'}}
@cancelText={{t 'actions.cancel'}}
/>
{{/if}}
</Rose::Form>
@@ -0,0 +1,156 @@
<Rose::Form
@onSubmit={{@submit}}
@cancel={{@cancel}}
@disabled={{@model.isSaving}}
@error={{@model.errors.base}}
@showEditToggle={{if @model.isNew false true}}
as |form|
>
<form.input
@name='name'
@type='name'
@value={{@model.name}}
@label={{t 'form.name.label'}}
@helperText={{t 'form.name.help'}}
@error={{@model.errors.name}}
disabled={{@model.isSaving}}
readonly={{false}}
as |field|
>
{{#if @model.errors.name}}
<field.errors as |errors|>
{{#each @model.errors.name as |error|}}
<errors.message>{{error.message}}</errors.message>
{{/each}}
</field.errors>
{{/if}}
</form.input>
<form.textarea
@name='description'
@type='description'
@value={{@model.description}}
@label={{t 'form.description.label'}}
@helperText={{t 'form.description.help'}}
@error={{@model.errors.description}}
disabled={{@model.isSaving}}
readonly={{false}}
as |field|
>
{{#if @model.errors.description}}
<field.errors as |errors|>
{{#each @model.errors.description as |error|}}
<errors.message>{{error.message}}</errors.message>
{{/each}}
</field.errors>
{{/if}}
</form.textarea>
{{#if @model.isNew}}
{{!-- <form.label>{{t
'resources.host-catalog.form.provider.label'
}}</form.label> --}}
<form.fieldset>
<:title>{{t 'form.type.label'}}</:title>
<:body>
<form.radioGroup
@name='types'
@selectedValue={{@model.type}}
@changed={{@changeType}}
as |radioGroup|
>
<radioGroup.card
@label={{t 'resources.host-catalog.types.static.label'}}
@value='static'
@helperText={{t 'resources.host-catalog.types.static.help'}}
@layout='wide'
/>
<radioGroup.card
@label={{t 'resources.host-catalog.types.dynamic.label'}}
@value='plugin'
@helperText={{t 'resources.host-catalog.types.dynamic.help'}}
@layout='wide'
/>
</form.radioGroup>
</:body>
</form.fieldset>
<Rose::Separator />
<form.fieldset>
<:title>{{t 'resources.host-catalog.form.provider.label'}}</:title>
</form.fieldset>
<form.fieldset>
<:title>{{t
'resources.host-catalog.form.provider.choose_a_provider.label'
}}</:title>
<:description>{{t
'resources.host-catalog.form.provider.choose_a_provider.description'
}}</:description>
<:body>
<form.radioGroup
@name='provider'
@selectedValue={{@model.compositeType}}
@changed={{@changeType}}
as |radioGroup|
>
<radioGroup.card
@label={{t 'resources.host-catalog.types.aws.label'}}
@value='aws'
@layout='tile'
@icon='flight-icons/svg/aws-color-16'
/>
<radioGroup.card
@label={{t 'resources.host-catalog.types.azure.label'}}
@value='azure'
@layout='tile'
@icon='flight-icons/svg/azure-color-16'
/>
</form.radioGroup>
</:body>
</form.fieldset>
{{else}}
<form.fieldset>
<:title>{{t 'resources.host-catalog.form.provider.label'}}</:title>
<:description>{{t
'resources.host-catalog.form.provider.description'
}}</:description>
<:body>
<form.radioGroup
@name='provider'
@selectedValue={{@model.compositeType}}
@disabled={{true}}
as |radioGroup|
>
<radioGroup.card
@label={{t 'resources.host-catalog.types.azure.label'}}
@value='azure'
@layout='tile'
@icon='flight-icons/svg/azure-color-16'
/>
</form.radioGroup>
</:body>
</form.fieldset>
{{/if}}
<form.input
@name='tenant_id'
@type='tenant_id'
@value={{@model.tenant_id}}
@label={{t 'form.tenant_id.label'}}
readonly={{false}}
/>
{{#if (can 'save model' @model)}}
<form.actions
@disabled={{if @model.cannotSave @model.cannotSave}}
@enableEditText={{t 'actions.edit-form'}}
@submitText={{t 'actions.save'}}
@cancelText={{t 'actions.cancel'}}
/>
{{/if}}
</Rose::Form>
@@ -1,56 +1,7 @@
<Rose::Form
@onSubmit={{@submit}}
@cancel={{@cancel}}
@disabled={{@model.isSaving}}
@error={{@model.errors.base}}
@showEditToggle={{if @model.isNew false true}}
as |form|
>
<form.input
@name='name'
@type='name'
@value={{@model.name}}
@label={{t 'form.name.label'}}
@helperText={{t 'form.name.help'}}
@error={{@model.errors.name}}
readonly={{false}}
as |field|
>
{{#if @model.errors.name}}
<field.errors as |errors|>
{{#each @model.errors.name as |error|}}
<errors.message>{{error.message}}</errors.message>
{{/each}}
</field.errors>
{{/if}}
</form.input>
<form.textarea
@name='description'
@type='description'
@value={{@model.description}}
@label={{t 'form.description.label'}}
@helperText={{t 'form.description.help'}}
@error={{@model.errors.description}}
disabled={{@model.isSaving}}
readonly={{false}}
as |field|
>
{{#if @model.errors.description}}
<field.errors as |errors|>
{{#each @model.errors.description as |error|}}
<errors.message>{{error.message}}</errors.message>
{{/each}}
</field.errors>
{{/if}}
</form.textarea>
{{#if (can 'save model' @model)}}
<form.actions
@disabled={{if @model.cannotSave @model.cannotSave}}
@enableEditText={{t 'actions.edit-form'}}
@submitText={{t 'actions.save'}}
@cancelText={{t 'actions.cancel'}}
/>
{{/if}}
</Rose::Form>
{{component
(concat 'form/host-catalog/' @model.compositeType)
model=@model
submit=@submit
cancel=@cancel
changeType=@changeType
}}
@@ -0,0 +1,120 @@
<Rose::Form
@onSubmit={{@submit}}
@cancel={{@cancel}}
@disabled={{@model.isSaving}}
@error={{@model.errors.base}}
@showEditToggle={{if @model.isNew false true}}
as |form|
>
<form.input
@name='name'
@type='name'
@value={{@model.name}}
@label={{t 'form.name.label'}}
@helperText={{t 'form.name.help'}}
@error={{@model.errors.name}}
disabled={{@model.isSaving}}
readonly={{false}}
as |field|
>
{{#if @model.errors.name}}
<field.errors as |errors|>
{{#each @model.errors.name as |error|}}
<errors.message>{{error.message}}</errors.message>
{{/each}}
</field.errors>
{{/if}}
</form.input>
<form.textarea
@name='description'
@type='description'
@value={{@model.description}}
@label={{t 'form.description.label'}}
@helperText={{t 'form.description.help'}}
@error={{@model.errors.description}}
disabled={{@model.isSaving}}
readonly={{false}}
as |field|
>
{{#if @model.errors.description}}
<field.errors as |errors|>
{{#each @model.errors.description as |error|}}
<errors.message>{{error.message}}</errors.message>
{{/each}}
</field.errors>
{{/if}}
</form.textarea>
{{#if @model.isNew}}
<form.fieldset>
<:title>{{t 'form.type.label'}}</:title>
<:body>
<form.radioGroup
@name='types'
@selectedValue={{@model.type}}
@changed={{@changeType}}
as |radioGroup|
>
<radioGroup.card
@label={{t 'resources.host-catalog.types.static.label'}}
@value='static'
@helperText={{t 'resources.host-catalog.types.static.help'}}
@layout='wide'
/>
<radioGroup.card
@label={{t 'resources.host-catalog.types.dynamic.label'}}
@value='plugin'
@helperText={{t 'resources.host-catalog.types.dynamic.help'}}
@layout='wide'
/>
</form.radioGroup>
</:body>
</form.fieldset>
{{#if @model.isPlugin}}
<Rose::Separator />
<form.fieldset>
<:title>{{t 'resources.host-catalog.form.provider.label'}}</:title>
</form.fieldset>
<form.fieldset>
<:title>{{t
'resources.host-catalog.form.provider.choose_a_provider.label'
}}</:title>
<:description>{{t
'resources.host-catalog.form.provider.choose_a_provider.description'
}}</:description>
<:body>
<form.radioGroup
@name='provider'
@selectedValue={{@model.compositeType}}
@changed={{@changeType}}
as |radioGroup|
>
<radioGroup.card
@label={{t 'resources.host-catalog.types.aws.label'}}
@value='aws'
@layout='tile'
/>
<radioGroup.card
@label={{t 'resources.host-catalog.types.azure.label'}}
@value='azure'
@layout='tile'
/>
</form.radioGroup>
</:body>
</form.fieldset>
{{/if}}
{{/if}}
{{#if (can 'save model' @model)}}
<form.actions
@disabled={{if @model.cannotSave @model.cannotSave}}
@enableEditText={{t 'actions.edit-form'}}
@submitText={{t 'actions.save'}}
@cancelText={{t 'actions.cancel'}}
/>
{{/if}}
</Rose::Form>
@@ -0,0 +1,7 @@
import Controller from '@ember/controller';
export default class ScopesScopeHostCatalogsNewController extends Controller {
// =attributes
queryParams = ['type'];
}
@@ -1,17 +1,53 @@
import Route from '@ember/routing/route';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';
export default class ScopesScopeHostCatalogsNewRoute extends Route {
// =services
@service router;
// =attributes
queryParams = {
type: {
refreshModel: true,
},
};
// =methods
/**
* Rollback/destroy any new, unsaved instances from this route before
* creating another.
*/
beforeModel() {
if (this.currentModel?.isNew) this.currentModel.rollbackAttributes();
}
/**
* Creates a new unsaved host catalog belonging to the current scope.
* @return {HostCatalogModel}
*/
model() {
const scopeModel = this.modelFor('scopes.scope');
return this.store.createRecord('host-catalog', {
type: 'static',
scopeModel,
});
return this.store.createRecord('host-catalog', { scopeModel });
}
afterModel(model, transition) {
this.changeType(transition.to.queryParams?.type);
}
/**
* Update type of host catalog
* @param {string} type
*/
@action
async changeType(type) {
const model = this.modelFor('scopes.scope.host-catalogs.new');
if (!type) type = 'static'; // Unknown host catalog type defaults to 'static'
if (model.isUnknown) type = 'aws'; //Is this the default case for plugin type?
model.compositeType = type;
await this.router.replaceWith({ queryParams: { type } });
}
}
@@ -62,6 +62,7 @@
(concat
'resources.host-catalog.types.'
hostCatalog.compositeType
'.label'
)
}}
</Rose::Badge>
@@ -71,6 +72,7 @@
(concat
'resources.host-catalog.types.'
hostCatalog.compositeType
'.label'
)
}}
</Rose::Badge>
@@ -86,6 +88,7 @@
(concat
'resources.host-catalog.types.'
hostCatalog.compositeType
'.label'
)
}}
</Rose::Badge>
@@ -18,6 +18,7 @@
@model={{@model}}
@submit={{route-action 'save' @model}}
@cancel={{route-action 'cancel' @model}}
@changeType={{route-action 'changeType'}}
/>
</page.body>
@@ -31,6 +31,8 @@ module('Acceptance | host-catalogs | create', function (hooks) {
hostCatalogs: null,
hostCatalog: null,
newHostCatalog: null,
newStaticHostCatalog: null,
newDynamicHostCatalog: null,
};
hooks.beforeEach(function () {
@@ -64,6 +66,8 @@ module('Acceptance | host-catalogs | create', function (hooks) {
urls.hostCatalog = `${urls.hostCatalogs}/${instances.hostCatalog.id}`;
urls.newHostCatalog = `${urls.hostCatalogs}/new`;
urls.newStaticHostCatalog = `${urls.newHostCatalog}?type=static`;
urls.newDynamicHostCatalog = `${urls.newHostCatalog}?type=aws`;
// Generate resource couner
gethostCatalogCount = () =>
this.server.schema.hostCatalogs.all().models.length;
@@ -71,26 +75,49 @@ module('Acceptance | host-catalogs | create', function (hooks) {
authenticateSession({});
});
test('Users can create new host catalogs', async function (assert) {
test('Users can create new static host catalogs', async function (assert) {
assert.expect(1);
const count = gethostCatalogCount();
await visit(urls.newHostCatalog);
await visit(urls.newStaticHostCatalog);
await fillIn('[name="name"]', 'random string');
await fillIn('[name="description"]', 'random string');
await fillIn('[name="types"]', 'static');
await click('[type="submit"]');
assert.equal(gethostCatalogCount(), count + 1);
});
test('Users can cancel create new host catalogs', async function (assert) {
test('Users can create new dynamic host catalogs', async function (assert) {
assert.expect(1);
const count = gethostCatalogCount();
await visit(urls.newDynamicHostCatalog);
await fillIn('[name="name"]', 'random string');
await fillIn('[name="description"]', 'random string');
await fillIn('[name="types"]', 'aws');
await click('[type="submit"]');
assert.equal(gethostCatalogCount(), count + 1);
});
test('Users can cancel creation of new static host catalogs', async function (assert) {
assert.expect(2);
const count = gethostCatalogCount();
await visit(urls.newHostCatalog);
await visit(urls.newStaticHostCatalog);
await fillIn('[name="name"]', 'random string');
await click('.rose-form-actions [type="button"]');
assert.equal(currentURL(), urls.hostCatalogs);
assert.equal(gethostCatalogCount(), count);
});
test('Users can navigate to new host catalogs route with proper authorization', async function (assert) {
test('Users can cancel creation of new dynamic host catalogs', async function (assert) {
assert.expect(2);
const count = gethostCatalogCount();
await visit(urls.newDynamicHostCatalog);
await fillIn('[name="name"]', 'random string');
await click('.rose-form-actions [type="button"]');
assert.equal(currentURL(), urls.hostCatalogs);
assert.equal(gethostCatalogCount(), count);
});
test('Users can navigate to new static host catalogs route with proper authorization', async function (assert) {
assert.expect(2);
await visit(urls.hostCatalogs);
assert.ok(
@@ -101,7 +128,7 @@ module('Acceptance | host-catalogs | create', function (hooks) {
assert.ok(find(`[href="${urls.newHostCatalog}"]`));
});
test('Users cannot navigate to new host catalogs route without proper authorization', async function (assert) {
test('Users cannot navigate to new static host catalogs route without proper authorization', async function (assert) {
assert.expect(2);
instances.scopes.project.authorized_collection_actions['host-catalogs'] =
[];
@@ -111,10 +138,10 @@ module('Acceptance | host-catalogs | create', function (hooks) {
'host-catalogs'
].includes('create')
);
assert.notOk(find(`[href="${urls.newHostCatalog}"]`));
assert.notOk(find(`[href="${urls.newStaticHostCatalog}"]`));
});
test('saving a new host catalog with invalid fields displays error messages', async function (assert) {
test('saving a new static host catalog with invalid fields displays error messages', async function (assert) {
assert.expect(2);
this.server.post('/host-catalogs', () => {
return new Response(
@@ -135,7 +162,7 @@ module('Acceptance | host-catalogs | create', function (hooks) {
}
);
});
await visit(urls.newHostCatalog);
await visit(urls.newStaticHostCatalog);
await click('[type="submit"]');
assert.ok(
find('[role="alert"]').textContent.trim(),
@@ -0,0 +1,14 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Unit | Controller | scopes/scope/host-catalogs/new', function (hooks) {
setupTest(hooks);
// TODO: Replace this with your real tests.
test('it exists', function (assert) {
let controller = this.owner.lookup(
'controller:scopes/scope/host-catalogs/new'
);
assert.ok(controller);
});
});