Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UI: Namespaces redesign #10444

Merged
merged 32 commits into from
Apr 29, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
d08fd8d
New single select dropdown in the style of multiselect
DingoEatingFuzz Apr 7, 2021
7921a00
Add single select dropdown story
DingoEatingFuzz Apr 7, 2021
1ec464b
Style the single select dropdown
DingoEatingFuzz Apr 8, 2021
33e14cd
Rename multi-select-dropdown stories to filter-facets stories
DingoEatingFuzz Apr 8, 2021
8de3cc9
Modernize: use 'this.' when accessing properties in templates
DingoEatingFuzz Apr 8, 2021
09594ee
Stories for single select dropdown
DingoEatingFuzz Apr 8, 2021
6bc9e99
Test coverage for the single select dropdown component
DingoEatingFuzz Apr 14, 2021
3cf82ae
Remove namespace selection from the system service
DingoEatingFuzz Apr 22, 2021
cc17102
Remove system.activeNamespace lookups from everywhere
DingoEatingFuzz Apr 22, 2021
64bcf19
Rewrite abilities to assume a provided namespace instead of a system-…
DingoEatingFuzz Apr 22, 2021
698e91e
Remove the namespace selector from the gutter menu
DingoEatingFuzz Apr 22, 2021
ba1a163
Correct unit tests for the namespaces change
DingoEatingFuzz Apr 22, 2021
0b1635d
Namespaces tests triage
DingoEatingFuzz Apr 22, 2021
ece819a
Remove top-level jobs namespace qp in favor of jobs list and jobs.job…
DingoEatingFuzz Apr 22, 2021
d5e3a29
Support namespaces wildcard in mirage
DingoEatingFuzz Apr 22, 2021
e8112a0
Properly watch namespaced records
DingoEatingFuzz Apr 23, 2021
a821ea3
Load related resources now that jobs and namesapces aren't guaranteed…
DingoEatingFuzz Apr 23, 2021
76322f1
Correct tests now that namespace logic has been moved into the jobs i…
DingoEatingFuzz Apr 23, 2021
d86b08e
Mirage: support namespace=* for volumes
DingoEatingFuzz Apr 23, 2021
1b31b7a
Mirage: no longer need 501 support for namespaces
DingoEatingFuzz Apr 23, 2021
8b28da2
Add the namespaces facet to the volumes list page
DingoEatingFuzz Apr 23, 2021
1dde3de
Mirage: Properly handle namespaces in volume getter
DingoEatingFuzz Apr 23, 2021
9952600
Require the namespace query param on volume detail pages
DingoEatingFuzz Apr 23, 2021
bc16d6e
Replace the all namespaces toggle with a namespaces dropdown on the o…
DingoEatingFuzz Apr 24, 2021
99edf9b
Add namespace facet tests to all impacted pages
DingoEatingFuzz Apr 24, 2021
5812852
Always show the namespace facet, even when getting a forbidden error
DingoEatingFuzz Apr 24, 2021
af91fa1
Include namespace in job and volume rows when a cluster has namespaces
DingoEatingFuzz Apr 24, 2021
017fe38
Persist namespace selection across pages that have a namespace filter
DingoEatingFuzz Apr 24, 2021
7bef355
Add the DAS feature to the smallCluster scenario
DingoEatingFuzz Apr 24, 2021
4fa121a
Volume controller linting (prettier conflict)
DingoEatingFuzz Apr 24, 2021
a81e2b8
Document the new cachedNamespace system service property
DingoEatingFuzz Apr 29, 2021
735f056
Guard against no namespace passed into a can/cannot helper
DingoEatingFuzz Apr 29, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 19 additions & 14 deletions ui/app/abilities/abstract.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,24 @@ export default class Abstract extends Ability {
@not('token.aclEnabled') bypassAuthorization;
@equal('token.selfToken.type', 'management') selfTokenIsManagement;

@computed('system.activeNamespace.name')
get activeNamespace() {
return this.get('system.activeNamespace.name') || 'default';
// Pass in a namespace to `can` or `cannot` calls to override
// https://github.com/minutebase/ember-can#additional-attributes
namespace = 'default';

get _namespace() {
if (!this.namespace) return 'default';
if (typeof this.namespace === 'string') return this.namespace;
return get(this.namespace, 'name');
}
Comment on lines +19 to 23
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

activeNamespace was previously set in our models based on query parameters. We're noticing that we can't detect namespace-specific policy capabilities (with ACL's).

Should we continue the behavior of setting system.activeNamespace in our models to grab namespaces from the query params.

Also, since we're setting namespace to default on line 17, what's the purpose of the control flow logic on lines 19-22.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can set this value based on qpNamespace


@computed('activeNamespace', 'token.selfTokenPolicies.[]')
get rulesForActiveNamespace() {
let activeNamespace = this.activeNamespace;
@computed('_namespace', 'token.selfTokenPolicies.[]')
get rulesForNamespace() {
let namespace = this._namespace;

return (this.get('token.selfTokenPolicies') || []).toArray().reduce((rules, policy) => {
let policyNamespaces = get(policy, 'rulesJSON.Namespaces') || [];

let matchingNamespace = this._findMatchingNamespace(policyNamespaces, activeNamespace);
let matchingNamespace = this._findMatchingNamespace(policyNamespaces, namespace);

if (matchingNamespace) {
rules.push(policyNamespaces.find(namespace => namespace.Name === matchingNamespace));
Expand All @@ -46,8 +51,8 @@ export default class Abstract extends Ability {
}, []);
}

activeNamespaceIncludesCapability(capability) {
return this.rulesForActiveNamespace.some(rules => {
namespaceIncludesCapability(capability) {
return this.rulesForNamespace.some(rules => {
let capabilities = get(rules, 'Capabilities') || [];
return capabilities.includes(capability);
});
Expand All @@ -64,11 +69,11 @@ export default class Abstract extends Ability {

// Chooses the closest namespace as described at the bottom here:
// https://learn.hashicorp.com/tutorials/nomad/access-control-policies?in=nomad/access-control#namespace-rules
_findMatchingNamespace(policyNamespaces, activeNamespace) {
_findMatchingNamespace(policyNamespaces, namespace) {
let namespaceNames = policyNamespaces.mapBy('Name');

if (namespaceNames.includes(activeNamespace)) {
return activeNamespace;
if (namespaceNames.includes(namespace)) {
return namespace;
}

let globNamespaceNames = namespaceNames.filter(namespaceName => namespaceName.includes('*'));
Expand All @@ -77,11 +82,11 @@ export default class Abstract extends Ability {
(mostMatching, namespaceName) => {
// Convert * wildcards to .* for regex matching
let namespaceNameRegExp = new RegExp(namespaceName.replace(/\*/g, '.*'));
let characterDifference = activeNamespace.length - namespaceName.length;
let characterDifference = namespace.length - namespaceName.length;

if (
characterDifference < mostMatching.mostMatchingCharacterDifference &&
activeNamespace.match(namespaceNameRegExp)
namespace.match(namespaceNameRegExp)
) {
return {
mostMatchingNamespaceName: namespaceName,
Expand Down
4 changes: 2 additions & 2 deletions ui/app/abilities/allocation.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ export default class Allocation extends AbstractAbility {
@or('bypassAuthorization', 'selfTokenIsManagement', 'policiesSupportExec')
canExec;

@computed('rulesForActiveNamespace.@each.capabilities')
@computed('rulesForNamespace.@each.capabilities')
get policiesSupportExec() {
return this.rulesForActiveNamespace.some(rules => {
return this.rulesForNamespace.some(rules => {
let capabilities = get(rules, 'Capabilities') || [];
return capabilities.includes('alloc-exec');
});
Expand Down
8 changes: 4 additions & 4 deletions ui/app/abilities/job.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@ export default class Job extends AbstractAbility {
@or('bypassAuthorization', 'selfTokenIsManagement')
canListAll;

@computed('rulesForActiveNamespace.@each.capabilities')
@computed('rulesForNamespace.@each.capabilities')
get policiesSupportRunning() {
return this.activeNamespaceIncludesCapability('submit-job');
return this.namespaceIncludesCapability('submit-job');
}

@computed('rulesForActiveNamespace.@each.capabilities')
@computed('rulesForNamespace.@each.capabilities')
get policiesSupportScaling() {
return this.activeNamespaceIncludesCapability('scale-job');
return this.namespaceIncludesCapability('scale-job');
}
}
4 changes: 2 additions & 2 deletions ui/app/adapters/namespace.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import ApplicationAdapter from './application';
import Watchable from './watchable';
import codesForError from '../utils/codes-for-error';

export default class NamespaceAdapter extends ApplicationAdapter {
export default class NamespaceAdapter extends Watchable {
findRecord(store, modelClass, id) {
return super.findRecord(...arguments).catch(error => {
const errorCodes = codesForError(error);
Expand Down
18 changes: 12 additions & 6 deletions ui/app/adapters/watchable-namespace-ids.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,18 @@ export default class WatchableNamespaceIDs extends Watchable {
@service system;

findAll() {
const namespace = this.get('system.activeNamespace');
return super.findAll(...arguments).then(data => {
data.forEach(record => {
record.Namespace = namespace ? namespace.get('id') : 'default';
record.Namespace = 'default';
});
return data;
});
}

query(store, type, { namespace }) {
return super.query(...arguments).then(data => {
data.forEach(record => {
if (!record.Namespace) record.Namespace = namespace;
});
return data;
});
Expand All @@ -25,14 +33,12 @@ export default class WatchableNamespaceIDs extends Watchable {

urlForFindAll() {
const url = super.urlForFindAll(...arguments);
const namespace = this.get('system.activeNamespace.id');
return associateNamespace(url, namespace);
return associateNamespace(url);
}

urlForQuery() {
const url = super.urlForQuery(...arguments);
const namespace = this.get('system.activeNamespace.id');
return associateNamespace(url, namespace);
return associateNamespace(url);
}

urlForFindRecord(id, type, hash, pathSuffix) {
Expand Down
5 changes: 3 additions & 2 deletions ui/app/adapters/watchable.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,12 @@ export default class Watchable extends ApplicationAdapter {
}

findRecord(store, type, id, snapshot, additionalParams = {}) {
let [url, params] = this.buildURL(type.modelName, id, snapshot, 'findRecord').split('?');
const originalUrl = this.buildURL(type.modelName, id, snapshot, 'findRecord');
let [url, params] = originalUrl.split('?');
params = assign(queryString.parse(params) || {}, this.buildQuery(), additionalParams);

if (get(snapshot || {}, 'adapterOptions.watch')) {
params.index = this.watchList.getIndexFor(url);
params.index = this.watchList.getIndexFor(originalUrl);
}

const signal = get(snapshot || {}, 'adapterOptions.abortController.signal');
Expand Down
1 change: 1 addition & 0 deletions ui/app/components/job-row.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import classic from 'ember-classic-decorator';
@tagName('tr')
@classNames('job-row', 'is-interactive')
export default class JobRow extends Component {
@service system;
@service store;

job = null;
Expand Down
13 changes: 13 additions & 0 deletions ui/app/components/single-select-dropdown/index.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<div data-test-single-select-dropdown class="dropdown" ...attributes>
<PowerSelect
@options={{@options}}
@selected={{this.activeOption}}
@searchEnabled={{gt @options.length 10}}
@searchField="label"
@onChange={{action this.setSelection}}
@dropdownClass="dropdown-options"
as |option|>
<span class="ember-power-select-prefix">{{@label}}: </span>
<span class="dropdown-label" data-test-dropdown-option={{option.key}}>{{option.label}}</span>
</PowerSelect>
</div>
13 changes: 13 additions & 0 deletions ui/app/components/single-select-dropdown/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import Component from '@glimmer/component';
import { action } from '@ember/object';

export default class SingleSelectDropdown extends Component {
get activeOption() {
return this.args.options.findBy('key', this.args.selection);
}

@action
setSelection({ key }) {
this.args.onSelect && this.args.onSelect(key);
}
}
8 changes: 0 additions & 8 deletions ui/app/controllers/csi/volumes.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,5 @@
import Controller from '@ember/controller';

export default class VolumesController extends Controller {
queryParams = [
{
volumeNamespace: 'namespace',
},
];

isForbidden = false;

volumeNamespace = 'default';
}
60 changes: 48 additions & 12 deletions ui/app/controllers/csi/volumes/index.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { inject as service } from '@ember/service';
import { action, computed } from '@ember/object';
import { alias, readOnly } from '@ember/object/computed';
import { scheduleOnce } from '@ember/runloop';
import Controller, { inject as controller } from '@ember/controller';
import SortableFactory from 'nomad-ui/mixins/sortable-factory';
import Searchable from 'nomad-ui/mixins/searchable';
import { lazyClick } from 'nomad-ui/helpers/lazy-click';
import { serialize } from 'nomad-ui/utils/qp-serialize';
import classic from 'ember-classic-decorator';

@classic
Expand Down Expand Up @@ -38,6 +40,9 @@ export default class IndexController extends Controller.extend(
{
sortDescending: 'desc',
},
{
qpNamespace: 'namespace',
},
];

currentPage = 1;
Expand All @@ -58,29 +63,60 @@ export default class IndexController extends Controller.extend(

fuzzySearchEnabled = true;

@computed('qpNamespace', 'model.namespaces.[]', 'system.cachedNamespace')
get optionsNamespaces() {
const availableNamespaces = this.model.namespaces.map(namespace => ({
key: namespace.name,
label: namespace.name,
}));

availableNamespaces.unshift({
key: '*',
label: 'All (*)',
});

// Unset the namespace selection if it was server-side deleted
if (!availableNamespaces.mapBy('key').includes(this.qpNamespace)) {
// eslint-disable-next-line ember/no-incorrect-calls-with-inline-anonymous-functions
scheduleOnce('actions', () => {
// eslint-disable-next-line ember/no-side-effects
this.set('qpNamespace', this.system.cachedNamespace || 'default');
});
}

return availableNamespaces;
}

/**
Visible volumes are those that match the selected namespace
*/
@computed('model.@each.parent', 'system.{activeNamespace.id,namespaces.length}')
@computed('model.volumes.@each.parent', 'system.{namespaces.length}')
get visibleVolumes() {
if (!this.model) return [];

// Namespace related properties are ommitted from the dependent keys
// due to a prop invalidation bug caused by region switching.
const hasNamespaces = this.get('system.namespaces.length');
const activeNamespace = this.get('system.activeNamespace.id') || 'default';

return this.model
.compact()
.filter(volume => !hasNamespaces || volume.get('namespace.id') === activeNamespace);
if (!this.model.volumes) return [];
return this.model.volumes.compact();
}

@alias('visibleVolumes') listToSort;
@alias('listSorted') listToSearch;
@alias('listSearched') sortedVolumes;

@action
cacheNamespace(namespace) {
this.system.cachedNamespace = namespace;
}

setFacetQueryParam(queryParam, selection) {
this.set(queryParam, serialize(selection));
}

@action
gotoVolume(volume, event) {
lazyClick([() => this.transitionToRoute('csi.volumes.volume', volume.get('plainId')), event]);
lazyClick([
() =>
this.transitionToRoute('csi.volumes.volume', volume.get('plainId'), {
queryParams: { volumeNamespace: volume.get('namespace.name') },
}),
event,
]);
}
}
7 changes: 7 additions & 0 deletions ui/app/controllers/csi/volumes/volume.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ export default class VolumeController extends Controller {
// Used in the template
@service system;

queryParams = [
{
volumeNamespace: 'namespace',
},
];
volumeNamespace = 'default';

@computed('model.readAllocations.@each.modifyIndex')
get sortedReadAllocations() {
return this.model.readAllocations.sortBy('modifyIndex').reverse();
Expand Down
15 changes: 1 addition & 14 deletions ui/app/controllers/jobs.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,3 @@
import { inject as service } from '@ember/service';
import Controller from '@ember/controller';

export default class JobsController extends Controller {
@service system;

queryParams = [
{
jobNamespace: 'namespace',
},
];

isForbidden = false;

jobNamespace = 'default';
}
export default class JobsController extends Controller {}
Loading