Merge pull request #883 from hashicorp/feature-desktop-target-filters
Add targets filtering by projects in Desktop
This commit is contained in:
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}
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in new issue
Block a user