Compare commits

..
60 Commits
Author SHA1 Message Date
Carlos Alcaide Corvo 83c2e84647 Merge pull request #884 from hashicorp/chore-bump-binary-version
Chore bump binary version
2021-12-16 17:10:06 -06:00
Carlos Alcaide Corvo 429665a1e0 Add PR to changelog 2021-12-16 14:56:10 -08:00
Carlos Alcaide Corvo a174c332f3 Bump CLI version and update changelog 2021-12-16 14:53:24 -08:00
Carlos Alcaide Corvo d26b668b38 chore: 🤖 Bump CLI version
Bump the CLI version the desktop client uses
2021-12-16 14:53:24 -08:00
Carlos Alcaide Corvo a8bd89fafd Merge pull request #883 from hashicorp/feature-desktop-target-filters
Add targets filtering by projects in Desktop
2021-12-16 16:53:08 -06:00
Carlos Alcaide Corvo 33256f4c64 Merge branch 'main' into feature-desktop-target-filters 2021-12-16 16:44:36 -06:00
Randall Morey fc3b574425 Merge pull request #882 from hashicorp/refactor-scope-filter-ux
Improve project resource filtering for sessions in Desktop
2021-12-16 17:39:33 -05:00
Randall Morey ba10451293 fix: skip flawed test 2021-12-16 17:38:37 -05:00
Randall Morey 4dee759674 docs: add groupBy helper doc comment 2021-12-16 17:09:00 -05:00
Randall Morey 02858bbacb fix: broken development mocks 2021-12-16 16:56:35 -05:00
Randall Morey 69ac051786 feature: implement project filtering in desktop targets 2021-12-16 16:37:19 -05:00
Randall Morey 2c998bbb2b chore: add support for contains clauses in filter store 2021-12-16 16:02:00 -05:00
Randall Morey a79823d3b1 Merge branch 'main' into refactor-scope-filter-ux 2021-12-16 09:48:57 -05:00
Randall Morey 30d029a223 Merge pull request #878 from hashicorp/feature-host-catalogs-only-supports-static-type
feat: 🎸 Host catalogs only supports host type static
2021-12-15 13:26:42 -05:00
Randall Morey 9ce25b8e83 Merge branch 'main' into feature-host-catalogs-only-supports-static-type 2021-12-15 13:15:43 -05:00
Randall Morey ac5710979a refactor: 💡 apply type to host catalog and distribute down 2021-12-15 13:15:31 -05:00
Carlos Alcaide Corvo af749b57bc Merge pull request #881 from hashicorp/feature-add-targets-hosts-list-table-header
feat: 🎸 Add table header for hosts list within targets
2021-12-15 11:50:57 -06:00
Randall Morey 646125f039 fix: 🐛 sessions should poll 2021-12-15 10:10:18 -05:00
Randall Morey 53aade5322 refactor: 💡 improve project filter UX 2021-12-15 09:43:20 -05:00
Carlos Alcaide Corvo be20491d2e Delete type column on hosts list within targets. Change test back to its initial state 2021-12-14 16:18:18 -08:00
Carlos Alcaide Corvo 7d3a5227d6 Delete type column on hosts list 2021-12-14 15:44:51 -08:00
Carlos Alcaide Corvo 8daf6129d1 feat: 🎸 Add table header for hosts list within targets
Add table header for hosts list within targets
2021-12-14 10:39:15 -08:00
Carlos Alcaide Corvo 6cec91b09b Change test to filter the hosts list by type 2021-12-14 09:22:33 -08:00
Carlos Alcaide Corvo bc0af27afa Add conditional on template 2021-12-14 08:35:13 -08:00
Carlos Alcaide Corvo f4cdbfc99c Fix lint error 2021-12-14 08:13:18 -08:00
Carlos Alcaide Corvo aaa205b4e0 Add different host types in Mirage. Add conditional in admin host-catalog hosts list to just show type=static 2021-12-14 08:08:15 -08:00
Carlos Alcaide Corvo c8b49beba6 feat: 🎸 Host list in targets shows just type static 2021-12-14 08:08:15 -08:00
Carlos Alcaide Corvo 6c2694999c feat: 🎸 Host catalogs only supports host type static
Host Catalogs will just show the hosts with type: static
2021-12-14 08:08:15 -08:00
Randall Morey dfc3989a38 Merge pull request #880 from hashicorp/feature-desktop-sessions-project-filter
Desktop sessions filtering
2021-12-14 09:29:53 -05:00
Randall Morey d1fdfb2496 fix: 🐛 tweak helper usage to include both filters 2021-12-13 13:50:40 -05:00
Randall Morey 7b594ca906 chore: allow bexpr filtering of scope mocks 2021-12-13 13:34:28 -05:00
Randall Morey 88bb34828b feature: initial project filtering UX in desktop sessions 2021-12-10 17:29:50 -05:00
Randall Morey 3086cbf4e7 refactor: use resource filter store in projects route 2021-12-10 16:33:05 -05:00
Randall Morey d7a4dd40ea Merge pull request #879 from hashicorp/chore-mock-desktop-sessions
chore: 🤖 ensure mock sessions appear in desktop
2021-12-10 16:00:23 -05:00
Randall Morey 7238a698a1 chore: 🤖 ensure mock sessions appear in desktop 2021-12-09 15:21:24 -05:00
Randall Morey a7c5332a99 Merge pull request #877 from hashicorp/refactor-resource-filters
Refactor resource filters
2021-12-08 17:09:32 -05:00
Randall Morey e7b1d29d9d Merge branch 'main' into refactor-resource-filters 2021-12-08 14:49:15 -05:00
Carlos Alcaide Corvo 4f7e5bb732 Merge pull request #876 from hashicorp/chore-add-json-schema-global-resolutions
chore: 🤖 Add json-schema to global resolutions
2021-12-08 10:41:14 -08:00
Randall Morey 77aaacc8b3 Merge branch 'main' into refactor-resource-filters 2021-12-08 13:36:04 -05:00
Randall Morey ddc7fe939d refactor: 💡 more renaming 2021-12-06 17:26:36 -05:00
Carlos Alcaide Corvo 767be30476 chore: 🤖 Add json-schema to global resolutions
Add json-schema to global resolutions (root package.json). Apart from
our direct dependency within electron, in the admin, json-schema is a
dependency of a dependency.
2021-12-06 14:13:18 -08:00
Randall Morey 400b0b5f0b refactor: 💡 more resource filtering name changes 2021-12-06 16:50:45 -05:00
Randall Morey b3d0bf5685 refactor: support dynamic resource filters and apply to auth methods 2021-12-03 13:58:12 -05:00
Carlos Alcaide Corvo 5cb8826f4e Merge pull request #872 from hashicorp/chore-json-schema-resolutions
chore: 🤖 Add json-schema to desktop resolutions
2021-12-02 13:31:59 -08:00
Carlos Alcaide Corvo fd1b5cfd74 Merge branch 'main' into chore-json-schema-resolutions 2021-11-30 13:42:17 -08:00
Carlos Alcaide Corvo 6ef055bb15 Merge pull request #873 from hashicorp/chore-jsonpointer-desktop-resolutions
chore: 🤖 Add jsonpointer desktop resolutions
2021-11-30 13:32:07 -08:00
Carlos Alcaide Corvo 5293a21e03 Merge branch 'main' into chore-jsonpointer-desktop-resolutions 2021-11-30 13:19:22 -08:00
Randall Morey 01a90a4d75 Merge branch 'main' into refactor-resource-filters 2021-11-30 15:19:27 -05:00
Randall Morey 5dc99ef5ad refactor: 💡 move resource filter logic into dedicated class 2021-11-30 15:18:59 -05:00
Randall Morey 488e724745 Merge branch 'main' into chore-json-schema-resolutions 2021-11-30 09:39:14 -05:00
Randall Morey 126b8944c4 Merge pull request #864 from hashicorp/chore-update-vagrantfile
chore: update vm to node 14 and increase video memory
2021-11-30 09:38:32 -05:00
Carlos Alcaide Corvo b6b6eeff53 chore: 🤖 Add jsonpointer desktop resolutions
Add jsonpointer desktop resolution as per dependabot alert.
2021-11-24 14:26:33 -08:00
Carlos Alcaide Corvo 44078a78ad chore: 🤖 Add json-schema to desktop resolutions
Add json-schema to desktop resolution as per dependabot alert
2021-11-24 14:06:53 -08:00
Dharini Jeeva 39ece5a2c0 Merge branch 'main' into chore-update-vagrantfile 2021-11-22 15:34:13 -08:00
Carlos Alcaide Corvo 02fc874ef5 Merge pull request #859 from hashicorp/chore-bump-jsonpointer
chore: 🤖 resolve jsonpointer for dependabot
2021-11-22 16:09:32 -06:00
Carlos Alcaide Corvo 5f83fa53cf Merge branch 'main' into chore-bump-jsonpointer 2021-11-22 15:58:40 -06:00
Carlos Alcaide Corvo 097abcd3eb Merge branch 'main' into chore-bump-jsonpointer 2021-11-17 13:25:08 -08:00
Susmitha Girumala 1f4e60ce97 Merge branch 'main' into chore-update-vagrantfile 2021-11-16 18:09:16 -05:00
Randall Morey e22c53ea27 chore: update vm to node 14 and increase video memory 2021-11-16 16:33:36 -05:00
Randall Morey 425234148e chore: 🤖 resolve jsonpointer for dependabot 2021-11-16 11:48:10 -05:00
44 changed files with 708 additions and 242 deletions

No files matched your search

+4
View File
@@ -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();
+8 -1
View File
@@ -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}
+12 -7
View File
@@ -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 }) {
+3 -3
View File
@@ -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(),
+2 -2
View File
@@ -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: '',
+3 -2
View File
@@ -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;
},
};
};
}
@@ -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,
@@ -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
View File
@@ -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 -3
View File
@@ -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>
+7
View File
@@ -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
+24
View File
@@ -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 = [];
}
}
+10 -3
View File
@@ -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
View File
@@ -1 +1 @@
0.7.1
0.7.3
+3 -1
View File
@@ -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"
}
}
+8 -8
View File
@@ -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
View File
@@ -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);
});
});
+8 -8
View File
@@ -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"