UI variables made to be unique by namespace and path (#14072)
* Starting on namespaced id * Traversal for variables uniqued by namespace * Delog * Basic CRUD complete w namespaces included * Correct secvar breadcrumb joining and testfix now that namespaces are included * Testfixes with namespaces in place * Namespace-aware duplicate path warning * Duplicate path warning test additions * Trimpath reimplemented on dupe check * Solves a bug where slash was not being passed to the can write check * PR fixes * variable paths integration test fix now uses store * Seems far less hacky in retrospect * PR feedback addressed * test fixes after inclusion of path as local non-model var * Prevent confusion by dropping namespace from QPs on PUT, since its already in .data * Solves a harsh bug where you have namespace access but no secvars access (#14098) * Solves a harsh bug where you have namespace access but no secvars access * Lint cleanup * Remove unneeded condition
This commit is contained in:
parent
a4e89d72a8
commit
d7def242b8
|
@ -8,13 +8,10 @@ export default class VariableAdapter extends ApplicationAdapter {
|
|||
|
||||
// PUT instead of POST on create;
|
||||
// /v1/var instead of /v1/vars on create (urlForFindRecord)
|
||||
createRecord(_store, _type, snapshot) {
|
||||
createRecord(_store, type, snapshot) {
|
||||
let data = this.serialize(snapshot);
|
||||
return this.ajax(
|
||||
this.urlForFindRecord(snapshot.id, snapshot.modelName),
|
||||
'PUT',
|
||||
{ data }
|
||||
);
|
||||
let baseUrl = this.buildURL(type.modelName, data.ID);
|
||||
return this.ajax(baseUrl, 'PUT', { data });
|
||||
}
|
||||
|
||||
urlForFindAll(modelName) {
|
||||
|
@ -27,21 +24,30 @@ export default class VariableAdapter extends ApplicationAdapter {
|
|||
return pluralize(baseUrl);
|
||||
}
|
||||
|
||||
urlForFindRecord(id, modelName, snapshot) {
|
||||
const namespace = snapshot?.attr('namespace') || 'default';
|
||||
|
||||
let baseUrl = this.buildURL(modelName, id, snapshot);
|
||||
urlForFindRecord(identifier, modelName, snapshot) {
|
||||
const { namespace, id } = _extractIDAndNamespace(identifier, snapshot);
|
||||
let baseUrl = this.buildURL(modelName, id);
|
||||
return `${baseUrl}?namespace=${namespace}`;
|
||||
}
|
||||
|
||||
urlForUpdateRecord(id, modelName) {
|
||||
return this.buildURL(modelName, id);
|
||||
urlForUpdateRecord(identifier, modelName, snapshot) {
|
||||
const { id } = _extractIDAndNamespace(identifier, snapshot);
|
||||
let baseUrl = this.buildURL(modelName, id);
|
||||
return `${baseUrl}`;
|
||||
}
|
||||
|
||||
urlForDeleteRecord(id, modelName, snapshot) {
|
||||
const namespace = snapshot?.attr('namespace') || 'default';
|
||||
|
||||
urlForDeleteRecord(identifier, modelName, snapshot) {
|
||||
const { namespace, id } = _extractIDAndNamespace(identifier, snapshot);
|
||||
const baseUrl = this.buildURL(modelName, id);
|
||||
return `${baseUrl}?namespace=${namespace}`;
|
||||
}
|
||||
}
|
||||
|
||||
function _extractIDAndNamespace(identifier, snapshot) {
|
||||
const namespace = snapshot?.attr('namespace') || 'default';
|
||||
const id = snapshot?.attr('path') || identifier;
|
||||
return {
|
||||
namespace,
|
||||
id,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -16,18 +16,17 @@
|
|||
</span>
|
||||
<Input
|
||||
@type="text"
|
||||
@value={{@model.path}}
|
||||
@value={{this.path}}
|
||||
placeholder="nomad/jobs/my-job/my-group/my-task"
|
||||
class="input path-input {{if this.duplicatePathWarning "error"}}"
|
||||
disabled={{not @model.isNew}}
|
||||
{{on "input" this.validatePath}}
|
||||
{{autofocus}}
|
||||
data-test-path-input
|
||||
/>
|
||||
{{#if this.duplicatePathWarning}}
|
||||
<p class="duplicate-path-error help is-danger">
|
||||
There is already a Secure Variable located at
|
||||
{{@model.path}}
|
||||
{{this.path}}
|
||||
.
|
||||
<br />
|
||||
Please choose a different path, or
|
||||
|
|
|
@ -23,18 +23,15 @@ export default class SecureVariableFormComponent extends Component {
|
|||
@service router;
|
||||
@service store;
|
||||
|
||||
/**
|
||||
* @typedef {Object} DuplicatePathWarning
|
||||
* @property {string} path
|
||||
*/
|
||||
|
||||
/**
|
||||
* @type {DuplicatePathWarning}
|
||||
*/
|
||||
@tracked duplicatePathWarning = null;
|
||||
@tracked variableNamespace = null;
|
||||
@tracked namespaceOptions = null;
|
||||
|
||||
@tracked path = '';
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
set(this, 'path', this.args.model.path);
|
||||
}
|
||||
|
||||
@action
|
||||
setNamespace(namespace) {
|
||||
this.variableNamespace = namespace;
|
||||
|
@ -52,9 +49,8 @@ export default class SecureVariableFormComponent extends Component {
|
|||
|
||||
get shouldDisableSave() {
|
||||
const disallowedPath =
|
||||
this.args.model?.path?.startsWith('nomad/') &&
|
||||
!this.args.model?.path?.startsWith('nomad/jobs');
|
||||
return !!this.JSONError || !this.args.model?.path || disallowedPath;
|
||||
this.path?.startsWith('nomad/') && !this.path?.startsWith('nomad/jobs');
|
||||
return !!this.JSONError || !this.path || disallowedPath;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -94,19 +90,28 @@ export default class SecureVariableFormComponent extends Component {
|
|||
]);
|
||||
}
|
||||
|
||||
@action
|
||||
validatePath(e) {
|
||||
const value = trimPath([e.target.value]);
|
||||
/**
|
||||
* @typedef {Object} DuplicatePathWarning
|
||||
* @property {string} path
|
||||
*/
|
||||
|
||||
/**
|
||||
* @type {DuplicatePathWarning}
|
||||
*/
|
||||
get duplicatePathWarning() {
|
||||
const existingVariables = this.args.existingVariables || [];
|
||||
const pathValue = trimPath([this.path]);
|
||||
let existingVariable = existingVariables
|
||||
.without(this.args.model)
|
||||
.find((v) => v.path === value);
|
||||
.find(
|
||||
(v) => v.path === pathValue && v.namespace === this.variableNamespace
|
||||
);
|
||||
if (existingVariable) {
|
||||
this.duplicatePathWarning = {
|
||||
return {
|
||||
path: existingVariable.path,
|
||||
};
|
||||
} else {
|
||||
this.duplicatePathWarning = null;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -144,7 +149,7 @@ export default class SecureVariableFormComponent extends Component {
|
|||
if (e.type === 'submit') {
|
||||
e.preventDefault();
|
||||
}
|
||||
// TODO: temp, hacky way to force translation to tabular keyValues
|
||||
|
||||
if (this.view === 'json') {
|
||||
this.translateAndValidateItems('table');
|
||||
}
|
||||
|
@ -168,20 +173,21 @@ export default class SecureVariableFormComponent extends Component {
|
|||
}
|
||||
|
||||
this.args.model.set('keyValues', this.keyValues);
|
||||
this.args.model.set('path', this.path);
|
||||
this.args.model.setAndTrimPath();
|
||||
await this.args.model.save();
|
||||
|
||||
this.flashMessages.add({
|
||||
title: 'Secure Variable saved',
|
||||
message: `${this.args.model.path} successfully saved`,
|
||||
message: `${this.path} successfully saved`,
|
||||
type: 'success',
|
||||
destroyOnClick: false,
|
||||
timeout: 5000,
|
||||
});
|
||||
this.router.transitionTo('variables.variable', this.args.model.path);
|
||||
this.router.transitionTo('variables.variable', this.args.model.id);
|
||||
} catch (error) {
|
||||
this.flashMessages.add({
|
||||
title: `Error saving ${this.args.model.path}`,
|
||||
title: `Error saving ${this.path}`,
|
||||
message: error,
|
||||
type: 'error',
|
||||
destroyOnClick: false,
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
{{#if (can "read variable" path=file.absoluteFilePath namespace=file.variable.namespace)}}
|
||||
<LinkTo
|
||||
@route="variables.variable"
|
||||
@model={{file.absoluteFilePath}}
|
||||
@model={{file.variable.id}}
|
||||
@query={{hash namespace="*"}}
|
||||
>
|
||||
{{file.name}}
|
||||
|
|
|
@ -26,9 +26,9 @@ export default class VariablePathsComponent extends Component {
|
|||
}
|
||||
|
||||
@action
|
||||
async handleFileClick({ path, variable: { namespace } }) {
|
||||
async handleFileClick({ path, variable: { id, namespace } }) {
|
||||
if (this.can.can('read variable', null, { path, namespace })) {
|
||||
this.router.transitionTo('variables.variable', path);
|
||||
this.router.transitionTo('variables.variable', id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,16 +4,23 @@ import { action } from '@ember/object';
|
|||
const ALL_NAMESPACE_WILDCARD = '*';
|
||||
|
||||
export default class VariablesPathController extends Controller {
|
||||
get absolutePath() {
|
||||
return this.model?.absolutePath || '';
|
||||
}
|
||||
get breadcrumbs() {
|
||||
let crumbs = [];
|
||||
this.model.absolutePath.split('/').reduce((m, n) => {
|
||||
crumbs.push({
|
||||
label: n,
|
||||
args: [`variables.path`, m + n],
|
||||
});
|
||||
return m + n + '/';
|
||||
}, []);
|
||||
return crumbs;
|
||||
if (this.absolutePath) {
|
||||
let crumbs = [];
|
||||
this.absolutePath.split('/').reduce((m, n) => {
|
||||
crumbs.push({
|
||||
label: n,
|
||||
args: [`variables.path`, m + n],
|
||||
});
|
||||
return m + n + '/';
|
||||
}, []);
|
||||
return crumbs;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@controller variables;
|
||||
|
|
|
@ -3,12 +3,14 @@ import Controller from '@ember/controller';
|
|||
export default class VariablesVariableController extends Controller {
|
||||
get breadcrumbs() {
|
||||
let crumbs = [];
|
||||
this.params.path.split('/').reduce((m, n) => {
|
||||
let id = decodeURI(this.params.id.split('@').slice(0, -1).join('@')); // remove namespace
|
||||
let namespace = this.params.id.split('@').slice(-1)[0];
|
||||
id.split('/').reduce((m, n) => {
|
||||
crumbs.push({
|
||||
label: n,
|
||||
args:
|
||||
m + n === this.params.path // If the last crumb, link to the var itself
|
||||
? [`variables.variable`, m + n]
|
||||
m + n === id // If the last crumb, link to the var itself
|
||||
? [`variables.variable`, `${m + n}@${namespace}`]
|
||||
: [`variables.path`, m + n],
|
||||
});
|
||||
return m + n + '/';
|
||||
|
|
|
@ -6,11 +6,12 @@ import Helper from '@ember/component/helper';
|
|||
* @param {Array<string>} params
|
||||
* @returns {string}
|
||||
*/
|
||||
export function trimPath([path = '']) {
|
||||
export function trimPath([path]) {
|
||||
if (!path) return;
|
||||
if (path.startsWith('/')) {
|
||||
path = trimPath([path.slice(1)]);
|
||||
}
|
||||
if (path.endsWith('/')) {
|
||||
if (path?.endsWith('/')) {
|
||||
path = trimPath([path.slice(0, -1)]);
|
||||
}
|
||||
return path;
|
||||
|
|
|
@ -67,7 +67,9 @@ export default class VariableModel extends Model {
|
|||
*/
|
||||
setAndTrimPath() {
|
||||
this.set('path', trimPath([this.path]));
|
||||
this.set('id', this.get('path'));
|
||||
if (!this.get('id')) {
|
||||
this.set('id', `${this.get('path')}@${this.get('namespace')}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -84,7 +84,7 @@ Router.map(function () {
|
|||
this.route(
|
||||
'variable',
|
||||
{
|
||||
path: '/var/*path',
|
||||
path: '/var/*id',
|
||||
},
|
||||
function () {
|
||||
this.route('edit');
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
import WithForbiddenState from 'nomad-ui/mixins/with-forbidden-state';
|
||||
import notifyError from 'nomad-ui/utils/notify-error';
|
||||
import notifyForbidden from 'nomad-ui/utils/notify-forbidden';
|
||||
import PathTree from 'nomad-ui/utils/path-tree';
|
||||
|
||||
export default class VariablesRoute extends Route.extend(WithForbiddenState) {
|
||||
|
@ -30,13 +30,13 @@ export default class VariablesRoute extends Route.extend(WithForbiddenState) {
|
|||
{ namespace },
|
||||
{ reload: true }
|
||||
);
|
||||
|
||||
return {
|
||||
variables,
|
||||
pathTree: new PathTree(variables),
|
||||
};
|
||||
} catch (e) {
|
||||
notifyError(this)(e);
|
||||
notifyForbidden(this)(e);
|
||||
return e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,19 @@
|
|||
import Route from '@ember/routing/route';
|
||||
import WithForbiddenState from 'nomad-ui/mixins/with-forbidden-state';
|
||||
import notifyForbidden from 'nomad-ui/utils/notify-forbidden';
|
||||
|
||||
export default class VariablesIndexRoute extends Route {
|
||||
export default class VariablesIndexRoute extends Route.extend(
|
||||
WithForbiddenState
|
||||
) {
|
||||
model() {
|
||||
const { variables, pathTree } = this.modelFor('variables');
|
||||
return {
|
||||
variables,
|
||||
root: pathTree.paths.root,
|
||||
};
|
||||
if (this.modelFor('variables').errors) {
|
||||
notifyForbidden(this)(this.modelFor('variables'));
|
||||
} else {
|
||||
const { variables, pathTree } = this.modelFor('variables');
|
||||
return {
|
||||
variables,
|
||||
root: pathTree.paths.root,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,20 @@
|
|||
import Route from '@ember/routing/route';
|
||||
export default class VariablesPathRoute extends Route {
|
||||
import WithForbiddenState from 'nomad-ui/mixins/with-forbidden-state';
|
||||
import notifyForbidden from 'nomad-ui/utils/notify-forbidden';
|
||||
export default class VariablesPathRoute extends Route.extend(
|
||||
WithForbiddenState
|
||||
) {
|
||||
model({ absolutePath }) {
|
||||
const treeAtPath =
|
||||
this.modelFor('variables').pathTree.findPath(absolutePath);
|
||||
if (treeAtPath) {
|
||||
return { treeAtPath, absolutePath };
|
||||
if (this.modelFor('variables').errors) {
|
||||
notifyForbidden(this)(this.modelFor('variables'));
|
||||
} else {
|
||||
return { absolutePath };
|
||||
const treeAtPath =
|
||||
this.modelFor('variables').pathTree.findPath(absolutePath);
|
||||
if (treeAtPath) {
|
||||
return { treeAtPath, absolutePath };
|
||||
} else {
|
||||
return { absolutePath };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ export default class VariablesVariableRoute extends Route.extend(
|
|||
@service store;
|
||||
model(params) {
|
||||
return this.store
|
||||
.findRecord('variable', decodeURIComponent(params.path), {
|
||||
.findRecord('variable', decodeURIComponent(params.id), {
|
||||
reload: true,
|
||||
})
|
||||
.catch(notifyForbidden(this));
|
||||
|
|
|
@ -3,12 +3,16 @@ import ApplicationSerializer from './application';
|
|||
|
||||
@classic
|
||||
export default class VariableSerializer extends ApplicationSerializer {
|
||||
primaryKey = 'Path';
|
||||
separateNanos = ['CreateTime', 'ModifyTime'];
|
||||
|
||||
normalize(typeHash, hash) {
|
||||
// ID is a composite of both the job ID and the namespace the job is in
|
||||
hash.ID = `${hash.Path}@${hash.Namespace || 'default'}`;
|
||||
return super.normalize(typeHash, hash);
|
||||
}
|
||||
|
||||
// Transform API's Items object into an array of a KeyValue objects
|
||||
normalizeFindRecordResponse(store, typeClass, hash, id, ...args) {
|
||||
// TODO: prevent items-less saving at API layer
|
||||
if (!hash.Items) {
|
||||
hash.Items = { '': '' };
|
||||
}
|
||||
|
@ -31,6 +35,7 @@ export default class VariableSerializer extends ApplicationSerializer {
|
|||
// Transform our KeyValues array into an Items object
|
||||
serialize(snapshot, options) {
|
||||
const json = super.serialize(snapshot, options);
|
||||
json.ID = json.Path;
|
||||
json.Items = json.KeyValues.reduce((acc, { key, value }) => {
|
||||
acc[key] = value;
|
||||
return acc;
|
||||
|
|
|
@ -35,32 +35,36 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{#if this.hasVariables}}
|
||||
<VariablePaths
|
||||
@branch={{this.root}}
|
||||
/>
|
||||
{{#if this.isForbidden}}
|
||||
<ForbiddenMessage />
|
||||
{{else}}
|
||||
<div class="empty-message">
|
||||
{{#if (eq this.namespaceSelection "*")}}
|
||||
<h3 data-test-empty-variables-list-headline class="empty-message-headline">
|
||||
No Secure Variables
|
||||
</h3>
|
||||
{{#if (can "write variable" path="*" namespace=this.namespaceSelection)}}
|
||||
{{#if this.hasVariables}}
|
||||
<VariablePaths
|
||||
@branch={{this.root}}
|
||||
/>
|
||||
{{else}}
|
||||
<div class="empty-message">
|
||||
{{#if (eq this.namespaceSelection "*")}}
|
||||
<h3 data-test-empty-variables-list-headline class="empty-message-headline">
|
||||
No Secure Variables
|
||||
</h3>
|
||||
{{#if (can "write variable" path="*" namespace=this.namespaceSelection)}}
|
||||
<p class="empty-message-body">
|
||||
Get started by <LinkTo @route="variables.new">creating a new secure variable</LinkTo>
|
||||
</p>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<h3 data-test-no-matching-variables-list-headline class="empty-message-headline">
|
||||
No Matches
|
||||
</h3>
|
||||
<p class="empty-message-body">
|
||||
Get started by <LinkTo @route="variables.new">creating a new secure variable</LinkTo>
|
||||
No paths or variables match the namespace
|
||||
<strong>
|
||||
{{this.namespaceSelection}}
|
||||
</strong>
|
||||
</p>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<h3 data-test-no-matching-variables-list-headline class="empty-message-headline">
|
||||
No Matches
|
||||
</h3>
|
||||
<p class="empty-message-body">
|
||||
No paths or variables match the namespace
|
||||
<strong>
|
||||
{{this.namespaceSelection}}
|
||||
</strong>
|
||||
</p>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</section>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
{{page-title "Secure Variables: " this.model.absolutePath}}
|
||||
{{page-title "Secure Variables: " this.absolutePath}}
|
||||
{{#each this.breadcrumbs as |crumb|}}
|
||||
<Breadcrumb @crumb={{crumb}} />
|
||||
{{/each}}
|
||||
|
@ -15,52 +15,55 @@
|
|||
/>
|
||||
{{/if}}
|
||||
<div class="button-bar">
|
||||
{{#if (can "write variable" path=this.model.absolutePath namespace=this.namespaceSelection)}}
|
||||
<LinkTo
|
||||
@route="variables.new"
|
||||
@query={{hash path=(concat this.model.absolutePath "/")}}
|
||||
class="button is-primary"
|
||||
>
|
||||
Create Secure Variable
|
||||
</LinkTo>
|
||||
{{else}}
|
||||
<button
|
||||
class="button is-primary is-disabled tooltip is-right-aligned"
|
||||
aria-label="You don’t have sufficient permissions"
|
||||
disabled
|
||||
type="button"
|
||||
>
|
||||
Create Secure Variable
|
||||
</button>
|
||||
{{/if}}
|
||||
|
||||
{{#if (can "write variable" path=(concat this.absolutePath "/") namespace=this.namespaceSelection)}}
|
||||
<LinkTo
|
||||
@route="variables.new"
|
||||
@query={{hash path=(concat this.absolutePath "/")}}
|
||||
class="button is-primary"
|
||||
>
|
||||
Create Secure Variable
|
||||
</LinkTo>
|
||||
{{else}}
|
||||
<button
|
||||
class="button is-primary is-disabled tooltip is-right-aligned"
|
||||
aria-label="You don’t have sufficient permissions"
|
||||
disabled
|
||||
type="button"
|
||||
>
|
||||
Create Secure Variable
|
||||
</button>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{#if this.model.treeAtPath}}
|
||||
<VariablePaths
|
||||
@branch={{this.model.treeAtPath}}
|
||||
/>
|
||||
{{else}}
|
||||
<div class="empty-message">
|
||||
{{#if (eq this.namespaceSelection "*")}}
|
||||
<h3 data-test-empty-variables-list-headline class="empty-message-headline">
|
||||
Path /{{this.model.absolutePath}} contains no variables
|
||||
</h3>
|
||||
<p class="empty-message-body">
|
||||
To get started, <LinkTo @route="variables.new" @query={{hash path=(concat this.model.absolutePath "/")}}>create a new secure variable here</LinkTo>, or <LinkTo @route="variables">go back to the Secure Variables root directory</LinkTo>.
|
||||
</p>
|
||||
{{#if this.isForbidden}}
|
||||
<ForbiddenMessage />
|
||||
{{else}}
|
||||
<h3 data-test-no-matching-variables-list-headline class="empty-message-headline">
|
||||
No Matches
|
||||
</h3>
|
||||
<p class="empty-message-body">
|
||||
No paths or variables match the namespace
|
||||
<strong>
|
||||
{{this.namespaceSelection}}
|
||||
</strong>
|
||||
</p>
|
||||
{{#if this.model.treeAtPath}}
|
||||
<VariablePaths
|
||||
@branch={{this.model.treeAtPath}}
|
||||
/>
|
||||
{{else}}
|
||||
<div class="empty-message">
|
||||
{{#if (eq this.namespaceSelection "*")}}
|
||||
<h3 data-test-empty-variables-list-headline class="empty-message-headline">
|
||||
Path /{{this.absolutePath}} contains no variables
|
||||
</h3>
|
||||
<p class="empty-message-body">
|
||||
To get started, <LinkTo @route="variables.new" @query={{hash path=(concat this.absolutePath "/")}}>create a new secure variable here</LinkTo>, or <LinkTo @route="variables">go back to the Secure Variables root directory</LinkTo>.
|
||||
</p>
|
||||
{{else}}
|
||||
<h3 data-test-no-matching-variables-list-headline class="empty-message-headline">
|
||||
No Matches
|
||||
</h3>
|
||||
<p class="empty-message-body">
|
||||
No paths or variables match the namespace
|
||||
<strong>
|
||||
{{this.namespaceSelection}}
|
||||
</strong>
|
||||
</p>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</section>
|
||||
|
|
|
@ -853,9 +853,9 @@ export default function () {
|
|||
this.put('/var/:id', function (schema, request) {
|
||||
const { Path, Namespace, Items } = JSON.parse(request.requestBody);
|
||||
return server.create('variable', {
|
||||
Path,
|
||||
Namespace,
|
||||
Items,
|
||||
path: Path,
|
||||
namespace: Namespace,
|
||||
items: Items,
|
||||
id: Path,
|
||||
});
|
||||
});
|
||||
|
@ -863,7 +863,7 @@ export default function () {
|
|||
this.delete('/var/:id', function (schema, request) {
|
||||
const { id } = request.params;
|
||||
server.db.variables.remove(id);
|
||||
return okEmpty();
|
||||
return '';
|
||||
});
|
||||
|
||||
//#endregion Secure Variables
|
||||
|
|
|
@ -25,15 +25,11 @@ export default Factory.extend({
|
|||
},
|
||||
|
||||
afterCreate(variable, server) {
|
||||
if (!variable.namespaceId) {
|
||||
if (!variable.namespace) {
|
||||
const namespace = pickOne(server.db.jobs)?.namespace || 'default';
|
||||
variable.update({
|
||||
namespace,
|
||||
});
|
||||
} else {
|
||||
variable.update({
|
||||
namespace: variable.namespaceId,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -80,17 +80,17 @@ function smallCluster(server) {
|
|||
|
||||
server.create('variable', {
|
||||
id: `nomad/jobs/${variableLinkedJob.id}/${variableLinkedGroup.name}/${variableLinkedTask.name}`,
|
||||
namespaceId: variableLinkedJob.namespace,
|
||||
namespace: variableLinkedJob.namespace,
|
||||
});
|
||||
|
||||
server.create('variable', {
|
||||
id: `nomad/jobs/${variableLinkedJob.id}/${variableLinkedGroup.name}`,
|
||||
namespaceId: variableLinkedJob.namespace,
|
||||
namespace: variableLinkedJob.namespace,
|
||||
});
|
||||
|
||||
server.create('variable', {
|
||||
id: `nomad/jobs/${variableLinkedJob.id}`,
|
||||
namespaceId: variableLinkedJob.namespace,
|
||||
namespace: variableLinkedJob.namespace,
|
||||
});
|
||||
|
||||
// #region evaluations
|
||||
|
@ -200,24 +200,36 @@ function variableTestCluster(server) {
|
|||
'w/x/y/foo9',
|
||||
'w/x/y/z/foo10',
|
||||
'w/x/y/z/bar11',
|
||||
'just some arbitrary file',
|
||||
'another arbitrary file',
|
||||
'another arbitrary file again',
|
||||
].forEach((path) => server.create('variable', { id: path }));
|
||||
|
||||
server.create('variable', {
|
||||
id: `nomad/jobs/${variableLinkedJob.id}/${variableLinkedGroup.name}/${variableLinkedTask.name}`,
|
||||
namespaceId: variableLinkedJob.namespace,
|
||||
namespace: variableLinkedJob.namespace,
|
||||
});
|
||||
|
||||
server.create('variable', {
|
||||
id: `nomad/jobs/${variableLinkedJob.id}/${variableLinkedGroup.name}`,
|
||||
namespaceId: variableLinkedJob.namespace,
|
||||
namespace: variableLinkedJob.namespace,
|
||||
});
|
||||
|
||||
server.create('variable', {
|
||||
id: `nomad/jobs/${variableLinkedJob.id}`,
|
||||
namespaceId: variableLinkedJob.namespace,
|
||||
namespace: variableLinkedJob.namespace,
|
||||
});
|
||||
|
||||
server.create('variable', {
|
||||
id: 'just some arbitrary file',
|
||||
namespace: 'namespace-2',
|
||||
});
|
||||
|
||||
server.create('variable', {
|
||||
id: 'another arbitrary file',
|
||||
namespace: 'namespace-2',
|
||||
});
|
||||
|
||||
server.create('variable', {
|
||||
id: 'another arbitrary file again',
|
||||
namespace: 'namespace-2',
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -103,9 +103,8 @@ module('Acceptance | secure variables', function (hooks) {
|
|||
await percySnapshot(assert);
|
||||
|
||||
await click(fooLink);
|
||||
assert.equal(
|
||||
currentURL(),
|
||||
'/variables/var/a/b/c/foo0',
|
||||
assert.ok(
|
||||
currentURL().includes('/variables/var/a/b/c/foo0'),
|
||||
'correctly traverses to a deeply nested variable file'
|
||||
);
|
||||
const deleteButton = find('[data-test-delete-button] button');
|
||||
|
@ -173,9 +172,8 @@ module('Acceptance | secure variables', function (hooks) {
|
|||
assert.ok(nonJobLink, 'non-job file is present');
|
||||
|
||||
await click(nonJobLink);
|
||||
assert.equal(
|
||||
currentURL(),
|
||||
'/variables/var/just some arbitrary file',
|
||||
assert.ok(
|
||||
currentURL().includes('/variables/var/just some arbitrary file'),
|
||||
'correctly traverses to a non-job file'
|
||||
);
|
||||
let relatedEntitiesBox = find('.related-entities');
|
||||
|
@ -335,7 +333,10 @@ module('Acceptance | secure variables', function (hooks) {
|
|||
await click('button[type="submit"]');
|
||||
|
||||
assert.dom('.flash-message.alert-success').exists();
|
||||
assert.equal(currentURL(), '/variables/var/foo/bar');
|
||||
assert.ok(
|
||||
currentURL().includes('/variables/var/foo'),
|
||||
'drops you back off to the parent page'
|
||||
);
|
||||
});
|
||||
|
||||
test('it passes an accessibility audit', async function (assert) {
|
||||
|
@ -514,7 +515,6 @@ module('Acceptance | secure variables', function (hooks) {
|
|||
await typeIn('[data-test-var-key]', 'kiki');
|
||||
await typeIn('[data-test-var-value]', 'do you love me');
|
||||
await click('[data-test-submit-var]');
|
||||
|
||||
assert.equal(
|
||||
currentRouteName(),
|
||||
'variables.variable.index',
|
||||
|
|
|
@ -7,6 +7,10 @@ import { setupMirage } from 'ember-cli-mirage/test-support';
|
|||
import setupCodeMirror from 'nomad-ui/tests/helpers/codemirror';
|
||||
import { codeFillable, code } from 'nomad-ui/tests/pages/helpers/codemirror';
|
||||
import percySnapshot from '@percy/ember';
|
||||
import {
|
||||
selectChoose,
|
||||
clickTrigger,
|
||||
} from 'ember-power-select/test-support/helpers';
|
||||
|
||||
module('Integration | Component | secure-variable-form', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
@ -219,6 +223,8 @@ module('Integration | Component | secure-variable-form', function (hooks) {
|
|||
|
||||
module('Validation', function () {
|
||||
test('warns when you try to create a path that already exists', async function (assert) {
|
||||
this.server.createList('namespace', 3);
|
||||
|
||||
this.set(
|
||||
'mockedModel',
|
||||
server.create('variable', {
|
||||
|
@ -227,23 +233,41 @@ module('Integration | Component | secure-variable-form', function (hooks) {
|
|||
})
|
||||
);
|
||||
|
||||
this.set(
|
||||
'existingVariables',
|
||||
server.createList('variable', 1, {
|
||||
path: 'baz/bat',
|
||||
})
|
||||
);
|
||||
server.create('variable', {
|
||||
path: 'baz/bat',
|
||||
});
|
||||
server.create('variable', {
|
||||
path: 'baz/bat/qux',
|
||||
namespace: server.db.namespaces[2].id,
|
||||
});
|
||||
|
||||
this.set('existingVariables', server.db.variables.toArray());
|
||||
|
||||
await render(
|
||||
hbs`<SecureVariableForm @model={{this.mockedModel}} @existingVariables={{this.existingVariables}} />`
|
||||
);
|
||||
await typeIn('.path-input', 'baz/bat');
|
||||
assert.dom('.duplicate-path-error').exists();
|
||||
assert.dom('.path-input').hasClass('error');
|
||||
|
||||
await typeIn('.path-input', 'foo/bar');
|
||||
assert.dom('.duplicate-path-error').doesNotExist();
|
||||
assert.dom('.path-input').doesNotHaveClass('error');
|
||||
|
||||
document.querySelector('.path-input').value = ''; // clear current input
|
||||
await typeIn('.path-input', 'baz/bat');
|
||||
assert.dom('.duplicate-path-error').exists();
|
||||
assert.dom('.path-input').hasClass('error');
|
||||
|
||||
await clickTrigger('[data-test-variable-namespace-filter]');
|
||||
await selectChoose(
|
||||
'[data-test-variable-namespace-filter]',
|
||||
server.db.namespaces[2].id
|
||||
);
|
||||
assert.dom('.duplicate-path-error').doesNotExist();
|
||||
assert.dom('.path-input').doesNotHaveClass('error');
|
||||
|
||||
document.querySelector('.path-input').value = ''; // clear current input
|
||||
await typeIn('.path-input', 'baz/bat/qux');
|
||||
assert.dom('.duplicate-path-error').exists();
|
||||
assert.dom('.path-input').hasClass('error');
|
||||
});
|
||||
|
||||
test('warns you when you set a key with . in it', async function (assert) {
|
||||
|
|
|
@ -6,26 +6,34 @@ import { hbs } from 'ember-cli-htmlbars';
|
|||
import { componentA11yAudit } from 'nomad-ui/tests/helpers/a11y-audit';
|
||||
import pathTree from 'nomad-ui/utils/path-tree';
|
||||
import Service from '@ember/service';
|
||||
|
||||
const PATHSTRINGS = [
|
||||
{ path: '/foo/bar/baz' },
|
||||
{ path: '/foo/bar/bay' },
|
||||
{ path: '/foo/bar/bax' },
|
||||
{ path: '/a/b' },
|
||||
{ path: '/a/b/c' },
|
||||
{ path: '/a/b/canary' },
|
||||
{ path: '/a/b/canine' },
|
||||
{ path: '/a/b/chipmunk' },
|
||||
{ path: '/a/b/c/d' },
|
||||
{ path: '/a/b/c/dalmation/index' },
|
||||
{ path: '/a/b/c/doberman/index' },
|
||||
{ path: '/a/b/c/dachshund/index' },
|
||||
{ path: '/a/b/c/dachshund/poppy' },
|
||||
];
|
||||
const tree = new pathTree(PATHSTRINGS);
|
||||
let tree;
|
||||
|
||||
module('Integration | Component | variable-paths', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
hooks.beforeEach(function () {
|
||||
this.store = this.owner.lookup('service:store');
|
||||
|
||||
const PATHSTRINGS = [
|
||||
{ path: '/foo/bar/baz' },
|
||||
{ path: '/foo/bar/bay' },
|
||||
{ path: '/foo/bar/bax' },
|
||||
{ path: '/a/b' },
|
||||
{ path: '/a/b/c' },
|
||||
{ path: '/a/b/canary' },
|
||||
{ path: '/a/b/canine' },
|
||||
{ path: '/a/b/chipmunk' },
|
||||
{ path: '/a/b/c/d' },
|
||||
{ path: '/a/b/c/dalmation/index' },
|
||||
{ path: '/a/b/c/doberman/index' },
|
||||
{ path: '/a/b/c/dachshund/index' },
|
||||
{ path: '/a/b/c/dachshund/poppy' },
|
||||
].map((x) => {
|
||||
const varInstance = this.store.createRecord('variable', x);
|
||||
varInstance.setAndTrimPath();
|
||||
return varInstance;
|
||||
});
|
||||
tree = new pathTree(PATHSTRINGS);
|
||||
});
|
||||
|
||||
test('it renders without data', async function (assert) {
|
||||
assert.expect(2);
|
||||
|
@ -109,21 +117,21 @@ module('Integration | Component | variable-paths', function (hooks) {
|
|||
.dom('tbody tr:first-child td:first-child a')
|
||||
.hasAttribute(
|
||||
'href',
|
||||
'/ui/variables/var/foo/bar/baz',
|
||||
'/ui/variables/var/foo/bar/baz@default',
|
||||
'Correctly links the first file'
|
||||
);
|
||||
assert
|
||||
.dom('tbody tr:nth-child(2) td:first-child a')
|
||||
.hasAttribute(
|
||||
'href',
|
||||
'/ui/variables/var/foo/bar/bay',
|
||||
'/ui/variables/var/foo/bar/bay@default',
|
||||
'Correctly links the second file'
|
||||
);
|
||||
assert
|
||||
.dom('tbody tr:nth-child(3) td:first-child a')
|
||||
.hasAttribute(
|
||||
'href',
|
||||
'/ui/variables/var/foo/bar/bax',
|
||||
'/ui/variables/var/foo/bar/bax@default',
|
||||
'Correctly links the third file'
|
||||
);
|
||||
assert
|
||||
|
|
|
@ -12,7 +12,7 @@ module('Unit | Adapter | Variable', function (hooks) {
|
|||
// we're incorrectly passing an object with a `Model` interface
|
||||
// we should be passing a `Snapshot`
|
||||
// hacky fix to rectify the issue
|
||||
newVariable.attr = () => 'default';
|
||||
newVariable.attr = () => {};
|
||||
|
||||
assert.equal(
|
||||
this.subject().urlForFindAll('variable'),
|
||||
|
|
Loading…
Reference in New Issue