feat: 🎸 Add create worker page (#1244)
* feat: 🎸 Add create worker page ✅ Closes: ICU-5246
This commit is contained in:
1 parent
dd0d37241c
commit
3130831ae0
20 files changed
+507
-66
No files matched your search
@@ -37,12 +37,13 @@ export default class WorkerModel extends GeneratedWorkerModel {
|
||||
config_tags;
|
||||
|
||||
/**
|
||||
* Method to modify the adpater to handle custom POST route for creating worker.
|
||||
* Method to modify the adapter to handle custom POST route for creating worker.
|
||||
* @param {string} workerGeneratedAuthToken
|
||||
* @param {object} options
|
||||
* @param {object} options.adapterOptions
|
||||
* @return {Promise}
|
||||
*/
|
||||
addWorkerLed(workerGeneratedAuthToken, options = { adapterOptions: {} }) {
|
||||
createWorkerLed(workerGeneratedAuthToken, options = { adapterOptions: {} }) {
|
||||
const defaultAdapterOptions = {
|
||||
method: 'create:worker-led',
|
||||
workerGeneratedAuthToken,
|
||||
|
||||
@@ -30,7 +30,7 @@ export default class WorkerSerializer extends ApplicationSerializer {
|
||||
*/
|
||||
serializeWithGeneratedToken(snapshot, workerGeneratedAuthToken) {
|
||||
return {
|
||||
version: snapshot.attr('version'),
|
||||
scope_id: snapshot.attr('scope').scope_id,
|
||||
worker_generated_auth_token: workerGeneratedAuthToken,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -645,6 +645,16 @@ export default function () {
|
||||
this.get('/workers/:id');
|
||||
this.del('/workers/:id');
|
||||
this.patch('/workers/:id');
|
||||
this.post('/workers:create:worker-led', ({ workers, scopes }) => {
|
||||
const globalScope = scopes.find('global');
|
||||
|
||||
// This POST only takes in a token so we need to generate a random worker to return
|
||||
const newWorker = this.create('worker', {
|
||||
type: 'pki',
|
||||
scope: globalScope,
|
||||
});
|
||||
return workers.create(newWorker.attrs);
|
||||
});
|
||||
|
||||
/* Uncomment the following line and the Response import above
|
||||
* Then change the response code to simulate error responses.
|
||||
|
||||
@@ -6,19 +6,29 @@ module('Unit | Model | worker', function (hooks) {
|
||||
setupTest(hooks);
|
||||
setupMirage(hooks);
|
||||
|
||||
test('it has an `addWorkerLed` method that targets a specific POST API', async function (assert) {
|
||||
test('it has a `createWorkerLed` method that targets a specific POST API', async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
const scopeId = 'global';
|
||||
const workerGeneratedAuthToken = 'token';
|
||||
this.server.post('/v1/workers:create:worker-led', (schema, request) => {
|
||||
const body = JSON.parse(request.requestBody);
|
||||
assert.deepEqual(body, {
|
||||
worker_generated_auth_token: 'token',
|
||||
scope_id: scopeId,
|
||||
worker_generated_auth_token: workerGeneratedAuthToken,
|
||||
});
|
||||
return { id: '123abc' };
|
||||
});
|
||||
|
||||
const store = this.owner.lookup('service:store');
|
||||
const model = store.createRecord('worker', { type: 'pki' });
|
||||
await model.addWorkerLed('token');
|
||||
const model = store.createRecord('worker', {
|
||||
type: 'pki',
|
||||
scope: {
|
||||
scope_id: scopeId,
|
||||
type: 'global',
|
||||
},
|
||||
});
|
||||
await model.createWorkerLed(workerGeneratedAuthToken);
|
||||
const worker = store.peekRecord('worker', '123abc');
|
||||
assert.ok(worker);
|
||||
});
|
||||
|
||||
@@ -23,12 +23,17 @@ module('Unit | Serializer | worker', function (hooks) {
|
||||
});
|
||||
|
||||
test('it serializes using `adapterOptions.workerGeneratedAuthToken`', function (assert) {
|
||||
const scopeId = 'global';
|
||||
const store = this.owner.lookup('service:store');
|
||||
const serializer = store.serializerFor('worker');
|
||||
const record = store.createRecord('worker', {
|
||||
name: 'worker',
|
||||
description: 'Description',
|
||||
version: 1,
|
||||
scope: {
|
||||
scope_id: scopeId,
|
||||
type: 'global',
|
||||
},
|
||||
});
|
||||
const snapshot = record._createSnapshot();
|
||||
snapshot.adapterOptions = {
|
||||
@@ -36,7 +41,7 @@ module('Unit | Serializer | worker', function (hooks) {
|
||||
};
|
||||
const serializedRecord = serializer.serialize(snapshot);
|
||||
assert.deepEqual(serializedRecord, {
|
||||
version: 1,
|
||||
scope_id: scopeId,
|
||||
worker_generated_auth_token: '123-abc',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,7 +26,7 @@ auth-method:
|
||||
title: No Auth Methods
|
||||
description: No auth methods were found for this scope.
|
||||
no-primary-auth-method:
|
||||
title: "Recommendation: Select a Primary Auth Method"
|
||||
title: 'Recommendation: Select a Primary Auth Method'
|
||||
description: A primary auth method auto-creates users when they authenticate for the first time. Scopes without primary auth methods may experience authentication failures.
|
||||
pending:
|
||||
title: Authentication Pending
|
||||
@@ -52,7 +52,7 @@ auth-method:
|
||||
questions:
|
||||
make-primary: Make primary auth method?
|
||||
remove-as-primary: Remove primary auth method?
|
||||
make-primary-confirm: "A primary auth method auto-creates users on their first authentication attempt if needed. Note: if you change the primary auth method, the previous primary will no longer auto-create users."
|
||||
make-primary-confirm: 'A primary auth method auto-creates users on their first authentication attempt if needed. Note: if you change the primary auth method, the previous primary will no longer auto-create users.'
|
||||
remove-as-primary-confirm: Removing the primary auth method may lead to authentication failures for this scope.
|
||||
no-see-window: Don't see the new window?
|
||||
group:
|
||||
@@ -436,9 +436,40 @@ worker:
|
||||
title: Worker
|
||||
title_plural: Workers
|
||||
description: Workers are the server components that perform the session handling.
|
||||
actions:
|
||||
register: Register Worker
|
||||
messages:
|
||||
welcome:
|
||||
title: Welcome to Workers
|
||||
table:
|
||||
session_count: Session Count
|
||||
ip_address: IP Address
|
||||
form:
|
||||
cluster_id:
|
||||
label: Boundary Cluster ID
|
||||
help: The cluster ID is the UUID in the HCP Boundary Controller URL
|
||||
ip_address:
|
||||
label: IP Address
|
||||
help: The public IP address of the server
|
||||
config_file_path:
|
||||
label: Config file path
|
||||
help: The path on your server where you will store your configuration file
|
||||
worker_tags:
|
||||
label: Worker Tags
|
||||
help: Boundary can use tags to define key-value pairs which targets can use to determine where they should route connections
|
||||
steps:
|
||||
1:
|
||||
title: 1. Create a config file
|
||||
create_directory: Create a configuration file in a new directory on your server
|
||||
create_config: 'Open the file with a text editor, such as Vi. Paste the following configuration into the worker config file:'
|
||||
save_config: Save the configuration in your `pki-worker.hcl` file.
|
||||
2:
|
||||
title: 2. Install & start Boundary on your server
|
||||
run_command: 'Run this command on your server to install Boundary:'
|
||||
copy_registration_request: When the server starts, it will print details related to this worker. Copy the value for
|
||||
3:
|
||||
title: 3. Confirm the worker when they connect
|
||||
worker_auth_registration_request:
|
||||
label: Worker Auth Registration Request
|
||||
help: The registration key provided by this worker when it started
|
||||
registered: Registered
|
||||
@@ -31,8 +31,6 @@
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background-color: var(--ui-gray-subtler-6);
|
||||
border-top: 1px solid var(--ui-gray-subtler-3);
|
||||
border-bottom: 1px solid var(--ui-gray-subtler-3);
|
||||
height: 2.75rem;
|
||||
|
||||
@@ -72,6 +70,7 @@
|
||||
margin-right: sizing.rems(xs);
|
||||
margin-left: sizing.rems(s);
|
||||
height: sizing.rems(xl);
|
||||
white-space: nowrap;
|
||||
|
||||
&-icon {
|
||||
margin-right: sizing.rems(xs);
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
|
||||
.rose-layout-page-body {
|
||||
grid-area: body;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
<Rose::Form
|
||||
@onSubmit={{fn @submit this.generatedWorkerAuthToken}}
|
||||
@cancel={{@cancel}}
|
||||
@disabled={{@model.isSaving}}
|
||||
@showEditToggle={{false}}
|
||||
as |form|
|
||||
>
|
||||
<form.input
|
||||
@name='cluster_id'
|
||||
@value={{this.clusterId}}
|
||||
@label={{t 'resources.worker.form.cluster_id.label'}}
|
||||
@helperText={{t 'resources.worker.form.cluster_id.help'}}
|
||||
@linkText={{t 'actions.learn-more'}}
|
||||
@link={{doc-url 'worker.manage-workers'}}
|
||||
placeholder='69b6ddb3-ffec-42ab-a994-f43a6519470a'
|
||||
/>
|
||||
|
||||
<form.input
|
||||
@name='ip_address'
|
||||
@value={{this.ipAddress}}
|
||||
@label={{t 'resources.worker.form.ip_address.label'}}
|
||||
@helperText={{t 'resources.worker.form.ip_address.help'}}
|
||||
placeholder='127.0.0.1:9202'
|
||||
/>
|
||||
|
||||
<form.input
|
||||
@name='config_file_path'
|
||||
@value={{this.configFilePath}}
|
||||
@label={{t 'resources.worker.form.config_file_path.label'}}
|
||||
@helperText={{t 'resources.worker.form.config_file_path.help'}}
|
||||
placeholder='/home/ubuntu/boundary'
|
||||
/>
|
||||
|
||||
<form.input
|
||||
@name='worker_tags'
|
||||
@value={{this.workerTags}}
|
||||
@label={{t 'resources.worker.form.worker_tags.label'}}
|
||||
@helperText={{t 'resources.worker.form.worker_tags.help'}}
|
||||
placeholder='dev-worker, ubuntu'
|
||||
/>
|
||||
|
||||
<h3>{{t 'resources.worker.form.steps.1.title'}}</h3>
|
||||
<div class='worker-create-section'>
|
||||
<Rose::CodeEditor @codeValue={{this.createConfigText}} as |c|>
|
||||
<c.toolbar>
|
||||
<:action>
|
||||
<span>{{t 'resources.worker.form.steps.1.create_directory'}}</span>
|
||||
</:action>
|
||||
</c.toolbar>
|
||||
<c.fieldEditor class='rose-autosize' @options={{this.shellCodeEditor}} />
|
||||
</Rose::CodeEditor>
|
||||
|
||||
<Rose::CodeEditor @codeValue={{this.workerConfigText}} as |c|>
|
||||
<c.toolbar>
|
||||
<:action>
|
||||
<span>{{t 'resources.worker.form.steps.1.create_config'}}</span>
|
||||
</:action>
|
||||
</c.toolbar>
|
||||
<c.fieldEditor class='rose-autosize' @options={{this.hclCodeEditor}} />
|
||||
</Rose::CodeEditor>
|
||||
<p>{{t 'resources.worker.form.steps.1.save_config'}}</p>
|
||||
</div>
|
||||
|
||||
<h3>{{t 'resources.worker.form.steps.2.title'}}</h3>
|
||||
<div class='worker-create-section'>
|
||||
<Rose::CodeEditor @codeValue={{this.installBoundaryText}} as |c|>
|
||||
<c.toolbar>
|
||||
<:action>
|
||||
<span>{{t 'resources.worker.form.steps.2.run_command'}}</span>
|
||||
</:action>
|
||||
</c.toolbar>
|
||||
<c.fieldEditor class='rose-autosize' @options={{this.shellCodeEditor}} />
|
||||
</Rose::CodeEditor>
|
||||
<span>{{t 'resources.worker.form.steps.2.copy_registration_request'}}</span>
|
||||
<code>{{t
|
||||
'resources.worker.form.steps.3.worker_auth_registration_request.label'
|
||||
}}
|
||||
</code>
|
||||
</div>
|
||||
|
||||
<h3>{{t 'resources.worker.form.steps.3.title'}}</h3>
|
||||
<div class='worker-create-section'>
|
||||
<div class='worker-auth-token'>
|
||||
<form.input
|
||||
@name='worker_auth_registration_request'
|
||||
@value={{this.generatedWorkerAuthToken}}
|
||||
@disabled={{@model.cannotSave}}
|
||||
@label={{t
|
||||
'resources.worker.form.steps.3.worker_auth_registration_request.label'
|
||||
}}
|
||||
@helperText={{t
|
||||
'resources.worker.form.steps.3.worker_auth_registration_request.help'
|
||||
}}
|
||||
/>
|
||||
{{#if (can 'save worker' @model)}}
|
||||
<Rose::Button
|
||||
@submit={{true}}
|
||||
@style='primary'
|
||||
class='nowrap'
|
||||
@disabled={{@model.cannotSave}}
|
||||
>
|
||||
{{t 'resources.worker.actions.register'}}
|
||||
</Rose::Button>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
{{#if @model.cannotSave}}
|
||||
<div class='worker-success'>
|
||||
<div>
|
||||
<Rose::Icon @name='flight-icons/svg/cpu-16' @size='medium' />
|
||||
<p>{{this.generatedWorkerAuthToken}}</p>
|
||||
</div>
|
||||
<div>
|
||||
<Rose::Icon
|
||||
@name='flight-icons/svg/check-circle-fill-16'
|
||||
@size='medium'
|
||||
/>
|
||||
<p>{{t 'resources.worker.form.steps.3.registered'}}</p>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<Rose::LinkButton @route='scopes.scope.workers' @style='secondary'>
|
||||
{{t 'actions.done'}}
|
||||
</Rose::LinkButton>
|
||||
</Rose::Form>
|
||||
@@ -0,0 +1,68 @@
|
||||
import Component from '@glimmer/component';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
|
||||
export default class FormWorkerCreateWorkerLedComponent extends Component {
|
||||
// =attributes
|
||||
|
||||
@tracked generatedWorkerAuthToken;
|
||||
@tracked clusterId;
|
||||
@tracked ipAddress;
|
||||
@tracked configFilePath;
|
||||
@tracked workerTags;
|
||||
|
||||
get createConfigText() {
|
||||
return `mkdir ${this.configFilePath || '<path>'}/ ;\\
|
||||
touch ${this.configFilePath || '<path>'}/pki-worker.hcl`;
|
||||
}
|
||||
|
||||
get workerConfigText() {
|
||||
return `hcp_boundary_cluster_id = "${this.clusterId || '<config_id>'}"
|
||||
|
||||
listener "tcp" {
|
||||
address = "0.0.0.0:9202"
|
||||
purpose = "proxy"
|
||||
}
|
||||
|
||||
worker {
|
||||
public_addr = "${this.ipAddress || '<public_ip_address>'}"
|
||||
auth_storage_path = "${this.configFilePath || '<path>'}/worker1"
|
||||
tags {
|
||||
type = [${
|
||||
this.workerTags
|
||||
? this.workerTags
|
||||
.split(',')
|
||||
.filter((tag) => tag.trim())
|
||||
.map((tag) => `"${tag?.trim()}"`)
|
||||
.join(', ')
|
||||
: '"<tag1>", "<tag2>"'
|
||||
}]
|
||||
}
|
||||
}`;
|
||||
}
|
||||
|
||||
get installBoundaryText() {
|
||||
return `curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo apt-key add - ;\\
|
||||
sudo apt-add-repository "deb [arch=amd64] https://apt.releases.hashicorp.com $(lsb_release -cs) main" ;\\
|
||||
sudo apt-get update && sudo apt-get install boundary ;\\
|
||||
boundary server -config="${this.configFilePath || '<path>'}/pki-worker.hcl"`;
|
||||
}
|
||||
|
||||
get shellCodeEditor() {
|
||||
return {
|
||||
mode: 'shell',
|
||||
readOnly: true,
|
||||
lineNumbers: false,
|
||||
cursorBlinkRate: -1,
|
||||
styleActiveLine: false,
|
||||
};
|
||||
}
|
||||
|
||||
get hclCodeEditor() {
|
||||
return {
|
||||
mode: 'hcl',
|
||||
readOnly: true,
|
||||
cursorBlinkRate: -1,
|
||||
styleActiveLine: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import { action } from '@ember/object';
|
||||
export default class ScopesScopeWorkersRoute extends Route {
|
||||
// =services
|
||||
|
||||
@service can;
|
||||
@service session;
|
||||
@service router;
|
||||
|
||||
@@ -18,23 +17,11 @@ export default class ScopesScopeWorkersRoute extends Route {
|
||||
if (!this.session.isAuthenticated) this.router.transitionTo('index');
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all workers.
|
||||
* @return {WorkerModel}
|
||||
*/
|
||||
model() {
|
||||
const scope = this.modelFor('scopes.scope');
|
||||
const { id: scope_id } = scope;
|
||||
if (this.can.can('list worker', scope, { collection: 'workers' })) {
|
||||
return this.store.query('worker', { scope_id });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes worker data.
|
||||
*/
|
||||
@action
|
||||
refreshWorkers() {
|
||||
refresh() {
|
||||
return super.refresh(...arguments);
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,19 @@
|
||||
import Route from '@ember/routing/route';
|
||||
import { action } from '@ember/object';
|
||||
import { resourceFilter } from 'core/decorators/resource-filter';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class ScopesScopeWorkersIndexRoute extends Route {
|
||||
// =attributes
|
||||
@service can;
|
||||
|
||||
@resourceFilter({
|
||||
allowed: (route) => {
|
||||
const configTags = route
|
||||
.modelFor('scopes.scope.workers')
|
||||
.modelFor('scopes.scope.workers.index')
|
||||
.toArray()
|
||||
.flatMap((worker) => worker.config_tags.type);
|
||||
.flatMap((worker) => worker.config_tags?.type)
|
||||
.filter(Boolean);
|
||||
|
||||
// Filter out duplicate tags
|
||||
return [...new Set(configTags)];
|
||||
@@ -21,18 +24,24 @@ export default class ScopesScopeWorkersIndexRoute extends Route {
|
||||
// =methods
|
||||
|
||||
model() {
|
||||
const workers = this.modelFor('scopes.scope.workers');
|
||||
const scope = this.modelFor('scopes.scope');
|
||||
const { id: scope_id } = scope;
|
||||
if (this.can.can('list worker', scope, { collection: 'workers' })) {
|
||||
const workers = this.store.query('worker', { scope_id });
|
||||
|
||||
if (this.tags?.length) {
|
||||
// Return workers that have config tags that have at
|
||||
// least one intersection with the filter tags
|
||||
return workers.filter(
|
||||
(worker) =>
|
||||
worker.config_tags.type.filter(Set.prototype.has, new Set(this.tags))
|
||||
.length > 0
|
||||
);
|
||||
if (this.tags?.length) {
|
||||
// Return workers that have config tags that have at
|
||||
// least one intersection with the filter tags
|
||||
return workers.filter(
|
||||
(worker) =>
|
||||
worker.config_tags?.type.filter(
|
||||
Set.prototype.has,
|
||||
new Set(this.tags)
|
||||
).length > 0
|
||||
);
|
||||
}
|
||||
return workers;
|
||||
}
|
||||
return workers;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,3 +1,30 @@
|
||||
import Route from '@ember/routing/route';
|
||||
import { action } from '@ember/object';
|
||||
import { loading } from 'ember-loading';
|
||||
import { notifyError, notifySuccess } from 'core/decorators/notify';
|
||||
|
||||
export default class ScopesScopeWorkersNewRoute extends Route {}
|
||||
export default class ScopesScopeWorkersNewRoute extends Route {
|
||||
// =methods
|
||||
|
||||
/**
|
||||
* Creates a new unsaved worker.
|
||||
* @return {WorkerModel}
|
||||
*/
|
||||
model() {
|
||||
const scopeModel = this.modelFor('scopes.scope');
|
||||
return this.store.createRecord('worker', { scopeModel });
|
||||
}
|
||||
|
||||
/**
|
||||
* Created the worker and notifies the user of success or error.
|
||||
* @param {WorkerModel} worker
|
||||
* @param {string} workerGeneratedAuthToken
|
||||
*/
|
||||
@action
|
||||
@loading
|
||||
@notifyError(({ message }) => message, { catch: true })
|
||||
@notifySuccess('notifications.add-success')
|
||||
async createWorkerLed(worker, workerGeneratedAuthToken) {
|
||||
await worker.createWorkerLed(workerGeneratedAuthToken);
|
||||
}
|
||||
}
|
||||
@@ -394,3 +394,60 @@
|
||||
margin-top: sizing.rems(xs);
|
||||
}
|
||||
}
|
||||
|
||||
.worker-create-section {
|
||||
border: sizing.rems(xxxxs) solid var(--ui-gray-subtler-3);
|
||||
border-radius: sizing.rems(xxs);
|
||||
padding: sizing.rems(l);
|
||||
margin-bottom: sizing.rems(xl);
|
||||
|
||||
> div {
|
||||
margin-bottom: sizing.rems(l);
|
||||
}
|
||||
|
||||
> code {
|
||||
color: var(--failure);
|
||||
background-color: var(--ui-gray-subtler-5);
|
||||
border: sizing.rems(xxxxs) solid var(--ui-gray-subtler-2);
|
||||
}
|
||||
|
||||
.worker-auth-token {
|
||||
display: flex;
|
||||
gap: sizing.rems(m);
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.worker-success {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
gap: sizing.rems(xs);
|
||||
align-items: center;
|
||||
p {
|
||||
margin-bottom: sizing.rems(xxs);
|
||||
}
|
||||
}
|
||||
|
||||
> div:first-child {
|
||||
width: 40%;
|
||||
p {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
> div:last-child {
|
||||
p {
|
||||
color: var(--success);
|
||||
}
|
||||
}
|
||||
|
||||
.rose-icon {
|
||||
color: var(--success);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,29 +30,30 @@
|
||||
(resource-filter 'scopes.scope.workers.index' 'tags')
|
||||
as |filter|
|
||||
}}
|
||||
<Rose::Dropdown
|
||||
@text={{t (concat 'form.' filter.name '.label')}}
|
||||
@count={{filter.selectedValue.length}}
|
||||
@ignoreClickInside={{true}}
|
||||
as |dropdown|
|
||||
>
|
||||
<form.checkboxGroup
|
||||
@name={{filter.name}}
|
||||
@items={{filter.allowedValues}}
|
||||
@selectedItems={{filter.selectedValue}}
|
||||
@onChange={{route-action 'filterBy' filter.name}}
|
||||
as |group|
|
||||
{{#if filter.allowedValues}}
|
||||
<Rose::Dropdown
|
||||
@text={{t (concat 'form.' filter.name '.label')}}
|
||||
@count={{filter.selectedValue.length}}
|
||||
@ignoreClickInside={{true}}
|
||||
as |dropdown|
|
||||
>
|
||||
<dropdown.item>
|
||||
<group.checkbox
|
||||
@label={{group.item}}
|
||||
value={{group.item}}
|
||||
/>
|
||||
</dropdown.item>
|
||||
</form.checkboxGroup>
|
||||
</Rose::Dropdown>
|
||||
<form.checkboxGroup
|
||||
@name={{filter.name}}
|
||||
@items={{filter.allowedValues}}
|
||||
@selectedItems={{filter.selectedValue}}
|
||||
@onChange={{route-action 'filterBy' filter.name}}
|
||||
as |group|
|
||||
>
|
||||
<dropdown.item>
|
||||
<group.checkbox
|
||||
@label={{group.item}}
|
||||
value={{group.item}}
|
||||
/>
|
||||
</dropdown.item>
|
||||
</form.checkboxGroup>
|
||||
</Rose::Dropdown>
|
||||
{{/if}}
|
||||
{{/with}}
|
||||
|
||||
{{#if
|
||||
(has-resource-filter-selections
|
||||
'scopes.scope.workers.index' 'tags'
|
||||
@@ -69,7 +70,7 @@
|
||||
</toolbar.primary>
|
||||
|
||||
<toolbar.secondary>
|
||||
<ToolbarRefresher @onClick={{route-action 'refreshWorkers'}} />
|
||||
<ToolbarRefresher @onClick={{route-action 'refresh'}} />
|
||||
</toolbar.secondary>
|
||||
|
||||
</Rose::Toolbar>
|
||||
|
||||
@@ -1,2 +1,23 @@
|
||||
{{page-title (t 'titles.new')}}
|
||||
{{outlet}}
|
||||
|
||||
<Rose::Layout::Page as |page|>
|
||||
|
||||
<page.breadcrumbs>
|
||||
<BreadCrumbs />
|
||||
</page.breadcrumbs>
|
||||
|
||||
<page.header>
|
||||
<h2>
|
||||
{{t 'titles.new'}}
|
||||
<DocLink @doc='worker' @iconSize='large' />
|
||||
</h2>
|
||||
</page.header>
|
||||
|
||||
<page.body>
|
||||
<Form::Worker::CreateWorkerLed
|
||||
@model={{@model}}
|
||||
@submit={{route-action 'createWorkerLed' @model}}
|
||||
/>
|
||||
</page.body>
|
||||
|
||||
</Rose::Layout::Page>
|
||||
@@ -93,7 +93,7 @@ module.exports = function (environment) {
|
||||
'api-client.cli': '/api-client/cli',
|
||||
'api-client.api': '/api-client/api',
|
||||
worker: '/workers',
|
||||
'worker.new': '/workers/new',
|
||||
'worker.manage-workers': '/workers/manage-workers-on-hcp',
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
@@ -34,6 +34,9 @@ module.exports = function (defaults) {
|
||||
app.import('node_modules/clipboard/dist/clipboard.min.js');
|
||||
}
|
||||
|
||||
app.import('node_modules/codemirror/lib/codemirror.css');
|
||||
app.import('node_modules/codemirror/theme/monokai.css');
|
||||
|
||||
// Use `app.import` to add additional libraries to the generated
|
||||
// output files.
|
||||
//
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { visit, fillIn, click, find } from '@ember/test-helpers';
|
||||
import { setupApplicationTest } from 'ember-qunit';
|
||||
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
|
||||
import { Response } from 'miragejs';
|
||||
import {
|
||||
authenticateSession,
|
||||
// These are left here intentionally for future reference.
|
||||
//currentSession,
|
||||
//invalidateSession,
|
||||
} from 'ember-simple-auth/test-support';
|
||||
|
||||
module('Acceptance | workers | create', function (hooks) {
|
||||
setupApplicationTest(hooks);
|
||||
setupMirage(hooks);
|
||||
|
||||
let globalScope;
|
||||
let workersURL;
|
||||
let newWorkerURL;
|
||||
let getWorkersCount;
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
globalScope = this.server.create('scope', { id: 'global' });
|
||||
|
||||
workersURL = `/scopes/global/workers`;
|
||||
newWorkerURL = `${workersURL}/new`;
|
||||
getWorkersCount = () => this.server.schema.workers.all().length;
|
||||
|
||||
authenticateSession({});
|
||||
});
|
||||
|
||||
test('can create new workers', async function (assert) {
|
||||
assert.expect(1);
|
||||
const workersCount = getWorkersCount();
|
||||
await visit(newWorkerURL);
|
||||
await fillIn('[name="worker_auth_registration_request"]', 'token');
|
||||
await click('[type="submit"]');
|
||||
assert.strictEqual(getWorkersCount(), workersCount + 1);
|
||||
});
|
||||
|
||||
test('Users can navigate to new workers route with proper authorization', async function (assert) {
|
||||
assert.expect(2);
|
||||
await visit(newWorkerURL);
|
||||
assert.ok(
|
||||
globalScope.authorized_collection_actions.workers.includes(
|
||||
'create:worker-led'
|
||||
)
|
||||
);
|
||||
assert.dom(`[href="${newWorkerURL}"]`).isVisible();
|
||||
});
|
||||
|
||||
test('Users cannot navigate to new workers route without proper authorization', async function (assert) {
|
||||
assert.expect(2);
|
||||
globalScope.authorized_collection_actions.workers = [];
|
||||
await visit(workersURL);
|
||||
assert.notOk(
|
||||
globalScope.authorized_collection_actions.users.includes(
|
||||
'create:worker-led'
|
||||
)
|
||||
);
|
||||
assert.dom(`[href="${newWorkerURL}"]`).isNotVisible();
|
||||
});
|
||||
|
||||
test('saving a new user with invalid fields displays error messages', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.post('/workers:create:worker-led', () => {
|
||||
return new Response(
|
||||
500,
|
||||
{},
|
||||
{
|
||||
status: 500,
|
||||
code: 'api_error',
|
||||
message: 'rpc error: code = Unknown',
|
||||
}
|
||||
);
|
||||
});
|
||||
await visit(newWorkerURL);
|
||||
await fillIn('[name="worker_auth_registration_request"]', 'token');
|
||||
await click('[type="submit"]');
|
||||
assert.strictEqual(
|
||||
find('.rose-notification-body').textContent.trim(),
|
||||
'rpc error: code = Unknown',
|
||||
'Displays primary error message.'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -26,9 +26,7 @@ module('Acceptance | workers | list', function (hooks) {
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
instances.scopes.global = this.server.create('scope', { id: 'global' });
|
||||
instances.user = this.server.create('user', {
|
||||
scope: instances.scopes.global,
|
||||
});
|
||||
|
||||
urls.globalScope = `/scopes/global/scopes`;
|
||||
urls.workers = `/scopes/global/workers`;
|
||||
authenticateSession({});
|
||||
|
||||
Reference in new issue
Block a user