feat: 🎸 Added list view for workers (#1234)

* feat: 🎸 Add list view for workers

✅ Closes: ICU-5251
This commit is contained in:
Zhihe Li authored and GitHub committed 2022-08-09 16:12:56 -04:00
1 parent 62077d3c25
commit 4ab7304411
18 files changed
+324 -15

No files matched your search

+18 -1
View File
@@ -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,
+9 -5
View File
@@ -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,
};
}
}
+3 -2
View File
@@ -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 -5
View File
@@ -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(),
+2
View File
@@ -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
+1
View File
@@ -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);
});
});