feat: 🎸 Added list view for workers (#1234)
* feat: 🎸 Add list view for workers ✅ Closes: ICU-5251
This commit is contained in:
1 parent
62077d3c25
commit
4ab7304411
18 files changed
+324
-15
No files matched your search
@@ -4,6 +4,23 @@ import { attr } from '@ember-data/model';
|
||||
export default class WorkerModel extends GeneratedWorkerModel {
|
||||
// =attributes
|
||||
|
||||
/**
|
||||
* Names are optional on models in our API. Thus we need to fallback on ID
|
||||
* for display purposes.
|
||||
* @type {string}
|
||||
*/
|
||||
get displayName() {
|
||||
return this.name || this.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the worker is a pki worker.
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isPki() {
|
||||
return this.type === 'pki';
|
||||
}
|
||||
|
||||
@attr({
|
||||
description:
|
||||
'The deduplicated union of the tags reported by the worker ' +
|
||||
@@ -28,7 +45,7 @@ export default class WorkerModel extends GeneratedWorkerModel {
|
||||
addWorkerLed(workerGeneratedAuthToken, options = { adapterOptions: {} }) {
|
||||
const defaultAdapterOptions = {
|
||||
method: 'create:worker-led',
|
||||
workerGeneratedAuthToken
|
||||
workerGeneratedAuthToken,
|
||||
};
|
||||
return super.save({
|
||||
...options,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import ApplicationSerializer from './application';
|
||||
|
||||
export default class WorkerSerializer extends ApplicationSerializer {
|
||||
// =methods
|
||||
// =methods
|
||||
|
||||
/**
|
||||
* If `adapterOptions.workerGeneratedAuthToken` is set,
|
||||
@@ -10,11 +10,15 @@ export default class WorkerSerializer extends ApplicationSerializer {
|
||||
* @param {Snapshot} snapshot
|
||||
* @return {object}
|
||||
*/
|
||||
serialize(snapshot) {
|
||||
serialize(snapshot) {
|
||||
let serialized = super.serialize(...arguments);
|
||||
const workerGeneratedAuthToken = snapshot?.adapterOptions?.workerGeneratedAuthToken;
|
||||
const workerGeneratedAuthToken =
|
||||
snapshot?.adapterOptions?.workerGeneratedAuthToken;
|
||||
if (workerGeneratedAuthToken)
|
||||
serialized = this.serializeWithGeneratedToken(snapshot, workerGeneratedAuthToken);
|
||||
serialized = this.serializeWithGeneratedToken(
|
||||
snapshot,
|
||||
workerGeneratedAuthToken
|
||||
);
|
||||
return serialized;
|
||||
}
|
||||
|
||||
@@ -27,7 +31,7 @@ export default class WorkerSerializer extends ApplicationSerializer {
|
||||
serializeWithGeneratedToken(snapshot, workerGeneratedAuthToken) {
|
||||
return {
|
||||
version: snapshot.attr('version'),
|
||||
worker_generated_auth_token: workerGeneratedAuthToken
|
||||
worker_generated_auth_token: workerGeneratedAuthToken,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -642,8 +642,9 @@ export default function () {
|
||||
return workers.where({ scopeId });
|
||||
}
|
||||
);
|
||||
this.get('/worker/:id');
|
||||
this.patch('/worker/:id');
|
||||
this.get('/workers/:id');
|
||||
this.del('/workers/:id');
|
||||
this.patch('/workers/:id');
|
||||
|
||||
/* Uncomment the following line and the Response import above
|
||||
* Then change the response code to simulate error responses.
|
||||
|
||||
@@ -4,7 +4,6 @@ import generateId from '../helpers/id';
|
||||
import { faker } from '@faker-js/faker';
|
||||
|
||||
const types = ['pki', 'kms'];
|
||||
const tags = ['dev', faker.random.word(), faker.random.word()];
|
||||
|
||||
export default factory.extend({
|
||||
id: () => generateId('w_'),
|
||||
@@ -16,10 +15,10 @@ export default factory.extend({
|
||||
'delete',
|
||||
],
|
||||
type: (i) => types[i % types.length],
|
||||
canonical_tags: () => ({
|
||||
type: tags,
|
||||
canonical_tags: (i) => ({
|
||||
type: i % 2 === 0 ? ['dev', faker.random.word(), faker.random.word()] : [],
|
||||
}),
|
||||
config_tags: () => ({
|
||||
type: tags,
|
||||
config_tags: (i) => ({
|
||||
type: i % 2 === 0 ? ['dev', faker.random.word(), faker.random.word()] : [],
|
||||
}),
|
||||
});
|
||||
@@ -3,7 +3,7 @@ import { faker } from '@faker-js/faker';
|
||||
|
||||
/**
|
||||
* GeneratedWorkerModelFactory
|
||||
* User contains all fields related to a Worker resource
|
||||
* Worker contains all fields related to a Worker resource
|
||||
*/
|
||||
export default Factory.extend({
|
||||
name: () => faker.random.words(),
|
||||
|
||||
@@ -149,3 +149,5 @@ ip_addresses:
|
||||
sync-interval:
|
||||
label: Sync Interval
|
||||
help: Number of seconds between the time Boundary syncs hosts in this set.
|
||||
tags:
|
||||
label: Tags
|
||||
@@ -423,3 +423,6 @@ worker:
|
||||
messages:
|
||||
welcome:
|
||||
title: Welcome to Workers
|
||||
table:
|
||||
session_count: Session Count
|
||||
ip_address: IP Address
|
||||
@@ -127,6 +127,7 @@ Router.map(function () {
|
||||
});
|
||||
this.route('workers', function () {
|
||||
this.route('new');
|
||||
this.route('worker', { path: ':worker_id' }, function () {});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
|
||||
export default class ScopesScopeWorkersRoute extends Route {
|
||||
// =services
|
||||
@@ -28,4 +29,12 @@ export default class ScopesScopeWorkersRoute extends Route {
|
||||
return this.store.query('worker', { scope_id });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes worker data.
|
||||
*/
|
||||
@action
|
||||
refreshWorkers() {
|
||||
return super.refresh(...arguments);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,58 @@
|
||||
import Route from '@ember/routing/route';
|
||||
import { action } from '@ember/object';
|
||||
import { resourceFilter } from 'core/decorators/resource-filter';
|
||||
|
||||
export default class ScopesScopeWorkersIndexRoute extends Route {
|
||||
// =attributes
|
||||
|
||||
@resourceFilter({
|
||||
allowed: (route) => {
|
||||
const configTags = route
|
||||
.modelFor('scopes.scope.workers')
|
||||
.toArray()
|
||||
.flatMap((worker) => worker.config_tags.type);
|
||||
|
||||
// Filter out duplicate tags
|
||||
return [...new Set(configTags)];
|
||||
},
|
||||
})
|
||||
tags;
|
||||
|
||||
// =methods
|
||||
|
||||
model() {
|
||||
const workers = this.modelFor('scopes.scope.workers');
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears filter selections.
|
||||
*/
|
||||
@action
|
||||
clearAllFilters() {
|
||||
this.tags = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the specified resource filter field to the specified value.
|
||||
* @param {string} field
|
||||
* @param value
|
||||
*/
|
||||
@action
|
||||
filterBy(field, value) {
|
||||
this[field] = value;
|
||||
}
|
||||
|
||||
setupController(controller) {
|
||||
const scope = this.modelFor('scopes.scope');
|
||||
super.setupController(...arguments);
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import Route from '@ember/routing/route';
|
||||
|
||||
export default class ScopesScopeWorkersWorkerRoute extends Route {
|
||||
// =methods
|
||||
|
||||
/**
|
||||
* Load a worker in current scope.
|
||||
* @param {object} params
|
||||
* @param {string} params.worker_id
|
||||
* @return {WorkerModel}
|
||||
*/
|
||||
model({ worker_id }) {
|
||||
return this.store.findRecord('worker', worker_id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import Route from '@ember/routing/route';
|
||||
|
||||
export default class ScopesScopeWorkersWorkerIndexRoute extends Route {}
|
||||
@@ -23,7 +23,112 @@
|
||||
|
||||
<page.body>
|
||||
{{#if @model}}
|
||||
{{! TODO: List view }}
|
||||
<Rose::Toolbar as |toolbar|>
|
||||
<toolbar.primary>
|
||||
<Rose::Form as |form|>
|
||||
{{#with
|
||||
(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|
|
||||
>
|
||||
<dropdown.item>
|
||||
<group.checkbox
|
||||
@label={{group.item}}
|
||||
value={{group.item}}
|
||||
/>
|
||||
</dropdown.item>
|
||||
</form.checkboxGroup>
|
||||
</Rose::Dropdown>
|
||||
{{/with}}
|
||||
|
||||
{{#if
|
||||
(has-resource-filter-selections
|
||||
'scopes.scope.workers.index' 'tags'
|
||||
)
|
||||
}}
|
||||
<Rose::Button
|
||||
@style='inline-link-action'
|
||||
@iconLeft='flight-icons/svg/x-16'
|
||||
{{on 'click' (route-action 'clearAllFilters')}}
|
||||
>{{t 'actions.clear-all-filters'}}
|
||||
</Rose::Button>
|
||||
{{/if}}
|
||||
</Rose::Form>
|
||||
</toolbar.primary>
|
||||
|
||||
<toolbar.secondary>
|
||||
<ToolbarRefresher @onClick={{route-action 'refreshWorkers'}} />
|
||||
</toolbar.secondary>
|
||||
|
||||
</Rose::Toolbar>
|
||||
|
||||
<Rose::Table as |table|>
|
||||
<table.header as |header|>
|
||||
<header.row as |row|>
|
||||
<row.headerCell>{{t 'resources.worker.title'}}</row.headerCell>
|
||||
<row.headerCell>{{t 'form.id.label'}}</row.headerCell>
|
||||
<row.headerCell>{{t 'form.type.label'}}</row.headerCell>
|
||||
<row.headerCell>{{t
|
||||
'resources.worker.table.session_count'
|
||||
}}</row.headerCell>
|
||||
<row.headerCell>{{t
|
||||
'resources.worker.table.ip_address'
|
||||
}}</row.headerCell>
|
||||
</header.row>
|
||||
</table.header>
|
||||
<table.body as |body|>
|
||||
{{#each @model as |worker|}}
|
||||
<body.row as |row|>
|
||||
<row.headerCell>
|
||||
{{#if (and (can 'read worker' worker) worker.isPki)}}
|
||||
<LinkTo
|
||||
@route='scopes.scope.workers.worker'
|
||||
@model={{worker.id}}
|
||||
>
|
||||
{{worker.displayName}}
|
||||
</LinkTo>
|
||||
{{else}}
|
||||
{{worker.displayName}}
|
||||
{{/if}}
|
||||
{{#if worker.description}}
|
||||
<p>{{worker.description}}</p>
|
||||
{{/if}}
|
||||
{{#if worker.config_tags.type}}
|
||||
<div>
|
||||
{{#each worker.config_tags.type as |tag|}}
|
||||
<Rose::Badge>{{tag}}</Rose::Badge>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</row.headerCell>
|
||||
<row.cell>
|
||||
<Copyable
|
||||
@text={{worker.id}}
|
||||
@buttonText={{t 'actions.copy-to-clipboard'}}
|
||||
@acknowledgeText={{t 'states.copied'}}
|
||||
>
|
||||
<code>{{worker.id}}</code>
|
||||
</Copyable></row.cell>
|
||||
|
||||
<row.cell>{{worker.type}}</row.cell>
|
||||
<row.cell>{{worker.active_connection_count}}</row.cell>
|
||||
<row.cell>{{worker.address}} </row.cell>
|
||||
</body.row>
|
||||
{{/each}}
|
||||
</table.body>
|
||||
</Rose::Table>
|
||||
{{else}}
|
||||
<Rose::Layout::Centered>
|
||||
<Rose::Message
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
{{page-title @model.displayName}}
|
||||
|
||||
{{outlet}}
|
||||
@@ -0,0 +1,2 @@
|
||||
{{page-title 'Worker'}}
|
||||
{{outlet}}
|
||||
@@ -0,0 +1,73 @@
|
||||
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 {
|
||||
authenticateSession,
|
||||
// These are left here intentionally for future reference.
|
||||
//currentSession,
|
||||
//invalidateSession,
|
||||
} from 'ember-simple-auth/test-support';
|
||||
|
||||
module('Acceptance | workers | list', function (hooks) {
|
||||
setupApplicationTest(hooks);
|
||||
setupMirage(hooks);
|
||||
|
||||
const instances = {
|
||||
scopes: {
|
||||
global: null,
|
||||
},
|
||||
};
|
||||
|
||||
const urls = {
|
||||
globalScope: null,
|
||||
workers: null,
|
||||
};
|
||||
|
||||
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({});
|
||||
});
|
||||
|
||||
test('Users can navigate to workers with proper authorization', async function (assert) {
|
||||
assert.expect(2);
|
||||
await visit(urls.globalScope);
|
||||
assert.ok(
|
||||
instances.scopes.global.authorized_collection_actions.workers.includes(
|
||||
'list'
|
||||
)
|
||||
);
|
||||
assert.dom(`[href="${urls.workers}"]`).isVisible();
|
||||
});
|
||||
|
||||
test('Users cannot navigate to workers without list or create permissions', async function (assert) {
|
||||
assert.expect(1);
|
||||
instances.scopes.global.authorized_collection_actions.workers = [];
|
||||
|
||||
await visit(urls.globalScope);
|
||||
assert.dom(`[href="${urls.workers}"]`).isNotVisible();
|
||||
});
|
||||
|
||||
test('Users can navigate to workers with only create permission', async function (assert) {
|
||||
assert.expect(1);
|
||||
instances.scopes.global.authorized_collection_actions.workers = [
|
||||
'create:worker-led',
|
||||
];
|
||||
|
||||
await visit(urls.globalScope);
|
||||
assert.dom(`[href="${urls.workers}"]`).isVisible();
|
||||
});
|
||||
|
||||
test('Users can navigate to workers with only list permission', async function (assert) {
|
||||
assert.expect(1);
|
||||
instances.scopes.global.authorized_collection_actions.workers = ['list'];
|
||||
|
||||
await visit(urls.globalScope);
|
||||
assert.dom(`[href="${urls.workers}"]`).isVisible();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
|
||||
module('Unit | Route | scopes/scope/workers/worker', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test('it exists', function (assert) {
|
||||
let route = this.owner.lookup('route:scopes/scope/workers/worker');
|
||||
assert.ok(route);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
|
||||
module('Unit | Route | scopes/scope/workers/worker/index', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test('it exists', function (assert) {
|
||||
let route = this.owner.lookup('route:scopes/scope/workers/worker/index');
|
||||
assert.ok(route);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user