UI: Ember deprecation - addObject, removeObject (#25952)

* Update add-to-array and remove-from-array helpers

* remove search-select-has-many, moved logic directly into mfa-login-enforcement-form (see #16470)

* Replace add/remove object in MFA files - All MFA tests pass

* Replace in PKI components (pki tests all passing)

* Replace in core addon where applicable

* glimmerize console service -- console tests pass

* more replacements

* update string-list, add comment to vertical-bar-chart

* Refactor CSP Event service

- only used one place (auth-form) so simplified that usage
- glimmerize and refactor so that the tests work

* small updates

* more cleanup

* Fix tests

* Remove objectAt from console-helpers

* Address PR comments

* move commandIndex clearing back

* Remove extra model set
This commit is contained in:
Chelsea Shaw
2024-03-25 13:31:31 -05:00
committed by GitHub
parent 74c350474b
commit 5c18a4e7a4
39 changed files with 239 additions and 193 deletions

View File

@@ -144,7 +144,7 @@ export default ApplicationAdapter.extend({
async _updateAllowedRoles(store, { role, backend, db, type = 'add' }) { async _updateAllowedRoles(store, { role, backend, db, type = 'add' }) {
const connection = await store.queryRecord('database/connection', { backend, id: db }); const connection = await store.queryRecord('database/connection', { backend, id: db });
const roles = [...(connection.allowed_roles || [])]; const roles = [...(connection.allowed_roles || [])];
const allowedRoles = type === 'add' ? addToArray([roles, role]) : removeFromArray([roles, role]); const allowedRoles = type === 'add' ? addToArray(roles, role) : removeFromArray(roles, role);
connection.allowed_roles = allowedRoles; connection.allowed_roles = allowedRoles;
return connection.save(); return connection.save();
}, },

View File

@@ -7,6 +7,7 @@ import NamedPathAdapter from 'vault/adapters/named-path';
import { encodePath } from 'vault/utils/path-encoding-helpers'; import { encodePath } from 'vault/utils/path-encoding-helpers';
import { service } from '@ember/service'; import { service } from '@ember/service';
import AdapterError from '@ember-data/adapter/error'; import AdapterError from '@ember-data/adapter/error';
import { addManyToArray } from 'vault/helpers/add-to-array';
export default class LdapRoleAdapter extends NamedPathAdapter { export default class LdapRoleAdapter extends NamedPathAdapter {
@service flashMessages; @service flashMessages;
@@ -33,7 +34,7 @@ export default class LdapRoleAdapter extends NamedPathAdapter {
async query(store, type, query, recordArray, options) { async query(store, type, query, recordArray, options) {
const { showPartialError } = options.adapterOptions || {}; const { showPartialError } = options.adapterOptions || {};
const { backend } = query; const { backend } = query;
const roles = []; let roles = [];
const errors = []; const errors = [];
for (const roleType of ['static', 'dynamic']) { for (const roleType of ['static', 'dynamic']) {
@@ -42,7 +43,7 @@ export default class LdapRoleAdapter extends NamedPathAdapter {
const models = await this.ajax(url, 'GET', { data: { list: true } }).then((resp) => { const models = await this.ajax(url, 'GET', { data: { list: true } }).then((resp) => {
return resp.data.keys.map((name) => ({ id: name, name, backend, type: roleType })); return resp.data.keys.map((name) => ({ id: name, name, backend, type: roleType }));
}); });
roles.addObjects(models); roles = addManyToArray(roles, models);
} catch (error) { } catch (error) {
if (error.httpStatus !== 404) { if (error.httpStatus !== 404) {
errors.push(error); errors.push(error);

View File

@@ -6,7 +6,7 @@
import Ember from 'ember'; import Ember from 'ember';
import { next } from '@ember/runloop'; import { next } from '@ember/runloop';
import { service } from '@ember/service'; import { service } from '@ember/service';
import { match, alias, or } from '@ember/object/computed'; import { match, or } from '@ember/object/computed';
import { dasherize } from '@ember/string'; import { dasherize } from '@ember/string';
import Component from '@ember/component'; import Component from '@ember/component';
import { computed } from '@ember/object'; import { computed } from '@ember/object';
@@ -166,9 +166,12 @@ export default Component.extend(DEFAULTS, {
return templateName; return templateName;
}), }),
hasCSPError: alias('csp.connectionViolations'), cspError: computed('csp.connectionViolations.length', function () {
if (this.csp.connectionViolations.length) {
cspErrorText: `This is a standby Vault node but can't communicate with the active node via request forwarding. Sign in at the active node to use the Vault UI.`, return `This is a standby Vault node but can't communicate with the active node via request forwarding. Sign in at the active node to use the Vault UI.`;
}
return '';
}),
allSupportedMethods: computed('methodsToShow', 'hasMethodsWithPath', 'authMethods', function () { allSupportedMethods: computed('methodsToShow', 'hasMethodsWithPath', 'authMethods', function () {
const hasMethodsWithPath = this.hasMethodsWithPath; const hasMethodsWithPath = this.hasMethodsWithPath;

View File

@@ -175,7 +175,9 @@ export default class HorizontalBarChart extends Component {
this.isLabel = false; this.isLabel = false;
this.tooltipText = []; // clear stats this.tooltipText = []; // clear stats
this.args.chartLegend.forEach(({ key, label }) => { this.args.chartLegend.forEach(({ key, label }) => {
this.tooltipText.pushObject(`${formatNumber([data[key]])} ${label}`); // since we're relying on D3 not ember reactivity,
// pushing directly to this.tooltipText updates the DOM
this.tooltipText.push(`${formatNumber([data[key]])} ${label}`);
}); });
select(hoveredElement).style('opacity', 1); select(hoveredElement).style('opacity', 1);

View File

@@ -155,7 +155,9 @@ export default class VerticalBarChart extends Component {
this.tooltipStats = []; // clear stats this.tooltipStats = []; // clear stats
this.args.chartLegend.forEach(({ key, label }) => { this.args.chartLegend.forEach(({ key, label }) => {
stackedNumbers.push(data[key]); stackedNumbers.push(data[key]);
this.tooltipStats.pushObject(`${formatNumber([data[key]])} ${label}`); // since we're relying on D3 not ember reactivity,
// pushing directly to this.tooltipStats updates the DOM
this.tooltipStats.push(`${formatNumber([data[key]])} ${label}`);
}); });
this.tooltipTotal = `${formatNumber([calculateSum(stackedNumbers)])} ${ this.tooltipTotal = `${formatNumber([calculateSum(stackedNumbers)])} ${
data.new_clients ? 'total' : 'new' data.new_clients ? 'total' : 'new'

View File

@@ -9,6 +9,7 @@ import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking'; import { tracked } from '@glimmer/tracking';
import { task } from 'ember-concurrency'; import { task } from 'ember-concurrency';
import { waitFor } from '@ember/test-waiters'; import { waitFor } from '@ember/test-waiters';
import { removeFromArray } from 'vault/helpers/remove-from-array';
/** /**
* @module KeymgmtProviderEdit * @module KeymgmtProviderEdit
@@ -94,8 +95,9 @@ export default class KeymgmtProviderEdit extends Component {
@action @action
async onDeleteKey(model) { async onDeleteKey(model) {
try { try {
const providerKeys = removeFromArray(this.args.model.keys, model);
await model.destroyRecord(); await model.destroyRecord();
this.args.model.keys.removeObject(model); this.args.model.keys = providerKeys;
} catch (error) { } catch (error) {
this.flashMessages.danger(error.errors.join('. ')); this.flashMessages.danger(error.errors.join('. '));
} }

View File

@@ -8,7 +8,8 @@ import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object'; import { action } from '@ember/object';
import { service } from '@ember/service'; import { service } from '@ember/service';
import { task } from 'ember-concurrency'; import { task } from 'ember-concurrency';
import handleHasManySelection from 'core/utils/search-select-has-many'; import { addManyToArray, addToArray } from 'vault/helpers/add-to-array';
import { removeFromArray } from 'vault/helpers/remove-from-array';
/** /**
* @module MfaLoginEnforcementForm * @module MfaLoginEnforcementForm
@@ -64,7 +65,7 @@ export default class MfaLoginEnforcementForm extends Component {
for (const { label, key } of this.targetTypes) { for (const { label, key } of this.targetTypes) {
const targetArray = await this.args.model[key]; const targetArray = await this.args.model[key];
const targets = targetArray.map((value) => ({ label, key, value })); const targets = targetArray.map((value) => ({ label, key, value }));
this.targets.addObjects(targets); this.targets = addManyToArray(this.targets, targets);
} }
} }
async resetTargetState() { async resetTargetState() {
@@ -102,7 +103,6 @@ export default class MfaLoginEnforcementForm extends Component {
updateModelForKey(key) { updateModelForKey(key) {
const newValue = this.targets.filter((t) => t.key === key).map((t) => t.value); const newValue = this.targets.filter((t) => t.key === key).map((t) => t.value);
// Set the model value directly instead of using Array methods (like .addObject)
this.args.model[key] = newValue; this.args.model[key] = newValue;
} }
@@ -126,8 +126,18 @@ export default class MfaLoginEnforcementForm extends Component {
@action @action
async onMethodChange(selectedIds) { async onMethodChange(selectedIds) {
// first make sure the async relationship is loaded
const methods = await this.args.model.mfa_methods; const methods = await this.args.model.mfa_methods;
handleHasManySelection(selectedIds, methods, this.store, 'mfa-method'); // then remove items that are no longer selected
const updatedList = methods.filter((model) => {
return selectedIds.includes(model.id);
});
// then add selected items that don't exist in the list already
const modelIds = updatedList.map((model) => model.id);
const toAdd = selectedIds
.filter((id) => !modelIds.includes(id))
.map((id) => this.store.peekRecord('mfa-method', id));
this.args.model.mfa_methods = addManyToArray(updatedList, toAdd);
} }
@action @action
@@ -150,7 +160,7 @@ export default class MfaLoginEnforcementForm extends Component {
addTarget() { addTarget() {
const { label, key } = this.selectedTarget; const { label, key } = this.selectedTarget;
const value = this.selectedTargetValue; const value = this.selectedTargetValue;
this.targets.addObject({ label, value, key }); this.targets = addToArray(this.targets, { label, value, key });
// recalculate value for appropriate model property // recalculate value for appropriate model property
this.updateModelForKey(key); this.updateModelForKey(key);
this.selectedTargetValue = null; this.selectedTargetValue = null;
@@ -158,7 +168,7 @@ export default class MfaLoginEnforcementForm extends Component {
} }
@action @action
removeTarget(target) { removeTarget(target) {
this.targets.removeObject(target); this.targets = removeFromArray(this.targets, target);
// recalculate value for appropriate model property // recalculate value for appropriate model property
this.updateModelForKey(target.key); this.updateModelForKey(target.key);
} }

View File

@@ -195,6 +195,7 @@ export default class SecretCreateOrUpdate extends Component {
if (isBlank(item.name)) { if (isBlank(item.name)) {
return; return;
} }
// secretData is a KVObject/ArrayProxy so removeObject is fine here
data.removeObject(item); data.removeObject(item);
this.checkRows(); this.checkRows();
this.handleChange(); this.handleChange();

View File

@@ -9,6 +9,7 @@ import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object'; import { action } from '@ember/object';
import { capitalize } from '@ember/string'; import { capitalize } from '@ember/string';
import { task } from 'ember-concurrency'; import { task } from 'ember-concurrency';
import { addToArray } from 'vault/helpers/add-to-array';
export default class MfaMethodCreateController extends Controller { export default class MfaMethodCreateController extends Controller {
@service store; @service store;
@@ -95,7 +96,8 @@ export default class MfaMethodCreateController extends Controller {
// first save method // first save method
yield this.method.save(); yield this.method.save();
if (this.enforcement) { if (this.enforcement) {
this.enforcement.mfa_methods.addObject(this.method); // mfa_methods is type PromiseManyArray so slice in necessary to convert it to an Array
this.enforcement.mfa_methods = addToArray(this.enforcement.mfa_methods.slice(), this.method);
try { try {
// now save enforcement and catch error separately // now save enforcement and catch error separately
yield this.enforcement.save(); yield this.enforcement.save();

View File

@@ -10,7 +10,13 @@ function dedupe(items) {
return items.filter((v, i) => items.indexOf(v) === i); return items.filter((v, i) => items.indexOf(v) === i);
} }
export function addToArray([array, string]) { export function addManyToArray(array, otherArray) {
assert(`Both values must be an array`, Array.isArray(array) && Array.isArray(otherArray));
const newArray = [...array].concat(otherArray);
return dedupe(newArray);
}
export function addToArray(array, string) {
if (!Array.isArray(array)) { if (!Array.isArray(array)) {
assert(`Value provided is not an array`, false); assert(`Value provided is not an array`, false);
} }
@@ -19,4 +25,9 @@ export function addToArray([array, string]) {
return dedupe(newArray); return dedupe(newArray);
} }
export default buildHelper(addToArray); export default buildHelper(function ([array, string]) {
if (Array.isArray(string)) {
return addManyToArray(array, string);
}
return addToArray(array, string);
});

View File

@@ -10,10 +10,14 @@ function dedupe(items) {
return items.filter((v, i) => items.indexOf(v) === i); return items.filter((v, i) => items.indexOf(v) === i);
} }
export function removeFromArray([array, string]) { export function removeManyFromArray(array, toRemove) {
if (!Array.isArray(array)) { assert(`Both values must be an array`, Array.isArray(array) && Array.isArray(toRemove));
assert(`Value provided is not an array`, false); const a = [...(array || [])];
} return a.filter((v) => !toRemove.includes(v));
}
export function removeFromArray(array, string) {
assert(`Value provided is not an array`, Array.isArray(array));
const newArray = [...array]; const newArray = [...array];
const idx = newArray.indexOf(string); const idx = newArray.indexOf(string);
if (idx >= 0) { if (idx >= 0) {
@@ -22,4 +26,9 @@ export function removeFromArray([array, string]) {
return dedupe(newArray); return dedupe(newArray);
} }
export default buildHelper(removeFromArray); export default buildHelper(function ([array, string]) {
if (Array.isArray(string)) {
return removeManyFromArray(array, string);
}
return removeFromArray(array, string);
});

View File

@@ -241,7 +241,8 @@ export function shiftCommandIndex(keyCode: number, history: CommandLog[], index:
} }
if (newInputValue !== '') { if (newInputValue !== '') {
newInputValue = history.objectAt(index)?.content; const objAt = history[index];
newInputValue = objAt?.content;
} }
return [index, newInputValue]; return [index, newInputValue];

View File

@@ -8,6 +8,7 @@ import { computed } from '@ember/object';
import fieldToAttrs, { expandAttributeMeta } from 'vault/utils/field-to-attrs'; import fieldToAttrs, { expandAttributeMeta } from 'vault/utils/field-to-attrs';
import apiPath from 'vault/utils/api-path'; import apiPath from 'vault/utils/api-path';
import lazyCapabilities from 'vault/macros/lazy-capabilities'; import lazyCapabilities from 'vault/macros/lazy-capabilities';
import { removeManyFromArray } from 'vault/helpers/remove-from-array';
export const COMPUTEDS = { export const COMPUTEDS = {
operationFields: computed('newFields', function () { operationFields: computed('newFields', function () {
@@ -15,7 +16,7 @@ export const COMPUTEDS = {
}), }),
operationFieldsWithoutSpecial: computed('operationFields', function () { operationFieldsWithoutSpecial: computed('operationFields', function () {
return this.operationFields.slice().removeObjects(['operationAll', 'operationNone']); return removeManyFromArray(this.operationFields, ['operationAll', 'operationNone']);
}), }),
tlsFields: computed(function () { tlsFields: computed(function () {
@@ -25,12 +26,12 @@ export const COMPUTEDS = {
// For rendering on the create/edit pages // For rendering on the create/edit pages
defaultFields: computed('newFields', 'operationFields', 'tlsFields', function () { defaultFields: computed('newFields', 'operationFields', 'tlsFields', function () {
const excludeFields = ['role'].concat(this.operationFields, this.tlsFields); const excludeFields = ['role'].concat(this.operationFields, this.tlsFields);
return this.newFields.slice().removeObjects(excludeFields); return removeManyFromArray(this.newFields, excludeFields);
}), }),
// For adapter/serializer // For adapter/serializer
nonOperationFields: computed('newFields', 'operationFields', function () { nonOperationFields: computed('newFields', 'operationFields', function () {
return this.newFields.slice().removeObjects(this.operationFields); return removeManyFromArray(this.newFields, this.operationFields);
}), }),
}; };
@@ -64,9 +65,11 @@ export default Model.extend(COMPUTEDS, {
const attributes = ['operationAddAttribute', 'operationGetAttributes']; const attributes = ['operationAddAttribute', 'operationGetAttributes'];
const server = ['operationDiscoverVersions']; const server = ['operationDiscoverVersions'];
const others = this.operationFieldsWithoutSpecial const others = removeManyFromArray(this.operationFieldsWithoutSpecial, [
.slice() ...objects,
.removeObjects(objects.concat(attributes, server)); ...attributes,
...server,
]);
const groups = [ const groups = [
{ 'Managed Cryptographic Objects': objects }, { 'Managed Cryptographic Objects': objects },
{ 'Object Attributes': attributes }, { 'Object Attributes': attributes },

View File

@@ -10,6 +10,7 @@ import { methods } from 'vault/helpers/mountable-auth-methods';
import { withModelValidations } from 'vault/decorators/model-validations'; import { withModelValidations } from 'vault/decorators/model-validations';
import { isPresent } from '@ember/utils'; import { isPresent } from '@ember/utils';
import { service } from '@ember/service'; import { service } from '@ember/service';
import { addManyToArray, addToArray } from 'vault/helpers/add-to-array';
const validations = { const validations = {
name: [{ type: 'presence', message: 'Name is required' }], name: [{ type: 'presence', message: 'Name is required' }],
@@ -52,7 +53,7 @@ export default class MfaLoginEnforcementModel extends Model {
async prepareTargets() { async prepareTargets() {
let authMethods; let authMethods;
const targets = []; let targets = [];
if (this.auth_method_accessors.length || this.auth_method_types.length) { if (this.auth_method_accessors.length || this.auth_method_types.length) {
// fetch all auth methods and lookup by accessor to get mount path and type // fetch all auth methods and lookup by accessor to get mount path and type
@@ -68,7 +69,8 @@ export default class MfaLoginEnforcementModel extends Model {
const selectedAuthMethods = authMethods.filter((model) => { const selectedAuthMethods = authMethods.filter((model) => {
return this.auth_method_accessors.includes(model.accessor); return this.auth_method_accessors.includes(model.accessor);
}); });
targets.addObjects( targets = addManyToArray(
targets,
selectedAuthMethods.map((method) => ({ selectedAuthMethods.map((method) => ({
icon: this.iconForMount(method.type), icon: this.iconForMount(method.type),
link: 'vault.cluster.access.method', link: 'vault.cluster.access.method',
@@ -82,7 +84,7 @@ export default class MfaLoginEnforcementModel extends Model {
this.auth_method_types.forEach((type) => { this.auth_method_types.forEach((type) => {
const icon = this.iconForMount(type); const icon = this.iconForMount(type);
const mountCount = authMethods.filterBy('type', type).length; const mountCount = authMethods.filterBy('type', type).length;
targets.addObject({ targets = addToArray(targets, {
key: 'auth_method_types', key: 'auth_method_types',
icon, icon,
title: type, title: type,
@@ -92,7 +94,7 @@ export default class MfaLoginEnforcementModel extends Model {
for (const key of ['identity_entities', 'identity_groups']) { for (const key of ['identity_entities', 'identity_groups']) {
(await this[key]).forEach((model) => { (await this[key]).forEach((model) => {
targets.addObject({ targets = addToArray(targets, {
key, key,
icon: 'user', icon: 'user',
link: 'vault.cluster.access.identity.show', link: 'vault.cluster.access.identity.show',

View File

@@ -50,7 +50,7 @@ export default class SyncDestinationSerializer extends ApplicationSerializer {
const type = key.replace(/\/$/, ''); const type = key.replace(/\/$/, '');
const id = `${type}/${name}`; const id = `${type}/${name}`;
// create object with destination's id and attributes, add to payload // create object with destination's id and attributes, add to payload
transformedPayload.pushObject({ id, name, type }); transformedPayload.push({ id, name, type });
}); });
} }
return transformedPayload; return transformedPayload;

View File

@@ -17,6 +17,7 @@ import { resolve, reject } from 'rsvp';
import getStorage from 'vault/lib/token-storage'; import getStorage from 'vault/lib/token-storage';
import ENV from 'vault/config/environment'; import ENV from 'vault/config/environment';
import { allSupportedAuthBackends } from 'vault/helpers/supported-auth-backends'; import { allSupportedAuthBackends } from 'vault/helpers/supported-auth-backends';
import { addToArray } from 'vault/helpers/add-to-array';
const TOKEN_SEPARATOR = '☃'; const TOKEN_SEPARATOR = '☃';
const TOKEN_PREFIX = 'vault-'; const TOKEN_PREFIX = 'vault-';
@@ -250,7 +251,6 @@ export default Service.extend({
persistAuthData() { persistAuthData() {
const [firstArg, resp] = arguments; const [firstArg, resp] = arguments;
const tokens = this.tokens;
const currentNamespace = this.namespaceService.path || ''; const currentNamespace = this.namespaceService.path || '';
// dropdown vs tab format // dropdown vs tab format
const mountPath = firstArg?.data?.path || firstArg?.selectedAuth; const mountPath = firstArg?.data?.path || firstArg?.selectedAuth;
@@ -303,8 +303,7 @@ export default Service.extend({
if (!data.displayName) { if (!data.displayName) {
data.displayName = (this.getTokenData(tokenName) || {}).displayName; data.displayName = (this.getTokenData(tokenName) || {}).displayName;
} }
tokens.addObject(tokenName); this.set('tokens', addToArray(this.tokens, tokenName));
this.set('tokens', tokens);
this.set('allowExpiration', false); this.set('allowExpiration', false);
this.setTokenData(tokenName, data); this.setTokenData(tokenName, data);
return resolve({ return resolve({

View File

@@ -6,12 +6,12 @@
// Low level service that allows users to input paths to make requests to vault // Low level service that allows users to input paths to make requests to vault
// this service provides the UI synecdote to the cli commands read, write, delete, and list // this service provides the UI synecdote to the cli commands read, write, delete, and list
import Service from '@ember/service'; import Service from '@ember/service';
import { getOwner } from '@ember/application'; import { getOwner } from '@ember/application';
import { computed } from '@ember/object';
import { shiftCommandIndex } from 'vault/lib/console-helpers'; import { shiftCommandIndex } from 'vault/lib/console-helpers';
import { encodePath } from 'vault/utils/path-encoding-helpers'; import { encodePath } from 'vault/utils/path-encoding-helpers';
import { sanitizePath, ensureTrailingSlash } from 'core/utils/sanitize-path'; import { sanitizePath, ensureTrailingSlash } from 'core/utils/sanitize-path';
import { tracked } from '@glimmer/tracking';
import { addManyToArray } from 'vault/helpers/add-to-array';
const VERBS = { const VERBS = {
read: 'GET', read: 'GET',
@@ -20,51 +20,51 @@ const VERBS = {
delete: 'DELETE', delete: 'DELETE',
}; };
export default Service.extend({ export default class ConsoleService extends Service {
isOpen: false, @tracked isOpen = false;
@tracked commandIndex = null;
@tracked log = [];
adapter() {
return getOwner(this).lookup('adapter:console');
},
get commandHistory() { get commandHistory() {
return this.log.filter((log) => log.type === 'command'); return this.log.filter((log) => log.type === 'command');
}, }
log: computed(function () {
return []; // Not a getter so it can be stubbed in tests
}), adapter() {
commandIndex: null, return getOwner(this).lookup('adapter:console');
}
shiftCommandIndex(keyCode, setCommandFn = () => {}) { shiftCommandIndex(keyCode, setCommandFn = () => {}) {
const [newIndex, newCommand] = shiftCommandIndex(keyCode, this.commandHistory, this.commandIndex); const [newIndex, newCommand] = shiftCommandIndex(keyCode, this.commandHistory, this.commandIndex);
if (newCommand !== undefined && newIndex !== undefined) { if (newCommand !== undefined && newIndex !== undefined) {
this.set('commandIndex', newIndex); this.commandIndex = newIndex;
setCommandFn(newCommand); setCommandFn(newCommand);
} }
}, }
clearLog(clearAll = false) { clearLog(clearAll = false) {
const log = this.log;
let history; let history;
if (!clearAll) { if (!clearAll) {
history = this.commandHistory.slice(); history = this.commandHistory.slice();
history.setEach('hidden', true); history.setEach('hidden', true);
} }
log.clear(); this.log = [];
if (history) { if (history) {
log.addObjects(history); this.log = addManyToArray(this.log, history);
} }
}, }
logAndOutput(command, logContent) { logAndOutput(command, logContent) {
const log = this.log; const log = this.log.slice();
if (command) { if (command) {
log.pushObject({ type: 'command', content: command }); log.push({ type: 'command', content: command });
this.set('commandIndex', null); this.commandIndex = null;
} }
if (logContent) { if (logContent) {
log.pushObject(logContent); log.push(logContent);
} }
}, this.log = log;
}
ajax(operation, path, options = {}) { ajax(operation, path, options = {}) {
const verb = VERBS[operation]; const verb = VERBS[operation];
@@ -75,7 +75,7 @@ export default Service.extend({
data, data,
wrapTTL, wrapTTL,
}); });
}, }
kvGet(path, data, flags = {}) { kvGet(path, data, flags = {}) {
const { wrapTTL, metadata } = flags; const { wrapTTL, metadata } = flags;
@@ -84,21 +84,21 @@ export default Service.extend({
const [backend, secretPath] = path.split(/\/(.+)?/); const [backend, secretPath] = path.split(/\/(.+)?/);
const kvPath = `${backend}/${pathSegment}/${secretPath}`; const kvPath = `${backend}/${pathSegment}/${secretPath}`;
return this.ajax('read', sanitizePath(kvPath), { wrapTTL }); return this.ajax('read', sanitizePath(kvPath), { wrapTTL });
}, }
read(path, data, flags) { read(path, data, flags) {
const wrapTTL = flags?.wrapTTL; const wrapTTL = flags?.wrapTTL;
return this.ajax('read', sanitizePath(path), { wrapTTL }); return this.ajax('read', sanitizePath(path), { wrapTTL });
}, }
write(path, data, flags) { write(path, data, flags) {
const wrapTTL = flags?.wrapTTL; const wrapTTL = flags?.wrapTTL;
return this.ajax('write', sanitizePath(path), { data, wrapTTL }); return this.ajax('write', sanitizePath(path), { data, wrapTTL });
}, }
delete(path) { delete(path) {
return this.ajax('delete', sanitizePath(path)); return this.ajax('delete', sanitizePath(path));
}, }
list(path, data, flags) { list(path, data, flags) {
const wrapTTL = flags?.wrapTTL; const wrapTTL = flags?.wrapTTL;
@@ -109,5 +109,5 @@ export default Service.extend({
}, },
wrapTTL, wrapTTL,
}); });
}, }
}); }

View File

@@ -3,34 +3,35 @@
* SPDX-License-Identifier: BUSL-1.1 * SPDX-License-Identifier: BUSL-1.1
*/ */
/*eslint-disable no-constant-condition*/
import { computed } from '@ember/object';
import Service from '@ember/service'; import Service from '@ember/service';
import { tracked } from '@glimmer/tracking';
import { task, waitForEvent } from 'ember-concurrency'; import { task, waitForEvent } from 'ember-concurrency';
import { addToArray } from 'vault/helpers/add-to-array';
export default Service.extend({ export default class CspEventService extends Service {
events: computed(function () { @tracked connectionViolations = [];
return [];
}),
connectionViolations: computed('events.@each.violatedDirective', function () {
return this.events.some((e) => e.violatedDirective.startsWith('connect-src'));
}),
attach() { attach() {
this.monitor.perform(); this.monitor.perform();
}, }
remove() { remove() {
this.monitor.cancelAll(); this.monitor.cancelAll();
}, }
monitor: task(function* () { handleEvent(event) {
this.events.clear(); if (event.violatedDirective.startsWith('connect-src')) {
this.connectionViolations = addToArray(this.connectionViolations, event);
}
}
@task
*monitor() {
this.connectionViolations = [];
while (true) { while (true) {
const event = yield waitForEvent(window.document, 'securitypolicyviolation'); const event = yield waitForEvent(window.document, 'securitypolicyviolation');
this.events.addObject(event); this.handleEvent(event);
} }
}), }
}); }

View File

@@ -11,6 +11,7 @@ import { capitalize } from '@ember/string';
import getStorage from 'vault/lib/token-storage'; import getStorage from 'vault/lib/token-storage';
import { STORAGE_KEYS, DEFAULTS, MACHINES } from 'vault/helpers/wizard-constants'; import { STORAGE_KEYS, DEFAULTS, MACHINES } from 'vault/helpers/wizard-constants';
import { addToArray } from 'vault/helpers/add-to-array';
const { const {
TUTORIAL_STATE, TUTORIAL_STATE,
COMPONENT_STATE, COMPONENT_STATE,
@@ -101,7 +102,7 @@ export default Service.extend(DEFAULTS, {
} else { } else {
if (this.featureMachineHistory) { if (this.featureMachineHistory) {
if (!this.featureMachineHistory.includes(state)) { if (!this.featureMachineHistory.includes(state)) {
const newHistory = this.featureMachineHistory.addObject(state); const newHistory = addToArray(this.featureMachineHistory, state);
this.set('featureMachineHistory', newHistory); this.set('featureMachineHistory', newHistory);
} else { } else {
//we're repeating steps //we're repeating steps
@@ -293,7 +294,7 @@ export default Service.extend(DEFAULTS, {
return; return;
} }
this.startFeature(); this.startFeature();
const nextFeature = this.featureList.length > 1 ? capitalize(this.featureList.objectAt(1)) : 'Finish'; const nextFeature = this.featureList.length > 1 ? capitalize(this.featureList[1]) : 'Finish';
this.set('nextFeature', nextFeature); this.set('nextFeature', nextFeature);
let next; let next;
if (this.currentMachine === 'secrets' && this.featureState === 'display') { if (this.currentMachine === 'secrets' && this.featureState === 'display') {
@@ -311,9 +312,9 @@ export default Service.extend(DEFAULTS, {
}, },
startFeature() { startFeature() {
const FeatureMachineConfig = MACHINES[this.featureList.objectAt(0)]; const FeatureMachineConfig = MACHINES[this.featureList[0]];
FeatureMachine = Machine(FeatureMachineConfig); FeatureMachine = Machine(FeatureMachineConfig);
this.set('currentMachine', this.featureList.objectAt(0)); this.set('currentMachine', this.featureList[0]);
if (this.storageHasKey(FEATURE_STATE)) { if (this.storageHasKey(FEATURE_STATE)) {
this.saveState('featureState', this.getExtState(FEATURE_STATE)); this.saveState('featureState', this.getExtState(FEATURE_STATE));
} else { } else {
@@ -337,7 +338,7 @@ export default Service.extend(DEFAULTS, {
completed.push(done); completed.push(done);
this.saveExtState(COMPLETED_FEATURES, completed); this.saveExtState(COMPLETED_FEATURES, completed);
} else { } else {
this.saveExtState(COMPLETED_FEATURES, this.getExtState(COMPLETED_FEATURES).addObject(done)); this.saveExtState(COMPLETED_FEATURES, addToArray(this.getExtState(COMPLETED_FEATURES), done));
} }
this.saveExtState(FEATURE_LIST, features.length ? features : null); this.saveExtState(FEATURE_LIST, features.length ? features : null);

View File

@@ -50,7 +50,7 @@
</nav> </nav>
{{/if}} {{/if}}
<div class="box is-marginless is-shadowless"> <div class="box is-marginless is-shadowless">
<MessageError @errorMessage={{if (and this.cluster.standby this.hasCSPError) this.cspErrorText this.error}} /> <MessageError @errorMessage={{if (and this.cluster.standby this.cspError) this.cspError this.error}} />
{{#if this.selectedAuthBackend.path}} {{#if this.selectedAuthBackend.path}}
<div class="has-bottom-margin-s"> <div class="has-bottom-margin-s">
<p class="is-label">{{this.selectedAuthBackend.path}}</p> <p class="is-label">{{this.selectedAuthBackend.path}}</p>

View File

@@ -10,6 +10,8 @@ import { capitalize } from 'vault/helpers/capitalize';
import { humanize } from 'vault/helpers/humanize'; import { humanize } from 'vault/helpers/humanize';
import { dasherize } from 'vault/helpers/dasherize'; import { dasherize } from 'vault/helpers/dasherize';
import { assert } from '@ember/debug'; import { assert } from '@ember/debug';
import { addToArray } from 'vault/helpers/add-to-array';
import { removeFromArray } from 'vault/helpers/remove-from-array';
/** /**
* @module FormField * @module FormField
@@ -193,9 +195,12 @@ export default class FormFieldComponent extends Component {
@action @action
handleChecklist(event) { handleChecklist(event) {
const valueArray = this.args.model[this.valuePath]; let updatedValue = this.args.model[this.valuePath];
const method = event.target.checked ? 'addObject' : 'removeObject'; if (event.target.checked) {
valueArray[method](event.target.value); updatedValue = addToArray(updatedValue, event.target.value);
this.setAndBroadcast(valueArray); } else {
updatedValue = removeFromArray(updatedValue, event.target.value);
}
this.setAndBroadcast(updatedValue);
} }
} }

View File

@@ -40,6 +40,7 @@ import KVObject from 'vault/lib/kv-object';
*/ */
export default class KvObjectEditor extends Component { export default class KvObjectEditor extends Component {
// kvData is type ArrayProxy, so addObject etc are fine here
@tracked kvData; @tracked kvData;
get placeholders() { get placeholders() {

View File

@@ -7,6 +7,7 @@ import Component from '@glimmer/component';
import { action } from '@ember/object'; import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking'; import { tracked } from '@glimmer/tracking';
import { assert } from '@ember/debug'; import { assert } from '@ember/debug';
import { removeFromArray } from 'vault/helpers/remove-from-array';
/** /**
* @module ObjectListInput * @module ObjectListInput
@@ -65,7 +66,7 @@ export default class ObjectListInput extends Component {
@action @action
handleInput(idx, { target }) { handleInput(idx, { target }) {
const inputObj = this.inputList.objectAt(idx); const inputObj = this.inputList[idx];
inputObj[target.name] = target.value; inputObj[target.name] = target.value;
this.handleChange(); this.handleChange();
} }
@@ -79,8 +80,8 @@ export default class ObjectListInput extends Component {
@action @action
removeRow(idx) { removeRow(idx) {
const row = this.inputList.objectAt(idx); const row = this.inputList[idx];
this.inputList.removeObject(row); this.inputList = removeFromArray(this.inputList, row);
this.handleChange(); this.handleChange();
} }

View File

@@ -9,6 +9,8 @@ import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking'; import { tracked } from '@glimmer/tracking';
import { task } from 'ember-concurrency'; import { task } from 'ember-concurrency';
import { filterOptions, defaultMatcher } from 'ember-power-select/utils/group-utils'; import { filterOptions, defaultMatcher } from 'ember-power-select/utils/group-utils';
import { removeFromArray } from 'vault/helpers/remove-from-array';
import { addToArray } from 'vault/helpers/add-to-array';
/** /**
* @module SearchSelectWithModal * @module SearchSelectWithModal
@@ -31,7 +33,7 @@ import { filterOptions, defaultMatcher } from 'ember-power-select/utils/group-ut
* /> * />
* *
// * component functionality // * component functionality
* @param {function} onChange - The onchange action for this form field. ** SEE UTIL ** search-select-has-many.js if selecting models from a hasMany relationship * @param {function} onChange - The onchange action for this form field. ** SEE EXAMPLE ** mfa-login-enforcement-form.js (onMethodChange) for example when selecting models from a hasMany relationship
* @param {array} [inputValue] - Array of strings corresponding to the input's initial value, e.g. an array of model ids that on edit will appear as selected items below the input * @param {array} [inputValue] - Array of strings corresponding to the input's initial value, e.g. an array of model ids that on edit will appear as selected items below the input
* @param {boolean} [shouldRenderName=false] - By default an item's id renders in the dropdown, `true` displays the name with its id in smaller text beside it *NOTE: the boolean flips automatically with 'identity' models * @param {boolean} [shouldRenderName=false] - By default an item's id renders in the dropdown, `true` displays the name with its id in smaller text beside it *NOTE: the boolean flips automatically with 'identity' models
* @param {array} [excludeOptions] - array of strings containing model ids to filter from the dropdown (ex: ['allow_all']) * @param {array} [excludeOptions] - array of strings containing model ids to filter from the dropdown (ex: ['allow_all'])
@@ -81,7 +83,7 @@ export default class SearchSelectWithModal extends Component {
return inputValues.map((option) => { return inputValues.map((option) => {
const matchingOption = this.dropdownOptions.find((opt) => opt.id === option); const matchingOption = this.dropdownOptions.find((opt) => opt.id === option);
// remove any matches from dropdown list // remove any matches from dropdown list
this.dropdownOptions.removeObject(matchingOption); this.dropdownOptions = removeFromArray(this.dropdownOptions, matchingOption);
return { return {
id: option, id: option,
name: matchingOption ? matchingOption.name : option, name: matchingOption ? matchingOption.name : option,
@@ -168,8 +170,8 @@ export default class SearchSelectWithModal extends Component {
// ----- // -----
@action @action
discardSelection(selected) { discardSelection(selected) {
this.selectedOptions.removeObject(selected); this.selectedOptions = removeFromArray(this.selectedOptions, selected);
this.dropdownOptions.pushObject(selected); this.dropdownOptions = addToArray(this.dropdownOptions, selected);
this.handleChange(); this.handleChange();
} }
@@ -196,8 +198,8 @@ export default class SearchSelectWithModal extends Component {
this.showModal = true; this.showModal = true;
} else { } else {
// user has selected an existing item, handleChange immediately // user has selected an existing item, handleChange immediately
this.selectedOptions.pushObject(selection); this.selectedOptions = addToArray(this.selectedOptions, selection);
this.dropdownOptions.removeObject(selection); this.dropdownOptions = removeFromArray(this.dropdownOptions, selection);
this.handleChange(); this.handleChange();
} }
} }
@@ -209,7 +211,7 @@ export default class SearchSelectWithModal extends Component {
this.showModal = false; this.showModal = false;
if (model && model.currentState.isSaved) { if (model && model.currentState.isSaved) {
const { name } = model; const { name } = model;
this.selectedOptions.pushObject({ name, id: name }); this.selectedOptions = addToArray(this.selectedOptions, { name, id: name });
this.handleChange(); this.handleChange();
} }
this.nameInput = null; this.nameInput = null;

View File

@@ -10,6 +10,8 @@ import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking'; import { tracked } from '@glimmer/tracking';
import { resolve } from 'rsvp'; import { resolve } from 'rsvp';
import { filterOptions, defaultMatcher } from 'ember-power-select/utils/group-utils'; import { filterOptions, defaultMatcher } from 'ember-power-select/utils/group-utils';
import { removeFromArray } from 'vault/helpers/remove-from-array';
import { addToArray } from 'vault/helpers/add-to-array';
/** /**
* @module SearchSelect * @module SearchSelect
* The `SearchSelect` is an implementation of the [ember-power-select](https://github.com/cibernox/ember-power-select) used for form elements where options come dynamically from the API. * The `SearchSelect` is an implementation of the [ember-power-select](https://github.com/cibernox/ember-power-select) used for form elements where options come dynamically from the API.
@@ -28,7 +30,7 @@ import { filterOptions, defaultMatcher } from 'ember-power-select/utils/group-ut
* /> * />
* *
// * component functionality // * component functionality
* @param {function} onChange - The onchange action for this form field. ** SEE UTIL ** search-select-has-many.js if selecting models from a hasMany relationship * @param {function} onChange - The onchange action for this form field. ** SEE EXAMPLE ** mfa-login-enforcement-form.js (onMethodChange) for example when selecting models from a hasMany relationship
* @param {array} [inputValue] - Array of strings corresponding to the input's initial value, e.g. an array of model ids that on edit will appear as selected items below the input * @param {array} [inputValue] - Array of strings corresponding to the input's initial value, e.g. an array of model ids that on edit will appear as selected items below the input
* @param {boolean} [disallowNewItems=false] - Controls whether or not the user can add a new item if none found * @param {boolean} [disallowNewItems=false] - Controls whether or not the user can add a new item if none found
* @param {boolean} [shouldRenderName=false] - By default an item's id renders in the dropdown, `true` displays the name with its id in smaller text beside it *NOTE: the boolean flips automatically with 'identity' models or if this.idKey !== 'id' * @param {boolean} [shouldRenderName=false] - By default an item's id renders in the dropdown, `true` displays the name with its id in smaller text beside it *NOTE: the boolean flips automatically with 'identity' models or if this.idKey !== 'id'
@@ -118,7 +120,7 @@ export default class SearchSelect extends Component {
: false; : false;
// remove any matches from dropdown list // remove any matches from dropdown list
this.dropdownOptions.removeObject(matchingOption); this.dropdownOptions = removeFromArray(this.dropdownOptions, matchingOption);
return { return {
id: option, id: option,
name: matchingOption ? matchingOption[this.nameKey] : option, name: matchingOption ? matchingOption[this.nameKey] : option,
@@ -263,9 +265,9 @@ export default class SearchSelect extends Component {
@action @action
discardSelection(selected) { discardSelection(selected) {
this.selectedOptions.removeObject(selected); this.selectedOptions = removeFromArray(this.selectedOptions, selected);
if (!selected.new) { if (!selected.new) {
this.dropdownOptions.pushObject(selected); this.dropdownOptions = addToArray(this.dropdownOptions, selected);
} }
this.handleChange(); this.handleChange();
} }
@@ -291,10 +293,10 @@ export default class SearchSelect extends Component {
selectOrCreate(selection) { selectOrCreate(selection) {
if (selection && selection.__isSuggestion__) { if (selection && selection.__isSuggestion__) {
const name = selection.__value__; const name = selection.__value__;
this.selectedOptions.pushObject({ name, id: name, new: true }); this.selectedOptions = addToArray(this.selectedOptions, { name, id: name, new: true });
} else { } else {
this.selectedOptions.pushObject(selection); this.selectedOptions = addToArray(this.selectedOptions, selection);
this.dropdownOptions.removeObject(selection); this.dropdownOptions = removeFromArray(this.dropdownOptions, selection);
} }
this.handleChange(); this.handleChange();
} }

View File

@@ -10,6 +10,8 @@ import { action } from '@ember/object';
import { set } from '@ember/object'; import { set } from '@ember/object';
import { next } from '@ember/runloop'; import { next } from '@ember/runloop';
import { tracked } from '@glimmer/tracking'; import { tracked } from '@glimmer/tracking';
import { addToArray } from 'vault/helpers/add-to-array';
import { removeFromArray } from 'vault/helpers/remove-from-array';
/** /**
* @module StringList * @module StringList
@@ -33,6 +35,7 @@ export default class StringList extends Component {
constructor() { constructor() {
super(...arguments); super(...arguments);
// inputList is type ArrayProxy, so addObject etc are fine here
this.inputList = ArrayProxy.create({ this.inputList = ArrayProxy.create({
// trim the `value` when accessing objects // trim the `value` when accessing objects
content: [], content: [],
@@ -90,9 +93,11 @@ export default class StringList extends Component {
@action @action
inputChanged(idx, event) { inputChanged(idx, event) {
if (event.target.value.includes(',') && !this.indicesWithComma.includes(idx)) { if (event.target.value.includes(',') && !this.indicesWithComma.includes(idx)) {
this.indicesWithComma.pushObject(idx); this.indicesWithComma = addToArray(this.indicesWithComma, idx);
}
if (!event.target.value.includes(',')) {
this.indicesWithComma = removeFromArray(this.indicesWithComma, idx);
} }
if (!event.target.value.includes(',')) this.indicesWithComma.removeObject(idx);
const inputObj = this.inputList.objectAt(idx); const inputObj = this.inputList.objectAt(idx);
set(inputObj, 'value', event.target.value); set(inputObj, 'value', event.target.value);
@@ -109,8 +114,8 @@ export default class StringList extends Component {
@action @action
removeInput(idx) { removeInput(idx) {
const inputs = this.inputList; const itemToRemove = this.inputList.objectAt(idx);
inputs.removeObject(inputs.objectAt(idx)); this.inputList.removeObject(itemToRemove);
this.args.onChange(this.toVal()); this.args.onChange(this.toVal());
} }
} }

View File

@@ -1,38 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
/**
* Util to add/remove models in a hasMany relationship via the search-select component
*
* @example of using the util within an action in a component
*```js
* @action
* async onSearchSelectChange(selectedIds) {
* const methods = await this.args.model.mfa_methods;
* handleHasManySelection(selectedIds, methods, this.store, 'mfa-method');
* }
*
* @param selectedIds array of selected options from search-select component
* @param modelCollection array-like, list of models from the hasMany relationship
* @param store the store so we can call peekRecord()
* @param modelRecord string passed to peekRecord
*/
export default function handleHasManySelection(selectedIds, modelCollection, store, modelRecord) {
// first check for existing models that have been removed from selection
modelCollection.forEach((model) => {
if (!selectedIds.includes(model.id)) {
modelCollection.removeObject(model);
}
});
// now check for selected items that don't exist and add them to the model
const modelIds = modelCollection.map((model) => model.id);
selectedIds.forEach((id) => {
if (!modelIds.includes(id)) {
const model = store.peekRecord(modelRecord, id);
modelCollection.addObject(model);
}
});
}

View File

@@ -32,7 +32,6 @@ export default class RolesPageComponent extends Component {
try { try {
const message = `Successfully deleted role ${model.name}`; const message = `Successfully deleted role ${model.name}`;
await model.destroyRecord(); await model.destroyRecord();
this.args.roles.removeObject(model);
this.flashMessages.success(message); this.flashMessages.success(message);
} catch (error) { } catch (error) {
const message = errorMessage(error, 'Error deleting role. Please try again or contact support'); const message = errorMessage(error, 'Error deleting role. Please try again or contact support');

View File

@@ -18,6 +18,7 @@ import type PkiConfigClusterModel from 'vault/models/pki/config/cluster';
import type PkiConfigCrlModel from 'vault/models/pki/config/crl'; import type PkiConfigCrlModel from 'vault/models/pki/config/crl';
import type PkiConfigUrlsModel from 'vault/models/pki/config/urls'; import type PkiConfigUrlsModel from 'vault/models/pki/config/urls';
import type { FormField, TtlEvent } from 'vault/app-types'; import type { FormField, TtlEvent } from 'vault/app-types';
import { addToArray } from 'vault/helpers/add-to-array';
interface Args { interface Args {
acme: PkiConfigAcmeModel; acme: PkiConfigAcmeModel;
@@ -76,7 +77,7 @@ export default class PkiConfigurationEditComponent extends Component<Args> {
message: errorMessage(error), message: errorMessage(error),
}; };
this.flashMessages.danger(`Error updating config/${modelName}`, { sticky: true }); this.flashMessages.danger(`Error updating config/${modelName}`, { sticky: true });
this.errors.pushObject(errorObject); this.errors = addToArray(this.errors, errorObject);
} }
} }

View File

@@ -13,6 +13,8 @@ import errorMessage from 'vault/utils/error-message';
import type RouterService from '@ember/routing/router-service'; import type RouterService from '@ember/routing/router-service';
import type FlashMessageService from 'vault/services/flash-messages'; import type FlashMessageService from 'vault/services/flash-messages';
import type PkiIssuerModel from 'vault/models/pki/issuer'; import type PkiIssuerModel from 'vault/models/pki/issuer';
import { removeFromArray } from 'vault/helpers/remove-from-array';
import { addToArray } from 'vault/helpers/add-to-array';
interface Args { interface Args {
model: PkiIssuerModel; model: PkiIssuerModel;
@@ -36,8 +38,11 @@ export default class PkiIssuerEditComponent extends Component<Args> {
@action @action
setUsage(value: string) { setUsage(value: string) {
const method = this.usageValues.includes(value) ? 'removeObject' : 'addObject'; if (this.usageValues.includes(value)) {
this.usageValues[method](value); this.usageValues = removeFromArray(this.usageValues, value);
} else {
this.usageValues = addToArray(this.usageValues, value);
}
this.args.model.usage = this.usageValues.join(','); this.args.model.usage = this.usageValues.join(',');
} }

View File

@@ -11,6 +11,7 @@ import { tracked } from '@glimmer/tracking';
import errorMessage from 'vault/utils/error-message'; import errorMessage from 'vault/utils/error-message';
import { waitFor } from '@ember/test-waiters'; import { waitFor } from '@ember/test-waiters';
import { parseCertificate } from 'vault/utils/parse-pki-cert'; import { parseCertificate } from 'vault/utils/parse-pki-cert';
import { addToArray } from 'vault/helpers/add-to-array';
/** /**
* @module PkiIssuerCrossSign * @module PkiIssuerCrossSign
* PkiIssuerCrossSign components render from a parent issuer's details page to cross-sign an intermediate issuer (from a different mount). * PkiIssuerCrossSign components render from a parent issuer's details page to cross-sign an intermediate issuer (from a different mount).
@@ -92,7 +93,7 @@ export default class PkiIssuerCrossSign extends Component {
// for cross-signing error handling we want to record the list of issuers before the process starts // for cross-signing error handling we want to record the list of issuers before the process starts
this.intermediateIssuers[intermediateMount] = issuers; this.intermediateIssuers[intermediateMount] = issuers;
this.validationErrors.addObject({ this.validationErrors = addToArray(this.validationErrors, {
newCrossSignedIssuer: this.nameValidation(newCrossSignedIssuer, issuers), newCrossSignedIssuer: this.nameValidation(newCrossSignedIssuer, issuers),
}); });
} }
@@ -109,9 +110,9 @@ export default class PkiIssuerCrossSign extends Component {
intermediateIssuer, intermediateIssuer,
newCrossSignedIssuer newCrossSignedIssuer
); );
this.signedIssuers.addObject({ ...data, hasError: false }); this.signedIssuers = addToArray(this.signedIssuers, { ...data, hasError: false });
} catch (error) { } catch (error) {
this.signedIssuers.addObject({ this.signedIssuers = addToArray(this.signedIssuers, {
...this.formData[row], ...this.formData[row],
hasError: errorMessage(error), hasError: errorMessage(error),
hasUnsupportedParams: error.cause ? error.cause.map((e) => e.message).join(', ') : null, hasUnsupportedParams: error.cause ? error.cause.map((e) => e.message).join(', ') : null,

View File

@@ -38,7 +38,7 @@ export default function (server) {
let records = []; let records = [];
if (isMethod) { if (isMethod) {
methods.forEach((method) => { methods.forEach((method) => {
records.addObjects(schema.db[dbKeyFromType(method)].where({})); records = [...records, ...schema.db[dbKeyFromType(method)].where({})];
}); });
} else { } else {
records = schema.db.mfaLoginEnforcements.where({}); records = schema.db.mfaLoginEnforcements.where({});

View File

@@ -21,7 +21,7 @@ module('Acceptance | mfa-method', function (hooks) {
this.store = this.owner.lookup('service:store'); this.store = this.owner.lookup('service:store');
this.getMethods = () => this.getMethods = () =>
['Totp', 'Duo', 'Okta', 'Pingid'].reduce((methods, type) => { ['Totp', 'Duo', 'Okta', 'Pingid'].reduce((methods, type) => {
methods.addObjects(this.server.db[`mfa${type}Methods`].where({})); methods = [...methods, ...this.server.db[`mfa${type}Methods`].where({})];
return methods; return methods;
}, []); }, []);
return authPage.login(); return authPage.login();

View File

@@ -51,9 +51,9 @@ module('Integration | Component | auth form', function (hooks) {
assert.expect(2); assert.expect(2);
this.set('cluster', EmberObject.create({ standby: true })); this.set('cluster', EmberObject.create({ standby: true }));
this.set('selectedAuth', 'token'); this.set('selectedAuth', 'token');
await render(hbs`{{auth-form cluster=this.cluster selectedAuth=this.selectedAuth}}`); await render(hbs`<AuthForm @cluster={{this.cluster}} @selectedAuth={{this.selectedAuth}} />`);
assert.false(component.errorMessagePresent, false); assert.false(component.errorMessagePresent, false);
this.owner.lookup('service:csp-event').events.addObject({ violatedDirective: 'connect-src' }); this.owner.lookup('service:csp-event').handleEvent({ violatedDirective: 'connect-src' });
await settled(); await settled();
assert.strictEqual(component.errorText, CSP_ERR_TEXT); assert.strictEqual(component.errorText, CSP_ERR_TEXT);
}); });
@@ -75,7 +75,7 @@ module('Integration | Component | auth form', function (hooks) {
this.set('cluster', EmberObject.create({})); this.set('cluster', EmberObject.create({}));
this.set('selectedAuth', 'token'); this.set('selectedAuth', 'token');
await render(hbs`{{auth-form cluster=this.cluster selectedAuth=this.selectedAuth}}`); await render(hbs`<AuthForm @cluster={{this.cluster}} @selectedAuth={{this.selectedAuth}} />`);
return component.login().then(() => { return component.login().then(() => {
assert.strictEqual(component.errorText, 'Error Authentication failed: Not allowed'); assert.strictEqual(component.errorText, 'Error Authentication failed: Not allowed');
server.shutdown(); server.shutdown();
@@ -86,17 +86,20 @@ module('Integration | Component | auth form', function (hooks) {
assert.expect(1); assert.expect(1);
const server = new Pretender(function () { const server = new Pretender(function () {
this.get('/v1/auth/**', () => { this.get('/v1/auth/**', () => {
return [400, { 'Content-Type': 'application/json' }]; return [400, { 'Content-Type': 'application/json' }, JSON.stringify({ errors: ['API Error here'] })];
}); });
this.get('/v1/sys/internal/ui/mounts', this.passthrough); this.get('/v1/sys/internal/ui/mounts', this.passthrough);
}); });
this.set('cluster', EmberObject.create({})); this.set('cluster', EmberObject.create({}));
this.set('selectedAuth', 'token'); this.set('selectedAuth', 'token');
await render(hbs`{{auth-form cluster=this.cluster selectedAuth=this.selectedAuth}}`); await render(hbs`<AuthForm @cluster={{this.cluster}} @selectedAuth={{this.selectedAuth}} />`);
// returns null because test does not return details of failed network request. On the app it will return the details of the error instead of null.
return component.login().then(() => { return component.login().then(() => {
assert.strictEqual(component.errorText, 'Error Authentication failed: null'); assert.strictEqual(
component.errorText,
'Error Authentication failed: API Error here',
'shows the error from the API'
);
server.shutdown(); server.shutdown();
}); });
}); });
@@ -134,7 +137,7 @@ module('Integration | Component | auth form', function (hooks) {
}); });
this.set('cluster', EmberObject.create({})); this.set('cluster', EmberObject.create({}));
await render(hbs`{{auth-form cluster=this.cluster }}`); await render(hbs`<AuthForm @cluster={{this.cluster}} />`);
assert.strictEqual(component.tabs.length, 2, 'renders a tab for userpass and Other'); assert.strictEqual(component.tabs.length, 2, 'renders a tab for userpass and Other');
assert.strictEqual(component.tabs.objectAt(0).name, 'foo', 'uses the path in the label'); assert.strictEqual(component.tabs.objectAt(0).name, 'foo', 'uses the path in the label');
@@ -155,7 +158,7 @@ module('Integration | Component | auth form', function (hooks) {
}); });
}); });
this.set('cluster', EmberObject.create({})); this.set('cluster', EmberObject.create({}));
await render(hbs`{{auth-form cluster=this.cluster }}`); await render(hbs`<AuthForm @cluster={{this.cluster}} />`);
assert.strictEqual( assert.strictEqual(
component.descriptionText, component.descriptionText,
@@ -183,7 +186,7 @@ module('Integration | Component | auth form', function (hooks) {
this.set('cluster', EmberObject.create({})); this.set('cluster', EmberObject.create({}));
this.set('selectedAuth', 'foo/'); this.set('selectedAuth', 'foo/');
await render(hbs`{{auth-form cluster=this.cluster selectedAuth=this.selectedAuth}}`); await render(hbs`<AuthForm @cluster={{this.cluster}} @selectedAuth={{this.selectedAuth}} />`);
await component.login(); await component.login();
await settled(); await settled();
@@ -267,7 +270,7 @@ module('Integration | Component | auth form', function (hooks) {
}); });
this.set('wrappedToken', '54321'); this.set('wrappedToken', '54321');
await render(hbs`{{auth-form cluster=this.cluster wrappedToken=this.wrappedToken}}`); await render(hbs`<AuthForm @cluster={{this.cluster}} @wrappedToken={{this.wrappedToken}} />`);
later(() => cancelTimers(), 50); later(() => cancelTimers(), 50);
await settled(); await settled();

View File

@@ -147,7 +147,7 @@ module('Integration | Component | ldap | Page::Library::CreateAndEdit', function
await this.renderComponent(); await this.renderComponent();
await click('[data-test-string-list-button="delete"]'); await click('[data-test-string-list-row="0"] [data-test-string-list-button="delete"]');
await click('[data-test-input="disable_check_in_enforcement"] input#Disabled'); await click('[data-test-input="disable_check_in_enforcement"] input#Disabled');
await click('[data-test-save]'); await click('[data-test-save]');

View File

@@ -153,8 +153,8 @@ module('Integration | Component | mfa-login-enforcement-form', function (hooks)
test('it should populate fields with model data', async function (assert) { test('it should populate fields with model data', async function (assert) {
this.model.name = 'foo'; this.model.name = 'foo';
const [method] = await this.store.query('mfa-method', {}); const [method] = await this.store.query('mfa-method', {});
this.model.mfa_methods.addObject(method); this.model.mfa_methods = [method];
this.model.auth_method_accessors.addObject('auth_userpass_1234'); this.model.auth_method_accessors = ['auth_userpass_1234'];
await render(hbs` await render(hbs`
<Mfa::MfaLoginEnforcementForm <Mfa::MfaLoginEnforcementForm
@@ -207,12 +207,12 @@ module('Integration | Component | mfa-login-enforcement-form', function (hooks)
keys: ['1234'], keys: ['1234'],
}, },
})); }));
this.model.auth_method_accessors.addObject('auth_userpass_1234'); this.model.auth_method_accessors = ['auth_userpass_1234'];
this.model.auth_method_types.addObject('userpass'); this.model.auth_method_types = ['userpass'];
const [entity] = await this.store.query('identity/entity', {}); const [entity] = await this.store.query('identity/entity', {});
this.model.identity_entities.addObject(entity); this.model.identity_entities = [entity];
const [group] = await this.store.query('identity/group', {}); const [group] = await this.store.query('identity/group', {});
this.model.identity_groups.addObject(group); this.model.identity_groups = [group];
await render(hbs` await render(hbs`
<Mfa::MfaLoginEnforcementForm <Mfa::MfaLoginEnforcementForm

View File

@@ -159,6 +159,7 @@ module('Integration | Component | path filter config list', function (hooks) {
await clickTrigger(); await clickTrigger();
await searchSelect.deleteButtons.objectAt(1).click(); await searchSelect.deleteButtons.objectAt(1).click();
await clickTrigger(); await clickTrigger();
await typeInSearch('ns1');
assert.dom('.ember-power-select-group').hasText('Namespaces ns1', 'puts ns back within group'); assert.dom('.ember-power-select-group').hasText('Namespaces ns1', 'puts ns back within group');
await clickTrigger(); await clickTrigger();
}); });

View File

@@ -12,7 +12,7 @@ module('Integration | Helper | add-to-array', function (hooks) {
test('it correctly adds a value to an array without mutating the original', function (assert) { test('it correctly adds a value to an array without mutating the original', function (assert) {
const ARRAY = ['horse', 'cow', 'chicken']; const ARRAY = ['horse', 'cow', 'chicken'];
const result = addToArray([ARRAY, 'pig']); const result = addToArray(ARRAY, 'pig');
assert.deepEqual(result, [...ARRAY, 'pig'], 'Result has additional item'); assert.deepEqual(result, [...ARRAY, 'pig'], 'Result has additional item');
assert.deepEqual(ARRAY, ['horse', 'cow', 'chicken'], 'original array is not mutated'); assert.deepEqual(ARRAY, ['horse', 'cow', 'chicken'], 'original array is not mutated');
}); });
@@ -20,7 +20,7 @@ module('Integration | Helper | add-to-array', function (hooks) {
test('it fails if the first value is not an array', function (assert) { test('it fails if the first value is not an array', function (assert) {
let result; let result;
try { try {
result = addToArray(['not-array', 'string']); result = addToArray('not-array', 'string');
} catch (e) { } catch (e) {
result = e.message; result = e.message;
} }
@@ -29,13 +29,13 @@ module('Integration | Helper | add-to-array', function (hooks) {
test('it works with non-string arrays', function (assert) { test('it works with non-string arrays', function (assert) {
const ARRAY = ['five', 6, '7']; const ARRAY = ['five', 6, '7'];
const result = addToArray([ARRAY, 10]); const result = addToArray(ARRAY, 10);
assert.deepEqual(result, ['five', 6, '7', 10], 'added number value'); assert.deepEqual(result, ['five', 6, '7', 10], 'added number value');
}); });
test('it de-dupes the result', function (assert) { test('it de-dupes the result', function (assert) {
const ARRAY = ['horse', 'cow', 'chicken']; const ARRAY = ['horse', 'cow', 'chicken'];
const result = addToArray([ARRAY, 'horse']); const result = addToArray(ARRAY, 'horse');
assert.deepEqual(result, ['horse', 'cow', 'chicken']); assert.deepEqual(result, ['horse', 'cow', 'chicken']);
}); });
}); });

View File

@@ -5,28 +5,28 @@
import { module, test } from 'qunit'; import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit'; import { setupRenderingTest } from 'ember-qunit';
import { removeFromArray } from '../../../helpers/remove-from-array'; import { removeManyFromArray, removeFromArray } from 'vault/helpers/remove-from-array';
module('Integration | Helper | remove-from-array', function (hooks) { module('Integration | Helper | remove-from-array', function (hooks) {
setupRenderingTest(hooks); setupRenderingTest(hooks);
test('it correctly removes a value from an array without mutating the original', function (assert) { test('it correctly removes a value from an array without mutating the original', function (assert) {
const ARRAY = ['horse', 'cow', 'chicken']; const ARRAY = ['horse', 'cow', 'chicken'];
const result = removeFromArray([ARRAY, 'horse']); const result = removeFromArray(ARRAY, 'horse');
assert.deepEqual(result, ['cow', 'chicken'], 'Result does not have removed item'); assert.deepEqual(result, ['cow', 'chicken'], 'Result does not have removed item');
assert.deepEqual(ARRAY, ['horse', 'cow', 'chicken'], 'original array is not mutated'); assert.deepEqual(ARRAY, ['horse', 'cow', 'chicken'], 'original array is not mutated');
}); });
test('it returns the same value if the item is not found', function (assert) { test('it returns the same value if the item is not found', function (assert) {
const ARRAY = ['horse', 'cow', 'chicken']; const ARRAY = ['horse', 'cow', 'chicken'];
const result = removeFromArray([ARRAY, 'pig']); const result = removeFromArray(ARRAY, 'pig');
assert.deepEqual(result, ARRAY, 'Results are the same as original array'); assert.deepEqual(result, ARRAY, 'Results are the same as original array');
}); });
test('it fails if the first value is not an array', function (assert) { test('it fails if the first value is not an array', function (assert) {
let result; let result;
try { try {
result = removeFromArray(['not-array', 'string']); result = removeFromArray('not-array', 'string');
} catch (e) { } catch (e) {
result = e.message; result = e.message;
} }
@@ -35,15 +35,23 @@ module('Integration | Helper | remove-from-array', function (hooks) {
test('it works with non-string arrays', function (assert) { test('it works with non-string arrays', function (assert) {
const ARRAY = ['five', 6, '7']; const ARRAY = ['five', 6, '7'];
const result1 = removeFromArray([ARRAY, 6]); const result1 = removeFromArray(ARRAY, 6);
const result2 = removeFromArray([ARRAY, 7]); const result2 = removeFromArray(ARRAY, 7);
assert.deepEqual(result1, ['five', '7'], 'removed number value'); assert.deepEqual(result1, ['five', '7'], 'removed number value');
assert.deepEqual(result2, ARRAY, 'did not match on different types'); assert.deepEqual(result2, ARRAY, 'did not match on different types');
}); });
test('it de-dupes the result', function (assert) { test('it de-dupes the result', function (assert) {
const ARRAY = ['horse', 'cow', 'chicken', 'cow']; const ARRAY = ['horse', 'cow', 'chicken', 'cow'];
const result = removeFromArray([ARRAY, 'horse']); const result = removeFromArray(ARRAY, 'horse');
assert.deepEqual(result, ['cow', 'chicken']); assert.deepEqual(result, ['cow', 'chicken']);
}); });
test('it works with two arrays', function (assert) {
const ARRAY = ['five', 6, '7'];
const result1 = removeManyFromArray(ARRAY, [6, '7']);
const result2 = removeManyFromArray(ARRAY, ['foo', 'five']);
assert.deepEqual(result1, ['five'], 'removed multiple values');
assert.deepEqual(result2, [6, '7'], 'did nothing with values that were not in the array');
});
}); });