Merge pull request #883 from hashicorp/feature-desktop-target-filters

Add targets filtering by projects in Desktop
This commit is contained in:
Carlos Alcaide Corvo authored and GitHub committed 2021-12-16 16:53:08 -06:00
commit a8bd89fafd
7 files changed
+164 -30

No files matched your search

@@ -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}
+7 -4
View File
@@ -349,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 }) {
@@ -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);
});
});
@@ -105,22 +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;
}
/**
* 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 = [];
}
/**
* 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 = [];
}
}
@@ -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|>
@@ -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(