Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83c2e84647 | ||
|
|
429665a1e0 | ||
|
|
a174c332f3 | ||
|
|
d26b668b38 | ||
|
|
a8bd89fafd | ||
|
|
33256f4c64 | ||
|
|
fc3b574425 | ||
|
|
ba10451293 | ||
|
|
4dee759674 | ||
|
|
02858bbacb | ||
|
|
69ac051786 | ||
|
|
2c998bbb2b | ||
|
|
a79823d3b1 | ||
|
|
30d029a223 | ||
|
|
9ce25b8e83 | ||
|
|
ac5710979a | ||
|
|
af749b57bc | ||
|
|
646125f039 | ||
|
|
53aade5322 | ||
|
|
be20491d2e | ||
|
|
7d3a5227d6 | ||
|
|
8daf6129d1 | ||
|
|
6cec91b09b | ||
|
|
bc0af27afa | ||
|
|
f4cdbfc99c | ||
|
|
aaa205b4e0 | ||
|
|
c8b49beba6 | ||
|
|
6c2694999c | ||
|
|
dfc3989a38 | ||
|
|
d1fdfb2496 | ||
|
|
7b594ca906 | ||
|
|
88bb34828b | ||
|
|
3086cbf4e7 | ||
|
|
d7a4dd40ea | ||
|
|
7238a698a1 | ||
|
|
a7c5332a99 | ||
|
|
e7b1d29d9d | ||
|
|
4f7e5bb732 | ||
|
|
77aaacc8b3 | ||
|
|
ddc7fe939d | ||
|
|
767be30476 | ||
|
|
400b0b5f0b | ||
|
|
b3d0bf5685 | ||
|
|
5cb8826f4e | ||
|
|
fd1b5cfd74 | ||
|
|
6ef055bb15 | ||
|
|
5293a21e03 | ||
|
|
01a90a4d75 | ||
|
|
5dc99ef5ad | ||
|
|
488e724745 | ||
|
|
126b8944c4 | ||
|
|
b6b6eeff53 | ||
|
|
44078a78ad | ||
|
|
39ece5a2c0 | ||
|
|
02fc874ef5 | ||
|
|
5f83fa53cf | ||
|
|
097abcd3eb | ||
|
|
1f4e60ce97 | ||
|
|
e22c53ea27 | ||
|
|
425234148e |
No files matched your search
@@ -47,6 +47,10 @@ export default class BaseModel extends Model {
|
||||
})
|
||||
authorized_collection_actions;
|
||||
|
||||
get scopeModel() {
|
||||
const id = this.scopeID;
|
||||
return this.store.peekRecord('scope', id);
|
||||
}
|
||||
set scopeModel(model) {
|
||||
if (model) {
|
||||
const json = model.serialize();
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
import GeneratedHostCatalogModel from '../generated/models/host-catalog';
|
||||
|
||||
export default class HostCatalogModel extends GeneratedHostCatalogModel {}
|
||||
export default class HostCatalogModel extends GeneratedHostCatalogModel {
|
||||
/**
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isStatic() {
|
||||
return this.type === 'static';
|
||||
}
|
||||
}
|
||||
@@ -8,10 +8,11 @@ import { inject as service } from '@ember/service';
|
||||
* @example
|
||||
* const filter = new ResourceFilter({
|
||||
* type: 'oidc',
|
||||
* status: ['active', 'pending']
|
||||
* status: ['active', 'pending'],
|
||||
* authorized_actions: [{ contains: 'read' }]
|
||||
* });
|
||||
* log(filter.queryExpression);
|
||||
* // ("/item/type" == "oidc") and ("/item/status" == "active" or "/item/status" == "pending")
|
||||
* // ("/item/type" == "oidc") and ("/item/status" == "active" or "/item/status" == "pending") and ("read" in "/item/authorized_actions")
|
||||
*/
|
||||
export class ResourceFilter {
|
||||
// =attributes
|
||||
@@ -30,7 +31,14 @@ export class ResourceFilter {
|
||||
const value = this.#filterObject[key];
|
||||
const valuesArray = Array.isArray(value) ? value : [value];
|
||||
return this.parenthetical(
|
||||
this.or(valuesArray.map((value) => this.equals(key, value)))
|
||||
this.or(
|
||||
valuesArray.map((value) => {
|
||||
if (value && value.contains) {
|
||||
return this.in(value.contains, key);
|
||||
}
|
||||
return this.equals(key, value);
|
||||
})
|
||||
)
|
||||
);
|
||||
})
|
||||
.flat()
|
||||
@@ -56,6 +64,15 @@ export class ResourceFilter {
|
||||
return value !== null ? `"/item/${key}" == "${value}"` : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
* @param value
|
||||
* @return {string}
|
||||
*/
|
||||
in(value, key) {
|
||||
return value !== null ? `"${value}" in "/item/${key}"` : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string[]} clauses
|
||||
* @return {string}
|
||||
|
||||
@@ -26,16 +26,18 @@ export default function () {
|
||||
|
||||
this.get(
|
||||
'/scopes',
|
||||
({ scopes }, { queryParams: { scope_id, recursive } }) => {
|
||||
({ scopes }, { queryParams: { scope_id, recursive, filter } }) => {
|
||||
let resultSet;
|
||||
// Default parent scope is global
|
||||
if (!scope_id) scope_id = 'global';
|
||||
if (recursive && scope_id === 'global') {
|
||||
return scopes.all();
|
||||
resultSet = scopes.all();
|
||||
} else {
|
||||
return scopes.where((scope) => {
|
||||
resultSet = scopes.where((scope) => {
|
||||
return scope.scope ? scope.scope.id === scope_id : false;
|
||||
});
|
||||
}
|
||||
return resultSet.filter(makeBooleanFilter(filter));
|
||||
}
|
||||
);
|
||||
this.post('/scopes', function ({ scopes }) {
|
||||
@@ -347,19 +349,22 @@ export default function () {
|
||||
|
||||
this.get(
|
||||
'/targets',
|
||||
function ({ targets }, { queryParams: { scope_id, recursive } }) {
|
||||
function ({ targets }, { queryParams: { scope_id, recursive, filter } }) {
|
||||
let resultSet;
|
||||
if (recursive && scope_id === 'global') {
|
||||
return targets.all();
|
||||
resultSet = targets.all();
|
||||
} else if (recursive) {
|
||||
return targets.where((target) => {
|
||||
resultSet = targets.where((target) => {
|
||||
const targetModel = targets.find(target.id);
|
||||
return (
|
||||
target.scopeId === scope_id ||
|
||||
targetModel?.scope?.scope?.id === scope_id
|
||||
);
|
||||
});
|
||||
} else {
|
||||
resultSet = targets.where((target) => target.scopeId === scope_id);
|
||||
}
|
||||
return targets.where((target) => target.scopeId === scope_id);
|
||||
return resultSet.filter(makeBooleanFilter(filter));
|
||||
}
|
||||
);
|
||||
this.post('/targets', function ({ targets }) {
|
||||
|
||||
@@ -18,9 +18,9 @@ export default factory.extend({
|
||||
},
|
||||
withChildren: trait({
|
||||
afterCreate(hostCatalog, server) {
|
||||
const { scope } = hostCatalog;
|
||||
const hosts = server.createList('host', 10, { scope, hostCatalog });
|
||||
server.createList('host-set', 3, { scope, hostCatalog, hosts });
|
||||
const { scope, type } = hostCatalog;
|
||||
const hosts = server.createList('host', 10, { scope, hostCatalog, type });
|
||||
server.createList('host-set', 3, { scope, hostCatalog, hosts, type });
|
||||
},
|
||||
}),
|
||||
});
|
||||
@@ -5,7 +5,7 @@ import { random, date, datatype } from 'faker';
|
||||
* GeneratedHostCatalogModelFactory
|
||||
*/
|
||||
export default Factory.extend({
|
||||
type: 'static',
|
||||
type: () => random.arrayElement(['static', 'plugin']),
|
||||
name: () => random.words(),
|
||||
description: () => random.words(),
|
||||
created_time: () => date.recent(),
|
||||
|
||||
@@ -44,7 +44,7 @@ const commandHandlers = {
|
||||
id: 'token123',
|
||||
token: 'thetokenstring',
|
||||
account_id: '1',
|
||||
user_id: 'user123',
|
||||
user_id: 'authenticateduser',
|
||||
auth_method_id: 'authmethod123',
|
||||
created_time: '',
|
||||
updated_time: '',
|
||||
@@ -93,7 +93,7 @@ const commandHandlers = {
|
||||
id: 'token123',
|
||||
token: 'thetokenstring',
|
||||
account_id: '1',
|
||||
user_id: 'user123',
|
||||
user_id: 'authenticateduser',
|
||||
auth_method_id: 'authmethod123',
|
||||
created_time: '',
|
||||
updated_time: '',
|
||||
|
||||
@@ -13,8 +13,8 @@ module('Unit | Model | base', function (hooks) {
|
||||
assert.ok(model);
|
||||
});
|
||||
|
||||
test('it may have a scope fragment', function (assert) {
|
||||
assert.expect(1);
|
||||
test('it may have a scope fragment (and thus a scopeModel)', function (assert) {
|
||||
assert.expect(2);
|
||||
const store = this.owner.lookup('service:store');
|
||||
store.push({
|
||||
data: {
|
||||
@@ -37,6 +37,7 @@ module('Unit | Model | base', function (hooks) {
|
||||
const scope = store.peekRecord('scope', 'o_1');
|
||||
const model = store.peekRecord('user', '123abc');
|
||||
assert.equal(model.scope.scope_id, scope.id);
|
||||
assert.equal(model.scopeModel.id, scope.id);
|
||||
});
|
||||
|
||||
test('it may accept a `scopeModel` for convenience, instead of a fragment', function (assert) {
|
||||
|
||||
@@ -9,4 +9,14 @@ module('Unit | Model | host catalog', function (hooks) {
|
||||
let model = store.createRecord('host-catalog', {});
|
||||
assert.ok(model);
|
||||
});
|
||||
|
||||
test('it has isStatic function and returns the expected values', async function (assert) {
|
||||
assert.expect(3);
|
||||
const store = this.owner.lookup('service:store');
|
||||
const modelA = store.createRecord('host-catalog', { type: 'static' });
|
||||
const modelB = store.createRecord('host-catalog', { type: 'plugin' });
|
||||
assert.equal(typeof modelA.isStatic, 'boolean');
|
||||
assert.true(modelA.isStatic);
|
||||
assert.false(modelB.isStatic);
|
||||
});
|
||||
});
|
||||
@@ -5,12 +5,14 @@ module('Unit | Model | host', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test('it exists', function (assert) {
|
||||
assert.expect(1);
|
||||
let store = this.owner.lookup('service:store');
|
||||
let model = store.createRecord('host', {});
|
||||
assert.ok(model);
|
||||
});
|
||||
|
||||
test('it contains attributes', function (assert) {
|
||||
assert.expect(1);
|
||||
let store = this.owner.lookup('service:store');
|
||||
let model = store.createRecord('host', {
|
||||
type: 'static',
|
||||
|
||||
@@ -10,9 +10,10 @@ module('Unit | Service | resource-filter-store', function (hooks) {
|
||||
const filter = new ResourceFilter({
|
||||
type: 'oidc',
|
||||
status: ['active', 'pending'],
|
||||
authorized_actions: [{ contains: 'read' }],
|
||||
});
|
||||
const expected =
|
||||
'("/item/type" == "oidc") and ("/item/status" == "active" or "/item/status" == "pending")';
|
||||
'("/item/type" == "oidc") and ("/item/status" == "active" or "/item/status" == "pending") and ("read" in "/item/authorized_actions")';
|
||||
assert.equal(filter.queryExpression, expected);
|
||||
});
|
||||
});
|
||||
@@ -1,92 +0,0 @@
|
||||
/**
|
||||
* A resource filter param is a concise way to express a route query parameter.
|
||||
* Resource filter params abstract away a route's query parameter
|
||||
* interface, treating a query param as a simple attribute on a route instance.
|
||||
* Getting the attribute value is equivalent to looking up the associated query
|
||||
* parameter. Setting the attribute is equivalent to transitioning to the route
|
||||
* and passing the associated query parameter value.
|
||||
*
|
||||
* Additionally, resource filter params may declare `allowedValues`, an array
|
||||
* containing the set of values the parameter may take. While unenforced,
|
||||
* `allowedValues` are useful metadata about a resource filter and may be fetch
|
||||
* via `ResourceFilterParamHelper`. An optional default value may be specified
|
||||
* via `defaultValue` passed to the decorator.
|
||||
*
|
||||
* @see ResourceFilterParamHelper
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* export default class MyRoute extends Route {
|
||||
*
|
||||
* @resourceFilterParam(['active', 'pending'], ['active'])
|
||||
* status;
|
||||
*
|
||||
* @action
|
||||
* changeStatus(status) {
|
||||
* // Sets query param `filter-status` on the current route, which occurs
|
||||
* // via a route transition.
|
||||
* this.status = status;
|
||||
* // Log the value of query param `filter-status`.
|
||||
* log(this.status);
|
||||
* }
|
||||
* }
|
||||
*
|
||||
*/
|
||||
export function resourceFilterParam(allowedValues, defaultValue) {
|
||||
/**
|
||||
* @param {object} target
|
||||
* @param {string} name
|
||||
* @param {object} descriptor
|
||||
* @return {object{get, set}}
|
||||
*/
|
||||
return function (target, name /*, descriptor*/) {
|
||||
const filterKey = `filter-${name}`;
|
||||
const filterAllowedValuesKey = `filter-allowed-values-${name}`;
|
||||
|
||||
// If the route has no query params specified yet, declare them.
|
||||
if (!Object.keys(target.queryParams).length) target.queryParams = {};
|
||||
|
||||
// If the route has no resource filters list specified yet, create one.
|
||||
if (!target.resourceFilterParams) target.resourceFilterParams = [];
|
||||
|
||||
// Add the resource filter query parameter to the route. Resource filter
|
||||
// param changes refresh the model but do not contribute to browser history.
|
||||
// See https://guides.emberjs.com/release/routing/query-params/
|
||||
target.queryParams[filterKey] = {
|
||||
refreshModel: true,
|
||||
replace: true,
|
||||
};
|
||||
|
||||
// Note the name of this filter param.
|
||||
target.resourceFilterParams.push(name);
|
||||
|
||||
// Store the allowed values for future lookup
|
||||
// (via `ResourceFilterParamHelper`).
|
||||
target[filterAllowedValuesKey] = allowedValues;
|
||||
|
||||
// Override the decorated attribute with a getter and setter.
|
||||
return {
|
||||
/**
|
||||
* Returns the JSON-parsed query parameter value OR defaultValue.
|
||||
*/
|
||||
get() {
|
||||
const value =
|
||||
this.paramsFor(this.routeName)[filterKey] ||
|
||||
JSON.stringify(defaultValue);
|
||||
return value ? JSON.parse(value) : null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the resource filter param to the stringified representation
|
||||
* of `value`. NOTE: this is equivalent to transitioning into the
|
||||
* current route, changing the associated query param.
|
||||
* @param value
|
||||
*/
|
||||
set(value) {
|
||||
const queryParams = {};
|
||||
queryParams[filterKey] = JSON.stringify(value);
|
||||
this.transitionTo({ queryParams });
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
import EmberObject from '@ember/object';
|
||||
import { getOwner } from '@ember/application';
|
||||
import { typeOf } from '@ember/utils';
|
||||
|
||||
/**
|
||||
* @param value
|
||||
* @return {boolean} true if `value` is a function
|
||||
*/
|
||||
const isFunction = (value) => typeOf(value) === 'function';
|
||||
|
||||
/**
|
||||
* Represents a filter on a specific route and maintains filter state.
|
||||
* @class
|
||||
*/
|
||||
class RouteResourceFilter extends EmberObject {
|
||||
// =attributes
|
||||
|
||||
route;
|
||||
name;
|
||||
defaultValue;
|
||||
allowed;
|
||||
serialize;
|
||||
findBySerialized;
|
||||
|
||||
// =computed
|
||||
|
||||
/**
|
||||
* Query parameter name where the selected value(s) for this filter are
|
||||
* stored in the URL.
|
||||
* @type {string}
|
||||
*/
|
||||
get filterKey() {
|
||||
return `filter-${this.name}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* The allowed values for this filter. See `allowed`.
|
||||
* @type {?array}
|
||||
*/
|
||||
get allowedValues() {
|
||||
if (isFunction(this.allowed)) {
|
||||
return this.allowed(this.route);
|
||||
}
|
||||
return this.allowed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Currently selected value(s) for this filter, deserialized from the value of
|
||||
* the associated route query parameter.
|
||||
* @type {?array}
|
||||
*/
|
||||
get value() {
|
||||
const decodedValue =
|
||||
this.route.paramsFor(this.route.routeName)[this.filterKey] ||
|
||||
JSON.stringify(this.defaultValue);
|
||||
const value = decodedValue ? JSON.parse(decodedValue) : null;
|
||||
const deserializedValue = value
|
||||
? value.map((serializedValue) =>
|
||||
this.allowedValues.find((item) =>
|
||||
this.deserializeValue(item, serializedValue)
|
||||
)
|
||||
)
|
||||
: null;
|
||||
return deserializedValue;
|
||||
}
|
||||
set value(value) {
|
||||
const queryParams = {};
|
||||
const serialized = value.map((value) => this.serializeValue(value));
|
||||
queryParams[this.filterKey] = JSON.stringify(serialized);
|
||||
this.route.transitionTo({ queryParams });
|
||||
}
|
||||
|
||||
// =methods
|
||||
|
||||
/**
|
||||
* Calls `serialize` (if set) on the passed value. Otherwise returns the
|
||||
* value unaltered.
|
||||
*/
|
||||
serializeValue(value) {
|
||||
if (this.serialize) {
|
||||
return this.serialize(...arguments);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if `item` is represented by `serializedValue` when deserializing
|
||||
* items from the query string. If `findBySerialized` is unspecified,
|
||||
* a simple equality test is used.
|
||||
* @param item
|
||||
* @param {string} serializedValue
|
||||
* @return {boolean} item is represented by `serializedValue`
|
||||
*/
|
||||
deserializeValue(item, serializedValue) {
|
||||
if (this.findBySerialized) {
|
||||
return this.findBySerialized(...arguments);
|
||||
}
|
||||
return item === serializedValue;
|
||||
}
|
||||
|
||||
// =static methods
|
||||
|
||||
/**
|
||||
* Configures the specified route with a query parameter of `name` that, when
|
||||
* changed, refreshes the route model and replaces history (rather than
|
||||
* advancing history).
|
||||
* @static
|
||||
* @param {Route} routeClass
|
||||
* @param {string} name
|
||||
*/
|
||||
static setupRouteQueryParams(routeClass, name) {
|
||||
const filterKey = `filter-${name}`;
|
||||
|
||||
// If the route has no query params specified yet, declare them.
|
||||
if (!Object.keys(routeClass.queryParams).length)
|
||||
routeClass.queryParams = {};
|
||||
|
||||
// If the route has no resource filters list specified yet, create one.
|
||||
if (!routeClass.resourceFilterParams) routeClass.resourceFilterParams = [];
|
||||
|
||||
// Add the resource filter query parameter to the route. Resource filter
|
||||
// param changes refresh the model but do not contribute to browser history.
|
||||
// See https://guides.emberjs.com/release/routing/query-params/
|
||||
routeClass.queryParams[filterKey] = {
|
||||
refreshModel: true,
|
||||
replace: true,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method to instantiate an instance of this class.
|
||||
* @static
|
||||
* @param {Route} routeInstance
|
||||
* @param {string} name
|
||||
* @param {array|function} allowed
|
||||
* @param {?array} defaultValue
|
||||
* @param {?function} serialize
|
||||
* @param {?function} findBySerialized
|
||||
* @return {RouteResourceFilter}
|
||||
*/
|
||||
static getOrCreateRouteResourceFilter(
|
||||
routeInstance,
|
||||
name,
|
||||
allowed,
|
||||
defaultValue,
|
||||
serialize,
|
||||
findBySerialized
|
||||
) {
|
||||
const owner = getOwner(routeInstance);
|
||||
const containerKey = `resource-filter:${name}@${routeInstance.routeName}`;
|
||||
const factory = owner.factoryFor(containerKey);
|
||||
if (!factory) {
|
||||
// create factory
|
||||
class AppliedRouteResourceFilter extends this {
|
||||
route = routeInstance;
|
||||
name = name;
|
||||
allowed = allowed;
|
||||
defaultValue = defaultValue;
|
||||
serialize = serialize;
|
||||
findBySerialized = findBySerialized;
|
||||
}
|
||||
owner.register(containerKey, AppliedRouteResourceFilter);
|
||||
}
|
||||
const instance = owner.lookup(containerKey);
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This decorator provides a concise way to express a route resource filter.
|
||||
* Resource filter params abstract away a route's query parameter
|
||||
* interface, treating a query param as just an attribute on a route instance.
|
||||
* Getting the attribute value is equivalent to looking up the associated query
|
||||
* parameter. Setting the attribute is equivalent to transitioning to the route
|
||||
* and passing the associated query parameter value.
|
||||
*
|
||||
* Additionally, resource filter params may declare `allowed`, an array
|
||||
* (or function returning an array) containing the set of values the parameter
|
||||
* may take. While unenforced, `allowed` are useful metadata about a resource
|
||||
* filter and may be fetch via `ResourceFilterParamHelper`. An optional default
|
||||
* value may be specified via `defaultValue` passed to the decorator.
|
||||
*
|
||||
* @see ResourceFilterParamHelper
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* export default class MyRoute extends Route {
|
||||
*
|
||||
* @resourceFilter({
|
||||
* allowed: ['active', 'pending'],
|
||||
* defaultvalue: ['active'], // optional
|
||||
* })
|
||||
* status;
|
||||
*
|
||||
* @action
|
||||
* changeStatus(status) {
|
||||
* // Sets query param `filter-status` on the current route, which occurs
|
||||
* // via a route transition.
|
||||
* this.status = status;
|
||||
* // Log the value of query param `filter-status`.
|
||||
* log(this.status);
|
||||
* }
|
||||
* }
|
||||
*
|
||||
*/
|
||||
export function resourceFilter({
|
||||
allowed,
|
||||
defaultValue,
|
||||
serialize,
|
||||
findBySerialized,
|
||||
}) {
|
||||
/**
|
||||
* @param {object} target
|
||||
* @param {string} name
|
||||
* @param {object} descriptor
|
||||
* @return {object{get, set}}
|
||||
*/
|
||||
return function (target, name /*, descriptor*/) {
|
||||
RouteResourceFilter.setupRouteQueryParams(target, name);
|
||||
|
||||
let instance;
|
||||
|
||||
// Override the decorated attribute with a getter and setter.
|
||||
return {
|
||||
/**
|
||||
* Returns the current value of the resource filter.
|
||||
*/
|
||||
get() {
|
||||
instance = RouteResourceFilter.getOrCreateRouteResourceFilter(
|
||||
this,
|
||||
name,
|
||||
allowed,
|
||||
defaultValue,
|
||||
serialize,
|
||||
findBySerialized
|
||||
);
|
||||
return instance.value;
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the resource filter param to the stringified representation
|
||||
* of `value`. NOTE: this is equivalent to transitioning into the
|
||||
* current route, changing the associated query param.
|
||||
* @param value
|
||||
*/
|
||||
set(value) {
|
||||
instance.value = value;
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
||||
+5
-5
@@ -11,11 +11,11 @@ import { action } from '@ember/object';
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* {{#if (has-resource-filter-param-selections 'route.name' 'field')}}
|
||||
* {{#if (has-resource-filter-selections 'route.name' 'field')}}
|
||||
* Field has filter selections.
|
||||
* {{/if}}
|
||||
*/
|
||||
export default class HasResourceFilterParamSelectionsHelper extends Helper {
|
||||
export default class HasRouteResourceFilterSelectionsHelper extends Helper {
|
||||
// =services
|
||||
|
||||
@service router;
|
||||
@@ -44,14 +44,14 @@ export default class HasResourceFilterParamSelectionsHelper extends Helper {
|
||||
|
||||
/**
|
||||
* Returns true if the specified route has a value set for
|
||||
* any resource filter.
|
||||
* any resource filter passed into the helper.
|
||||
* @param {string} routeName
|
||||
* @param {string[]} names - names of resource filter fields
|
||||
* @return {boolean}
|
||||
*/
|
||||
compute([routeName]) {
|
||||
compute([routeName, ...names]) {
|
||||
const owner = getOwner(this);
|
||||
const route = owner.lookup(`route:${routeName}`);
|
||||
const names = route.resourceFilterParams;
|
||||
const selectedValues = names.map((name) => route[name] || null).flat();
|
||||
const anyTruthy = selectedValues.reduce(
|
||||
(previousValue, currentValue) => previousValue || currentValue,
|
||||
+6
-10
@@ -7,15 +7,15 @@ import { action } from '@ember/object';
|
||||
* Use this helper to fetch the current value of a resource filter param,
|
||||
* as well as its allowedValues.
|
||||
*
|
||||
* @see resourceFilterParam (decorator)
|
||||
* @see resourceFilter (decorator)
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* {{#with resource-filter-param 'status' as |param|}}
|
||||
* {{#with resource-filter 'status' as |param|}}
|
||||
* {{param.name}} / {{param.value}} / {{param.selectedValue}}
|
||||
* {{/with}}
|
||||
*/
|
||||
export default class ResourceFilterParamHelper extends Helper {
|
||||
export default class ResourceFilterHelper extends Helper {
|
||||
// =services
|
||||
|
||||
@service router;
|
||||
@@ -50,13 +50,9 @@ export default class ResourceFilterParamHelper extends Helper {
|
||||
*/
|
||||
compute([routeName, name]) {
|
||||
const owner = getOwner(this);
|
||||
// Filter options
|
||||
const route = owner.lookup(`route:${routeName}`);
|
||||
const filterAllowedValuesKey = `filter-allowed-values-${name}`;
|
||||
const allowedValues = route[filterAllowedValuesKey];
|
||||
// Selected filters
|
||||
const rawValue = route[name];
|
||||
const selectedValue = rawValue || null;
|
||||
const containerKey = `resource-filter:${name}@${routeName}`;
|
||||
const resourceFilter = owner.lookup(containerKey);
|
||||
const { allowedValues, value: selectedValue } = resourceFilter;
|
||||
return { name, allowedValues, selectedValue };
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from 'core/decorators/resource-filter-param';
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'core/decorators/resource-filter';
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from 'core/helpers/has-resource-filter-param-selections';
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'core/helpers/has-resource-filter-selections';
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from 'core/helpers/resource-filter-param';
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'core/helpers/resource-filter';
|
||||
+3
-1
@@ -82,7 +82,9 @@
|
||||
"tmpl": "^1.0.5",
|
||||
"set-value": "^4.0.1",
|
||||
"ansi-regex": "^5.0.1",
|
||||
"ember-cli-babel": "^7.26.6"
|
||||
"ember-cli-babel": "^7.26.6",
|
||||
"jsonpointer": "5.0.0",
|
||||
"json-schema": "^0.4.0"
|
||||
},
|
||||
"config": {
|
||||
"commitizen": {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { action } from '@ember/object';
|
||||
import loading from 'ember-loading/decorator';
|
||||
import { confirm } from 'core/decorators/confirm';
|
||||
import { notifySuccess, notifyError } from 'core/decorators/notify';
|
||||
import { resourceFilterParam } from 'core/decorators/resource-filter-param';
|
||||
import { resourceFilter } from 'core/decorators/resource-filter';
|
||||
|
||||
export default class ScopesScopeAuthMethodsRoute extends Route {
|
||||
// =services
|
||||
@@ -17,7 +17,7 @@ export default class ScopesScopeAuthMethodsRoute extends Route {
|
||||
|
||||
// =attributes
|
||||
|
||||
@resourceFilterParam(['password', 'oidc']) type;
|
||||
@resourceFilter({ allowed: ['password', 'oidc'] }) type;
|
||||
|
||||
// =methods
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { task, timeout } from 'ember-concurrency';
|
||||
import { A } from '@ember/array';
|
||||
import { notifySuccess, notifyError } from 'core/decorators/notify';
|
||||
import config from '../../../config/environment';
|
||||
import { resourceFilterParam } from 'core/decorators/resource-filter-param';
|
||||
import { resourceFilter } from 'core/decorators/resource-filter';
|
||||
|
||||
const POLL_TIMEOUT_SECONDS = config.sessionPollingTimeoutSeconds;
|
||||
|
||||
@@ -21,7 +21,10 @@ export default class ScopesScopeSessionsRoute extends Route {
|
||||
|
||||
// =attributes
|
||||
|
||||
@resourceFilterParam(['active', 'pending', 'canceling', 'terminated'], ['active','pending', 'canceling']) status;
|
||||
@resourceFilter({
|
||||
allowed: ['active', 'pending', 'canceling', 'terminated'],
|
||||
defaultValue: ['active','pending', 'canceling']
|
||||
}) status;
|
||||
|
||||
/**
|
||||
* A simple Ember Concurrency-based polling task that refreshes the route
|
||||
@@ -128,7 +131,7 @@ export default class ScopesScopeSessionsRoute extends Route {
|
||||
filterBy(field, value) {
|
||||
this[field] = value;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Clears and filter selections.
|
||||
*/
|
||||
|
||||
@@ -39,13 +39,13 @@
|
||||
{{#if
|
||||
(or
|
||||
@model
|
||||
(has-resource-filter-param-selections 'scopes.scope.auth-methods')
|
||||
(has-resource-filter-selections 'scopes.scope.auth-methods' 'type')
|
||||
)
|
||||
}}
|
||||
<Rose::Toolbar>
|
||||
<Rose::Form as |form|>
|
||||
{{#with
|
||||
(resource-filter-param 'scopes.scope.auth-methods' 'type')
|
||||
(resource-filter 'scopes.scope.auth-methods' 'type')
|
||||
as |filter|
|
||||
}}
|
||||
<Rose::Dropdown
|
||||
@@ -74,7 +74,7 @@
|
||||
{{/with}}
|
||||
|
||||
{{#if
|
||||
(has-resource-filter-param-selections 'scopes.scope.auth-methods')
|
||||
(has-resource-filter-selections 'scopes.scope.auth-methods' 'type')
|
||||
}}
|
||||
<Rose::Button
|
||||
@style='inline-link-action'
|
||||
@@ -207,8 +207,8 @@
|
||||
</table.body>
|
||||
</Rose::Table>
|
||||
|
||||
{{else if (has-resource-filter-param-selections
|
||||
'scopes.scope.auth-methods'
|
||||
{{else if (has-resource-filter-selections
|
||||
'scopes.scope.auth-methods' 'type'
|
||||
)}}
|
||||
|
||||
<Rose::Layout::Centered>
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
@route='scopes.scope.host-catalogs.host-catalog.hosts.host'
|
||||
@model={{host.id}}
|
||||
>
|
||||
|
||||
{{host.displayName}}
|
||||
</LinkTo>
|
||||
{{else}}
|
||||
|
||||
@@ -41,37 +41,39 @@
|
||||
</table.header>
|
||||
<table.body as |body|>
|
||||
{{#each @model as |hostCatalog|}}
|
||||
<body.row as |row|>
|
||||
<row.headerCell>
|
||||
{{#if (can 'read model' hostCatalog)}}
|
||||
<LinkTo
|
||||
@route='scopes.scope.host-catalogs.host-catalog'
|
||||
@model={{hostCatalog.id}}
|
||||
>
|
||||
{{#if hostCatalog.isStatic}}
|
||||
<body.row as |row|>
|
||||
<row.headerCell>
|
||||
{{#if (can 'read model' hostCatalog)}}
|
||||
<LinkTo
|
||||
@route='scopes.scope.host-catalogs.host-catalog'
|
||||
@model={{hostCatalog.id}}
|
||||
>
|
||||
{{hostCatalog.displayName}}
|
||||
</LinkTo>
|
||||
{{else}}
|
||||
{{hostCatalog.displayName}}
|
||||
</LinkTo>
|
||||
{{else}}
|
||||
{{hostCatalog.displayName}}
|
||||
{{/if}}
|
||||
<p>{{hostCatalog.description}}</p>
|
||||
</row.headerCell>
|
||||
<row.cell>
|
||||
<Rose::Badge>
|
||||
{{t
|
||||
(concat 'resources.host-catalog.types.' hostCatalog.type)
|
||||
}}
|
||||
</Rose::Badge>
|
||||
</row.cell>
|
||||
<row.cell>
|
||||
<Copyable
|
||||
@text={{hostCatalog.id}}
|
||||
@buttonText={{t 'actions.copy-to-clipboard'}}
|
||||
@acknowledgeText={{t 'states.copied'}}
|
||||
>
|
||||
<code>{{hostCatalog.id}}</code>
|
||||
</Copyable>
|
||||
</row.cell>
|
||||
</body.row>
|
||||
{{/if}}
|
||||
<p>{{hostCatalog.description}}</p>
|
||||
</row.headerCell>
|
||||
<row.cell>
|
||||
<Rose::Badge>
|
||||
{{t
|
||||
(concat 'resources.host-catalog.types.' hostCatalog.type)
|
||||
}}
|
||||
</Rose::Badge>
|
||||
</row.cell>
|
||||
<row.cell>
|
||||
<Copyable
|
||||
@text={{hostCatalog.id}}
|
||||
@buttonText={{t 'actions.copy-to-clipboard'}}
|
||||
@acknowledgeText={{t 'states.copied'}}
|
||||
>
|
||||
<code>{{hostCatalog.id}}</code>
|
||||
</Copyable>
|
||||
</row.cell>
|
||||
</body.row>
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
</table.body>
|
||||
</Rose::Table>
|
||||
|
||||
@@ -16,13 +16,13 @@
|
||||
{{#if (feature-flag 'filter')}}
|
||||
{{#if
|
||||
(or
|
||||
@model (has-resource-filter-param-selections 'scopes.scope.sessions')
|
||||
@model (has-resource-filter-selections 'scopes.scope.sessions' 'type')
|
||||
)
|
||||
}}
|
||||
<Rose::Toolbar>
|
||||
<Rose::Form as |form|>
|
||||
{{#with
|
||||
(resource-filter-param 'scopes.scope.sessions' 'status')
|
||||
(resource-filter 'scopes.scope.sessions' 'status')
|
||||
as |filter|
|
||||
}}
|
||||
<Rose::Dropdown
|
||||
@@ -51,7 +51,7 @@
|
||||
{{/with}}
|
||||
|
||||
{{#if
|
||||
(has-resource-filter-param-selections 'scopes.scope.sessions')
|
||||
(has-resource-filter-selections 'scopes.scope.sessions' 'type')
|
||||
}}
|
||||
<Rose::Button
|
||||
@style='inline-link-action'
|
||||
@@ -136,7 +136,7 @@
|
||||
</table.body>
|
||||
</Rose::Table>
|
||||
|
||||
{{else if (has-resource-filter-param-selections 'scopes.scope.sessions')}}
|
||||
{{else if (has-resource-filter-selections 'scopes.scope.sessions' 'type')}}
|
||||
<Rose::Layout::Centered>
|
||||
<Rose::Message @title={{t 'titles.no-results'}} as |message|>
|
||||
<message.description>
|
||||
|
||||
@@ -2,6 +2,13 @@
|
||||
|
||||
Canonical reference for changes, improvements, and bugfixes for Boundary Desktop.
|
||||
|
||||
## v1.4.1 [CLI 0.7.3] (2021.12.16)
|
||||
|
||||
### New
|
||||
|
||||
- Add sessions filtering by project. ([PR](https://github.com/hashicorp/boundary-ui/pull/880))
|
||||
- Add targets filtering by project. ([PR](https://github.com/hashicorp/boundary-ui/pull/883))
|
||||
|
||||
## v1.4.0 [CLI 0.7.1] (2021.11.18)
|
||||
|
||||
### New
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { helper } from '@ember/component/helper';
|
||||
|
||||
const groupByReducer = (items, key) => {
|
||||
const obj = items.reduce((result, item) => {
|
||||
if (!result[item[key]]) result[item[key]] = { key: item[key], items: [] };
|
||||
result[item[key]].items.push(item);
|
||||
return result;
|
||||
}, {});
|
||||
// Convert object group to an array for iteration
|
||||
const arr = Object.keys(obj).map((key) => obj[key]);
|
||||
return arr;
|
||||
};
|
||||
|
||||
/**
|
||||
* This helper accepts an array of objects and groups them by a specified key.
|
||||
* It returns a new array of objects in the form [{ key, items }], where `key`
|
||||
* is the value of the specified key and `items` is an array of matching items.
|
||||
* @param {object[]} items
|
||||
* @param {string} key
|
||||
*/
|
||||
export default helper(function groupBy([items, key]) {
|
||||
const result = groupByReducer(items, key);
|
||||
return result;
|
||||
});
|
||||
@@ -19,6 +19,7 @@ export default class ScopesScopeProjectsRoute extends Route {
|
||||
// =services
|
||||
|
||||
@service session;
|
||||
@service resourceFilterStore;
|
||||
|
||||
// =methods
|
||||
|
||||
@@ -34,8 +35,12 @@ export default class ScopesScopeProjectsRoute extends Route {
|
||||
* @return {Promise{ScopeModel}}
|
||||
*/
|
||||
model() {
|
||||
return this.store
|
||||
.query('scope', { recursive: true, scope_id: 'global' })
|
||||
.filter(({ isProject }) => isProject);
|
||||
const { id: scope_id } = this.modelFor('scopes.scope');
|
||||
const projects = this.resourceFilterStore.queryBy(
|
||||
'scope',
|
||||
{ type: 'project' },
|
||||
{ recursive: true, scope_id }
|
||||
);
|
||||
return projects;
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { task, timeout } from 'ember-concurrency';
|
||||
import config from '../../../../config/environment';
|
||||
import { action } from '@ember/object';
|
||||
import { notifySuccess, notifyError } from 'core/decorators/notify';
|
||||
import { resourceFilterParam } from 'core/decorators/resource-filter-param';
|
||||
import { resourceFilter } from 'core/decorators/resource-filter';
|
||||
|
||||
const POLL_TIMEOUT_SECONDS = config.sessionPollingTimeoutSeconds;
|
||||
|
||||
@@ -18,7 +18,16 @@ export default class ScopesScopeProjectsSessionsRoute extends Route {
|
||||
|
||||
// =attributes
|
||||
|
||||
@resourceFilterParam(['active', 'pending', 'canceling', 'terminated'], ['active','pending', 'canceling']) status;
|
||||
@resourceFilter({
|
||||
allowed: ['active', 'pending', 'canceling', 'terminated'],
|
||||
defaultValue: ['active','pending', 'canceling']
|
||||
}) status;
|
||||
|
||||
@resourceFilter({
|
||||
allowed: (route) => route.modelFor('scopes.scope.projects'),
|
||||
serialize: ({ id }) => id,
|
||||
findBySerialized: ({ id }, value) => id === value
|
||||
}) project;
|
||||
|
||||
/**
|
||||
* A simple Ember Concurrency-based polling task that refreshes the route
|
||||
@@ -50,14 +59,19 @@ export default class ScopesScopeProjectsSessionsRoute extends Route {
|
||||
/**
|
||||
* Loads all sessions under the current scope and encapsulates them into
|
||||
* an array of objects filtering to current user
|
||||
* @return {Promise{[{session: SessionModel, target: TargetModel}]}}
|
||||
* @return {Promise{SessionModel[]}}
|
||||
*/
|
||||
async model() {
|
||||
const { status } = this;
|
||||
const { id: scope_id } = this.modelFor('scopes.scope');
|
||||
const { user_id } = this.session.data.authenticated;
|
||||
const projects = this.project || [];
|
||||
await this.store.query('target', { recursive: true, scope_id });
|
||||
return await this.resourceFilterStore.queryBy('session', { user_id , status }, {
|
||||
return await this.resourceFilterStore.queryBy('session', {
|
||||
user_id,
|
||||
status,
|
||||
scope_id: projects.map(({ id }) => id)
|
||||
}, {
|
||||
recursive: true,
|
||||
scope_id
|
||||
});
|
||||
@@ -91,21 +105,22 @@ export default class ScopesScopeProjectsSessionsRoute extends Route {
|
||||
await this.ipc.invoke('stop', { session_id: session.id });
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the specified resource filter field to the specified value.
|
||||
* @param {string} field
|
||||
* @param value
|
||||
*/
|
||||
@action
|
||||
filterBy(field, value) {
|
||||
this[field] = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears and filter selections.
|
||||
*/
|
||||
@action
|
||||
clearAllFilters() {
|
||||
this.status = [];
|
||||
}
|
||||
/**
|
||||
* Sets the specified resource filter field to the specified value.
|
||||
* @param {string} field
|
||||
* @param value
|
||||
*/
|
||||
@action
|
||||
filterBy(field, value) {
|
||||
this[field] = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears and filter selections.
|
||||
*/
|
||||
@action
|
||||
clearAllFilters() {
|
||||
this.status = [];
|
||||
this.project = [];
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import { task, timeout } from 'ember-concurrency';
|
||||
import config from '../../../../config/environment';
|
||||
import { resourceFilter } from 'core/decorators/resource-filter';
|
||||
|
||||
const POLL_TIMEOUT_SECONDS = config.sessionPollingTimeoutSeconds;
|
||||
|
||||
@@ -15,9 +16,16 @@ export default class ScopesScopeProjectsTargetsRoute extends Route {
|
||||
@service origin;
|
||||
@service notify;
|
||||
@service confirm;
|
||||
@service resourceFilterStore;
|
||||
|
||||
// =attributes
|
||||
|
||||
@resourceFilter({
|
||||
allowed: (route) => route.modelFor('scopes.scope.projects'),
|
||||
serialize: ({ id }) => id,
|
||||
findBySerialized: ({ id }, value) => id === value
|
||||
}) project;
|
||||
|
||||
/**
|
||||
* A simple Ember Concurrency-based polling task that refreshes the route
|
||||
* every POLL_TIMEOUT_SECONDS seconds. This is necessary to display changes
|
||||
@@ -52,13 +60,17 @@ export default class ScopesScopeProjectsTargetsRoute extends Route {
|
||||
async model() {
|
||||
const { id: scope_id } = this.modelFor('scopes.scope');
|
||||
const { user_id } = this.session.data.authenticated;
|
||||
await this.store.query('session', {
|
||||
filter: `"/item/user_id" == "${user_id}"`,
|
||||
const projects = this.project || [];
|
||||
await this.resourceFilterStore.queryBy('session', { user_id }, {
|
||||
recursive: true,
|
||||
scope_id
|
||||
});
|
||||
return this.store.query('target', {
|
||||
filter: '"authorize-session" in "/item/authorized_actions"',
|
||||
return this.resourceFilterStore.queryBy('target', {
|
||||
scope_id: projects.map(({ id }) => id),
|
||||
authorized_actions: [
|
||||
{ contains: 'authorize-session' },
|
||||
]
|
||||
}, {
|
||||
recursive: true,
|
||||
scope_id
|
||||
});
|
||||
@@ -129,4 +141,22 @@ export default class ScopesScopeProjectsTargetsRoute extends Route {
|
||||
.catch(() => null /* no op */);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the specified resource filter field to the specified value.
|
||||
* @param {string} field
|
||||
* @param value
|
||||
*/
|
||||
@action
|
||||
filterBy(field, value) {
|
||||
this[field] = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears and filter selections.
|
||||
*/
|
||||
@action
|
||||
clearAllFilters() {
|
||||
this.project = [];
|
||||
}
|
||||
}
|
||||
@@ -95,15 +95,22 @@
|
||||
}
|
||||
}
|
||||
|
||||
.rose-icon {
|
||||
margin-right: sizing.rems(xxs);
|
||||
margin-left: sizing.rems(xxs);
|
||||
}
|
||||
|
||||
.indent-label-1 {
|
||||
.rose-form-radio-label {
|
||||
.rose-form-radio-label,
|
||||
.rose-form-checkbox-label {
|
||||
padding-left: $indent;
|
||||
}
|
||||
}
|
||||
|
||||
.indent-label-2 {
|
||||
.rose-form-radio-label {
|
||||
padding-left: $indent + $icon-width;
|
||||
.rose-form-radio-label,
|
||||
.rose-form-checkbox-label {
|
||||
padding-left: $indent * 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,15 +13,15 @@
|
||||
{{#if
|
||||
(or
|
||||
@model
|
||||
(has-resource-filter-param-selections
|
||||
'scopes.scope.projects.sessions'
|
||||
(has-resource-filter-selections
|
||||
'scopes.scope.projects.sessions' 'status' 'project'
|
||||
)
|
||||
)
|
||||
}}
|
||||
<Rose::Toolbar>
|
||||
<Rose::Form as |form|>
|
||||
{{#with
|
||||
(resource-filter-param 'scopes.scope.projects.sessions' 'status')
|
||||
(resource-filter 'scopes.scope.projects.sessions' 'status')
|
||||
as |filter|
|
||||
}}
|
||||
<Rose::Dropdown
|
||||
@@ -49,9 +49,60 @@
|
||||
</Rose::Dropdown>
|
||||
{{/with}}
|
||||
|
||||
{{#with
|
||||
(resource-filter 'scopes.scope.projects.sessions' 'project')
|
||||
as |filter|
|
||||
}}
|
||||
<Rose::Dropdown
|
||||
@text={{t (concat 'resources.' filter.name '.title')}}
|
||||
@count={{filter.selectedValue.length}}
|
||||
@ignoreClickInside={{true}}
|
||||
as |dropdown|
|
||||
>
|
||||
<ul>
|
||||
{{#with
|
||||
(group-by filter.allowedValues 'scopeModel')
|
||||
as |groups|
|
||||
}}
|
||||
{{#each groups as |groupedItems|}}
|
||||
<li>
|
||||
<dropdown.item>
|
||||
<Rose::Icon
|
||||
@name='flight-icons/svg/org-16'
|
||||
@size='medium'
|
||||
/>
|
||||
{{groupedItems.key.displayName}}
|
||||
</dropdown.item>
|
||||
<ul class='indent-label-2'>
|
||||
<form.checkboxGroup
|
||||
@name={{filter.name}}
|
||||
@items={{groupedItems.items}}
|
||||
@selectedItems={{filter.selectedValue}}
|
||||
@onChange={{route-action 'filterBy' filter.name}}
|
||||
as |group|
|
||||
>
|
||||
<li>
|
||||
<dropdown.item>
|
||||
<group.checkbox
|
||||
@label={{group.item.displayName}}
|
||||
value={{group.item}}
|
||||
/>
|
||||
</dropdown.item>
|
||||
</li>
|
||||
</form.checkboxGroup>
|
||||
</ul>
|
||||
</li>
|
||||
{{/each}}
|
||||
{{/with}}
|
||||
</ul>
|
||||
|
||||
{{! }}
|
||||
</Rose::Dropdown>
|
||||
{{/with}}
|
||||
|
||||
{{#if
|
||||
(has-resource-filter-param-selections
|
||||
'scopes.scope.projects.sessions'
|
||||
(has-resource-filter-selections
|
||||
'scopes.scope.projects.sessions' 'status' 'project'
|
||||
)
|
||||
}}
|
||||
<Rose::Button
|
||||
@@ -142,8 +193,8 @@
|
||||
{{/each}}
|
||||
</table.body>
|
||||
</Rose::Table>
|
||||
{{else if (has-resource-filter-param-selections
|
||||
'scopes.scope.projects.sessions'
|
||||
{{else if (has-resource-filter-selections
|
||||
'scopes.scope.projects.sessions' 'status' 'project'
|
||||
)}}
|
||||
<Rose::Layout::Centered>
|
||||
<Rose::Message @title={{t 'titles.no-results'}} as |message|>
|
||||
|
||||
@@ -9,6 +9,85 @@
|
||||
</page.header>
|
||||
|
||||
<page.body>
|
||||
{{#if (feature-flag 'filter')}}
|
||||
{{#if
|
||||
(or
|
||||
@model
|
||||
(has-resource-filter-selections
|
||||
'scopes.scope.projects.targets' 'project'
|
||||
)
|
||||
)
|
||||
}}
|
||||
<Rose::Toolbar>
|
||||
<Rose::Form as |form|>
|
||||
{{#with
|
||||
(resource-filter 'scopes.scope.projects.targets' 'project')
|
||||
as |filter|
|
||||
}}
|
||||
<Rose::Dropdown
|
||||
@text={{t (concat 'resources.' filter.name '.title')}}
|
||||
@count={{filter.selectedValue.length}}
|
||||
@ignoreClickInside={{true}}
|
||||
as |dropdown|
|
||||
>
|
||||
<ul>
|
||||
{{#with
|
||||
(group-by filter.allowedValues 'scopeModel')
|
||||
as |groups|
|
||||
}}
|
||||
{{#each groups as |groupedItems|}}
|
||||
<li>
|
||||
<dropdown.item>
|
||||
<Rose::Icon
|
||||
@name='flight-icons/svg/org-16'
|
||||
@size='medium'
|
||||
/>
|
||||
{{groupedItems.key.displayName}}
|
||||
</dropdown.item>
|
||||
<ul class='indent-label-2'>
|
||||
<form.checkboxGroup
|
||||
@name={{filter.name}}
|
||||
@items={{groupedItems.items}}
|
||||
@selectedItems={{filter.selectedValue}}
|
||||
@onChange={{route-action 'filterBy' filter.name}}
|
||||
as |group|
|
||||
>
|
||||
<li>
|
||||
<dropdown.item>
|
||||
<group.checkbox
|
||||
@label={{group.item.displayName}}
|
||||
value={{group.item}}
|
||||
/>
|
||||
</dropdown.item>
|
||||
</li>
|
||||
</form.checkboxGroup>
|
||||
</ul>
|
||||
</li>
|
||||
{{/each}}
|
||||
{{/with}}
|
||||
</ul>
|
||||
|
||||
{{! }}
|
||||
</Rose::Dropdown>
|
||||
{{/with}}
|
||||
|
||||
{{#if
|
||||
(has-resource-filter-selections
|
||||
'scopes.scope.projects.targets' 'project'
|
||||
)
|
||||
}}
|
||||
<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>
|
||||
</Rose::Toolbar>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
{{#if @model}}
|
||||
<Rose::Table as |table|>
|
||||
<table.header as |header|>
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
{{page-title (t 'resources.host-set.title_plural')}}
|
||||
|
||||
{{#if @model.hosts}}
|
||||
|
||||
<Rose::Table as |table|>
|
||||
<table.header as |header|>
|
||||
<header.row as |row|>
|
||||
<row.headerCell>{{t 'form.name.label'}}</row.headerCell>
|
||||
<row.headerCell>{{t 'form.id.label'}}</row.headerCell>
|
||||
<row.headerCell>{{t 'form.address.label'}}</row.headerCell>
|
||||
<row.headerCell />
|
||||
</header.row>
|
||||
</table.header>
|
||||
<table.body as |body|>
|
||||
{{#each @model.hosts as |host|}}
|
||||
<body.row as |row|>
|
||||
@@ -20,11 +27,6 @@
|
||||
</Copyable>
|
||||
</row.cell>
|
||||
<row.cell>{{host.attributes.address}}</row.cell>
|
||||
<row.cell>
|
||||
<Rose::Badge>{{t
|
||||
(concat 'resources.host.types.' host.type)
|
||||
}}</Rose::Badge>
|
||||
</row.cell>
|
||||
<row.cell>
|
||||
<Rose::Button
|
||||
@style='secondary'
|
||||
@@ -37,7 +39,6 @@
|
||||
{{/each}}
|
||||
</table.body>
|
||||
</Rose::Table>
|
||||
|
||||
{{else}}
|
||||
|
||||
<Rose::Layout::Centered>
|
||||
|
||||
@@ -1 +1 @@
|
||||
0.7.1
|
||||
0.7.3
|
||||
@@ -50,6 +50,8 @@
|
||||
"path-parse": "^1.0.7",
|
||||
"tar": "^6.1.9",
|
||||
"ansi-regex": "^5.0.1",
|
||||
"nth-check": "^2.0.1"
|
||||
"nth-check": "^2.0.1",
|
||||
"json-schema": "^0.4.0",
|
||||
"jsonpointer": "^5.0.0"
|
||||
}
|
||||
}
|
||||
@@ -2087,10 +2087,10 @@ json-schema-traverse@^0.4.1:
|
||||
resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
|
||||
integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
|
||||
|
||||
json-schema@0.2.3:
|
||||
version "0.2.3"
|
||||
resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13"
|
||||
integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=
|
||||
json-schema@0.2.3, json-schema@^0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5"
|
||||
integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==
|
||||
|
||||
json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1:
|
||||
version "5.0.1"
|
||||
@@ -2113,10 +2113,10 @@ jsonfile@^6.0.1:
|
||||
optionalDependencies:
|
||||
graceful-fs "^4.1.6"
|
||||
|
||||
jsonpointer@^4.0.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.1.0.tgz#501fb89986a2389765ba09e6053299ceb4f2c2cc"
|
||||
integrity sha512-CXcRvMyTlnR53xMcKnuMzfCA5i/nfblTnnr74CZb6C4vG39eu6w51t7nKmU5MfLfbTgGItliNyjO/ciNPDqClg==
|
||||
jsonpointer@^4.0.0, jsonpointer@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-5.0.0.tgz#f802669a524ec4805fa7389eadbc9921d5dc8072"
|
||||
integrity sha512-PNYZIdMjVIvVgDSYKTT63Y+KZ6IZvGRNNWcxwD+GNnUz1MKPfv30J8ueCjdwcN0nDx2SlshgyB7Oy0epAzVRRg==
|
||||
|
||||
jsprim@^1.2.2:
|
||||
version "1.4.1"
|
||||
|
||||
+4
-3
@@ -42,9 +42,10 @@ Vagrant.configure("2") do |config|
|
||||
|
||||
config.vm.provider "virtualbox" do |vb|
|
||||
# Display the VirtualBox GUI when booting the machine
|
||||
vb.gui = true
|
||||
vb.gui = true
|
||||
# Customize the amount of memory on the VM:
|
||||
vb.memory = "4096"
|
||||
vb.customize ["modifyvm", :id, "--vram", "64"]
|
||||
end
|
||||
|
||||
config.vm.provision "setup", type: "shell", inline: <<-SHELL
|
||||
@@ -80,8 +81,8 @@ Vagrant.configure("2") do |config|
|
||||
|
||||
# Setup for developing desktop
|
||||
config.vm.provision "development", type: "shell", inline: <<-SHELL
|
||||
# Setup node 12 source
|
||||
curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -
|
||||
# Setup node source
|
||||
curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash -
|
||||
sudo apt-get install -y nodejs
|
||||
|
||||
sudo apt-get remove cmdtest
|
||||
|
||||
@@ -255,7 +255,11 @@ module('Acceptance | projects | targets', function (hooks) {
|
||||
await visit(urls.targets);
|
||||
});
|
||||
|
||||
test('cannot connect to a target without proper authorization', async function (assert) {
|
||||
// Skipping because this test doesn't make sense. Users will never even see
|
||||
// targets for which they do not have authorize-session. The only reason this
|
||||
// test ever passed was because filtering in target mocks wasn't previously
|
||||
// implemented.
|
||||
test.skip('cannot connect to a target without proper authorization', async function (assert) {
|
||||
assert.expect(3);
|
||||
instances.target.update({
|
||||
authorized_actions: instances.target.authorized_actions.filter(
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { render, findAll } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
|
||||
module('Integration | Helper | group-by', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
// TODO: Replace this with your real tests.
|
||||
test('it renders', async function (assert) {
|
||||
this.set('inputValue', [
|
||||
{ group: 'a', id: '1' },
|
||||
{ group: 'a', id: '2' },
|
||||
{ group: 'b', id: '3' },
|
||||
{ group: 'b', id: '4' },
|
||||
{ group: 'c', id: '5' },
|
||||
{ group: 'c', id: '6' },
|
||||
]);
|
||||
|
||||
await render(hbs`
|
||||
<ul>
|
||||
{{#each (group-by this.inputValue 'group') as |group|}}
|
||||
<li>{{group.key}}</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
`);
|
||||
|
||||
assert.equal(findAll('li').length, 3);
|
||||
});
|
||||
});
|
||||
@@ -14609,10 +14609,10 @@ json-schema-traverse@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2"
|
||||
integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==
|
||||
|
||||
json-schema@0.2.3:
|
||||
version "0.2.3"
|
||||
resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13"
|
||||
integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=
|
||||
json-schema@0.2.3, json-schema@^0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5"
|
||||
integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==
|
||||
|
||||
json-stable-stringify-without-jsonify@^1.0.1:
|
||||
version "1.0.1"
|
||||
@@ -14690,10 +14690,10 @@ jsonparse@^1.2.0, jsonparse@^1.3.1:
|
||||
resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280"
|
||||
integrity sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=
|
||||
|
||||
jsonpointer@^4.0.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.1.0.tgz#501fb89986a2389765ba09e6053299ceb4f2c2cc"
|
||||
integrity sha512-CXcRvMyTlnR53xMcKnuMzfCA5i/nfblTnnr74CZb6C4vG39eu6w51t7nKmU5MfLfbTgGItliNyjO/ciNPDqClg==
|
||||
jsonpointer@5.0.0, jsonpointer@^4.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-5.0.0.tgz#f802669a524ec4805fa7389eadbc9921d5dc8072"
|
||||
integrity sha512-PNYZIdMjVIvVgDSYKTT63Y+KZ6IZvGRNNWcxwD+GNnUz1MKPfv30J8ueCjdwcN0nDx2SlshgyB7Oy0epAzVRRg==
|
||||
|
||||
jsprim@^1.2.2:
|
||||
version "1.4.1"
|
||||
|
||||
Reference in new issue
Block a user