Merge Boundary UI OSS branch 'main' at commit f2c0458f
This commit is contained in:
commit
fc642bfc6a
97 files changed
+1748
-1740
No files matched your search
@@ -0,0 +1,8 @@
|
||||
import ModelAbility from './model';
|
||||
|
||||
/**
|
||||
* Provides abilities for managed groups.
|
||||
*/
|
||||
export default class ManagedGroupAbility extends ModelAbility {
|
||||
// no op
|
||||
}
|
||||
@@ -1,9 +1,18 @@
|
||||
import ModelAbility from './model';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
class InvalidRolePrincipalTypeError extends Error {
|
||||
name = 'InvalidRolePrincipalTypeError';
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides abilities for roles.
|
||||
*/
|
||||
export default class RoleAbility extends ModelAbility {
|
||||
// =services
|
||||
|
||||
@service can;
|
||||
|
||||
// =permissions
|
||||
|
||||
/**
|
||||
@@ -26,4 +35,22 @@ export default class RoleAbility extends ModelAbility {
|
||||
get canRemovePrincipals() {
|
||||
return this.hasAuthorizedAction('remove-principals');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ability check for a principal model instance.
|
||||
* Valid role principal models are: User, Group, and ManagedGroup.
|
||||
* Throws an error if the principal type is invalid.
|
||||
* @type {boolean}
|
||||
* @throws {InvalidRolePrincipalTypeError}
|
||||
*/
|
||||
get canReadPrincipal() {
|
||||
const type = this.model.constructor.modelName;
|
||||
const typeIsValid =
|
||||
type === 'user' || type === 'group' || type === 'managed-group';
|
||||
if (!typeIsValid)
|
||||
throw new InvalidRolePrincipalTypeError(
|
||||
`Expected a role principal of type 'user', 'group', or 'managed-group'. Got '${type}'.`
|
||||
);
|
||||
return this.can.can(`read ${type}`, this.model);
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import GeneratedCredentialModel from '../generated/models/credential';
|
||||
/**
|
||||
* Supported Credential types.
|
||||
*/
|
||||
export const types = ['username_password', 'ssh_private_key'];
|
||||
export const types = ['username_password', 'ssh_private_key', 'json'];
|
||||
|
||||
export default class CredentialModel extends GeneratedCredentialModel {
|
||||
// =attributes
|
||||
@@ -18,6 +18,14 @@ export default class CredentialModel extends GeneratedCredentialModel {
|
||||
return this.id.startsWith('cred');
|
||||
}
|
||||
|
||||
/**
|
||||
* True if type is `json`.
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isJSON() {
|
||||
return this.type === 'json';
|
||||
}
|
||||
|
||||
/**
|
||||
* True if credential is an unknown type.
|
||||
* @type {boolean}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import GeneratedHostCatalogModel from '../generated/models/host-catalog';
|
||||
|
||||
export const types = ['aws', 'azure'];
|
||||
export const types = ['static', 'plugin'];
|
||||
export const pluginTypes = ['aws', 'azure'];
|
||||
|
||||
export default class HostCatalogModel extends GeneratedHostCatalogModel {
|
||||
// =attributes
|
||||
@@ -26,7 +27,7 @@ export default class HostCatalogModel extends GeneratedHostCatalogModel {
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isUnknown() {
|
||||
return this.isPlugin && !types.includes(this.plugin?.name);
|
||||
return this.isPlugin && !pluginTypes.includes(this.plugin?.name);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import GeneratedHostSetModel from '../generated/models/host-set';
|
||||
import { types } from './host-catalog';
|
||||
import { pluginTypes } from './host-catalog';
|
||||
|
||||
export default class HostSetModel extends GeneratedHostSetModel {
|
||||
// =attributes
|
||||
@@ -25,7 +25,7 @@ export default class HostSetModel extends GeneratedHostSetModel {
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isUnknown() {
|
||||
return this.isPlugin && !types.includes(this.plugin?.name);
|
||||
return this.isPlugin && !pluginTypes.includes(this.plugin?.name);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import GeneratedRoleModel from '../generated/models/role';
|
||||
import { attr } from '@ember-data/model';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { all, resolve } from 'rsvp';
|
||||
|
||||
export default class RoleModel extends GeneratedRoleModel {
|
||||
// =services
|
||||
|
||||
@service store;
|
||||
@service resourceFilterStore;
|
||||
|
||||
// =attributes
|
||||
|
||||
@@ -37,6 +39,123 @@ export default class RoleModel extends GeneratedRoleModel {
|
||||
: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* A list of IDs for principals of type `user`.
|
||||
* @type {string[]}
|
||||
*/
|
||||
get userIDs() {
|
||||
return this.principals
|
||||
.filter(({ type }) => type === 'user')
|
||||
.map(({ principal_id }) => principal_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* A list of IDs for principals of type `group`.
|
||||
* @type {string[]}
|
||||
*/
|
||||
get groupIDs() {
|
||||
return this.principals
|
||||
.filter(({ type }) => type === 'group')
|
||||
.map(({ principal_id }) => principal_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* A list of IDs for principals of type `managed group`.
|
||||
* @type {string[]}
|
||||
*/
|
||||
get managedGroupIDs() {
|
||||
return this.principals
|
||||
.filter(({ type }) => type === 'managed group')
|
||||
.map(({ principal_id }) => principal_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* A promise that resolves to an array of User instances.
|
||||
* When calling this getter, be sure to await resolution
|
||||
* before interacting with the results.
|
||||
* @type {Promise[UserModel]}
|
||||
*/
|
||||
get users() {
|
||||
const ids = this.userIDs;
|
||||
|
||||
// Role has prinicipal IDs,
|
||||
// return a promise which resolves model instances for those IDs
|
||||
if (ids?.length) {
|
||||
return this.resourceFilterStore
|
||||
.queryBy('user', { id: ids }, { scope_id: 'global', recursive: true })
|
||||
.then((models) => models.map((model) => model));
|
||||
}
|
||||
|
||||
// No principal IDs,
|
||||
// return a promise resolving to an empty array
|
||||
return resolve([]);
|
||||
}
|
||||
|
||||
/**
|
||||
* A promise that resolves to an array of Group instances.
|
||||
* When calling this getter, be sure to await resolution
|
||||
* before interacting with the results.
|
||||
* @type {Promise[GroupModel]}
|
||||
*/
|
||||
get groups() {
|
||||
const ids = this.groupIDs;
|
||||
|
||||
// Role has prinicipal IDs,
|
||||
// return a promise which resolves model instances for those IDs
|
||||
if (ids?.length) {
|
||||
return this.resourceFilterStore
|
||||
.queryBy('group', { id: ids }, { scope_id: 'global', recursive: true })
|
||||
.then((models) => models.map((model) => model));
|
||||
}
|
||||
|
||||
// No principal IDs,
|
||||
// return a promise resolving to an empty array
|
||||
return resolve([]);
|
||||
}
|
||||
|
||||
/**
|
||||
* A promise that resolves to an array of ManagedGroup instances.
|
||||
* When calling this getter, be sure to await resolution
|
||||
* before interacting with the results.
|
||||
* @type {Promise[ManagedGroupModel]}
|
||||
*/
|
||||
get managedGroups() {
|
||||
const ids = this.managedGroupIDs;
|
||||
|
||||
// Role has prinicipal IDs
|
||||
// return a promise which resolves model instances for those IDs
|
||||
if (ids?.length) {
|
||||
// Collect all auth methods.
|
||||
const authMethods = this.resourceFilterStore.queryBy(
|
||||
'auth-method',
|
||||
{ type: 'oidc' },
|
||||
{ scope_id: 'global', recursive: true }
|
||||
);
|
||||
|
||||
// For each auth method, query all managed groups with IDs
|
||||
// that match the role's manage group principal IDs.
|
||||
const managedGroups = authMethods
|
||||
.then((methods) =>
|
||||
all(
|
||||
methods.map(({ id: auth_method_id }) =>
|
||||
this.resourceFilterStore
|
||||
.queryBy('managed-group', { id: ids }, { auth_method_id })
|
||||
.then((models) => models.map((model) => model))
|
||||
)
|
||||
)
|
||||
)
|
||||
// The result is an array of arrays of model instances (grouped by
|
||||
// auth methods), so these must be flattened.
|
||||
.then((grouped) => grouped.flat());
|
||||
|
||||
return managedGroups;
|
||||
}
|
||||
|
||||
// No principal IDs,
|
||||
// return a promise resolving to an empty array
|
||||
return resolve([]);
|
||||
}
|
||||
|
||||
// =methods
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,7 +2,9 @@ import GeneratedTargetModel from '../generated/models/target';
|
||||
import { attr } from '@ember-data/model';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export const types = ['tcp', 'ssh'];
|
||||
export const TYPE_TARGET_TCP = 'tcp';
|
||||
export const TYPE_TARGET_SSH = 'ssh';
|
||||
export const TYPES_TARGET = Object.freeze([TYPE_TARGET_TCP, TYPE_TARGET_SSH]);
|
||||
|
||||
export default class TargetModel extends GeneratedTargetModel {
|
||||
// =services
|
||||
@@ -321,7 +323,7 @@ export default class TargetModel extends GeneratedTargetModel {
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isTCP() {
|
||||
return this.type === 'tcp';
|
||||
return this.type === TYPE_TARGET_TCP;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -329,6 +331,6 @@ export default class TargetModel extends GeneratedTargetModel {
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isSSH() {
|
||||
return this.type === 'ssh';
|
||||
return this.type === TYPE_TARGET_SSH;
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,9 @@ export default class CredentialSerializer extends ApplicationSerializer {
|
||||
delete serialized.attributes.username;
|
||||
|
||||
if (serialized?.attributes?.json_object) {
|
||||
serialized.attributes.object = JSON.parse(serialized.attributes.json_object);
|
||||
serialized.attributes.object = JSON.parse(
|
||||
serialized.attributes.json_object
|
||||
);
|
||||
delete serialized.attributes.json_object;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'api/abilities/managed-group';
|
||||
@@ -267,14 +267,17 @@ export default function () {
|
||||
this.del('/roles/:id');
|
||||
this.post(
|
||||
'/roles/:idMethod',
|
||||
function ({ roles, users, groups }, { params: { idMethod } }) {
|
||||
function (
|
||||
{ roles, users, groups, managedGroups },
|
||||
{ params: { idMethod } }
|
||||
) {
|
||||
const attrs = this.normalizedRequestAttrs();
|
||||
const id = idMethod.split(':')[0];
|
||||
const method = idMethod.split(':')[1];
|
||||
const role = roles.find(id);
|
||||
let updatedAttrs = {};
|
||||
|
||||
// Principals is a combined list of users and groups, but in Mirage we've
|
||||
// Principals is a combined list of users, groups and managed groups, but in Mirage we've
|
||||
// internally modelled them as separate lists. Therefore we must check
|
||||
// the type by looking up the user or group first, then determine which
|
||||
// list to add the principal to.
|
||||
@@ -283,25 +286,31 @@ export default function () {
|
||||
version: attrs.version,
|
||||
userIds: role.userIds,
|
||||
groupIds: role.groupIds,
|
||||
managedGroupIds: role.managedGroupIds,
|
||||
};
|
||||
attrs.principalIds.forEach((id) => {
|
||||
const isUser = users.find(id);
|
||||
const isGroup = groups.find(id);
|
||||
const isManagedGroup = managedGroups.find(id);
|
||||
if (isUser && !updatedAttrs.userIds.includes(id)) {
|
||||
updatedAttrs.userIds.push(id);
|
||||
}
|
||||
if (isGroup && !updatedAttrs.groupIds.includes(id)) {
|
||||
updatedAttrs.groupIds.push(id);
|
||||
}
|
||||
if (isManagedGroup && !updatedAttrs.managedGroupIds.includes(id)) {
|
||||
updatedAttrs.managedGroupIds.push(id);
|
||||
}
|
||||
});
|
||||
}
|
||||
// If deleting principals, filter them out of both users and groups,
|
||||
// If deleting principals, filter the ids out of users, groups and managed group lists,
|
||||
// and in this case don't care about the type
|
||||
if (method === 'remove-principals') {
|
||||
updatedAttrs = {
|
||||
version: attrs.version,
|
||||
userIds: role.userIds,
|
||||
groupIds: role.groupIds,
|
||||
managedGroupIds: role.managedGroupIds,
|
||||
};
|
||||
updatedAttrs.userIds = updatedAttrs.userIds.filter((id) => {
|
||||
return !attrs.principalIds.includes(id);
|
||||
@@ -309,6 +318,11 @@ export default function () {
|
||||
updatedAttrs.groupIds = updatedAttrs.groupIds.filter((id) => {
|
||||
return !attrs.principalIds.includes(id);
|
||||
});
|
||||
updatedAttrs.managedGroupIds = updatedAttrs.managedGroupIds.filter(
|
||||
(id) => {
|
||||
return !attrs.principalIds.includes(id);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (method === 'set-grants') {
|
||||
@@ -580,8 +594,17 @@ export default function () {
|
||||
// managed-groups
|
||||
this.get(
|
||||
'/managed-groups',
|
||||
({ managedGroups }, { queryParams: { auth_method_id: authMethodId } }) => {
|
||||
return managedGroups.where({ authMethodId });
|
||||
(
|
||||
{ managedGroups },
|
||||
{ queryParams: { auth_method_id: authMethodId, filter } }
|
||||
) => {
|
||||
let resultSet;
|
||||
if (authMethodId) {
|
||||
resultSet = managedGroups.where({ authMethodId });
|
||||
} else {
|
||||
resultSet = managedGroups.all();
|
||||
}
|
||||
return resultSet.filter(makeBooleanFilter(filter));
|
||||
}
|
||||
);
|
||||
this.post('/managed-groups');
|
||||
|
||||
@@ -28,9 +28,13 @@ export default factory.extend({
|
||||
withPrincipals: trait({
|
||||
afterCreate(role, server) {
|
||||
const { scope } = role;
|
||||
const { id: scopeId } = scope;
|
||||
const users = server.createList('user', 2, { scope });
|
||||
const groups = server.createList('group', 2, { scope });
|
||||
role.update({ users, groups });
|
||||
const managedGroups = server.schema.managedGroups.where({
|
||||
scopeId,
|
||||
}).models;
|
||||
role.update({ users, groups, managedGroups });
|
||||
},
|
||||
}),
|
||||
|
||||
|
||||
@@ -3,10 +3,11 @@ import { trait } from 'ember-cli-mirage';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import permissions from '../helpers/permissions';
|
||||
import generateId from '../helpers/id';
|
||||
import { TYPES_TARGET } from 'api/models/target';
|
||||
|
||||
const randomBoolean = (chance = 0.5) => Math.random() < chance;
|
||||
const hostSetChance = 0.3;
|
||||
const types = ['tcp', 'ssh'];
|
||||
const types = [...TYPES_TARGET];
|
||||
|
||||
export default factory.extend({
|
||||
authorized_actions: () =>
|
||||
@@ -35,13 +36,11 @@ export default factory.extend({
|
||||
* Generates attributes fields by type.
|
||||
*/
|
||||
afterCreate(target) {
|
||||
if (target.type === 'tcp' || target.type === 'ssh') {
|
||||
target.update({
|
||||
attributes: {
|
||||
default_port: faker.datatype.number(),
|
||||
},
|
||||
});
|
||||
}
|
||||
target.update({
|
||||
attributes: {
|
||||
default_port: faker.datatype.number(),
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Factory } from 'ember-cli-mirage';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { TYPE_TARGET_TCP } from 'api/models/target';
|
||||
|
||||
/**
|
||||
* GeneratedTargetModelFactory
|
||||
*/
|
||||
export default Factory.extend({
|
||||
type: 'tcp',
|
||||
type: TYPE_TARGET_TCP,
|
||||
name: () => faker.random.words(),
|
||||
session_max_seconds: () => faker.datatype.number(),
|
||||
session_connection_limit: () => faker.datatype.number(),
|
||||
|
||||
@@ -5,4 +5,5 @@ export default Model.extend({
|
||||
scope: belongsTo(),
|
||||
users: hasMany(),
|
||||
groups: hasMany(),
|
||||
managedGroups: hasMany(),
|
||||
});
|
||||
@@ -20,7 +20,7 @@ export default function (server) {
|
||||
// Auth
|
||||
const globalAuthMethods = server.createList(
|
||||
'auth-method',
|
||||
1,
|
||||
5,
|
||||
{ scope: globalScope },
|
||||
'withAccountsAndUsersAndManagedGroups'
|
||||
);
|
||||
@@ -51,8 +51,43 @@ export default function (server) {
|
||||
server.createList('group', 1, { scope: globalScope }, 'withMembers');
|
||||
server.createList('group', 5, { scope: orgScope }, 'withMembers');
|
||||
// Role
|
||||
server.createList('role', 1, { scope: globalScope }, 'withPrincipals');
|
||||
server.createList('role', 5, { scope: orgScope }, 'withPrincipals');
|
||||
const globalScopeRoles = server.createList(
|
||||
'role',
|
||||
1,
|
||||
{ scope: globalScope },
|
||||
'withPrincipals'
|
||||
);
|
||||
//create managed groups for role/principals in globalScope
|
||||
globalScopeRoles.forEach((role) => {
|
||||
const { scope } = role;
|
||||
const oidcAuthMethod = globalAuthMethods.filter(
|
||||
(auth) => auth.type === 'oidc'
|
||||
)[0];
|
||||
const managedGroups = server.createList('managed-group', 2, {
|
||||
scope,
|
||||
authMethodId: oidcAuthMethod.id,
|
||||
});
|
||||
role.update({ managedGroups });
|
||||
});
|
||||
|
||||
const orgScopeRoles = server.createList(
|
||||
'role',
|
||||
5,
|
||||
{ scope: orgScope },
|
||||
'withPrincipals'
|
||||
);
|
||||
//create managed groups for role/principals in orgScope
|
||||
orgScopeRoles.forEach((role) => {
|
||||
const { scope } = role;
|
||||
const oidcAuthMethod = orgAuthMethods.filter(
|
||||
(auth) => auth.type === 'oidc'
|
||||
)[0];
|
||||
const managedGroups = server.createList('managed-group', 2, {
|
||||
scope,
|
||||
authMethodId: oidcAuthMethod.id,
|
||||
});
|
||||
role.update({ managedGroups });
|
||||
});
|
||||
|
||||
// Other resources
|
||||
server.schema.scopes.where({ type: 'project' }).models.forEach((scope) => {
|
||||
|
||||
@@ -18,7 +18,19 @@ export default ApplicationSerializer.extend({
|
||||
id: group.id,
|
||||
type: 'group',
|
||||
}));
|
||||
json.principals = [...serializedUsers, ...serializedGroups];
|
||||
const serializedManagedGroups = model.managedGroups.models.map(
|
||||
(managedGroup) => ({
|
||||
scope_id: model.scope.id,
|
||||
id: managedGroup.id,
|
||||
type: 'managed group',
|
||||
})
|
||||
);
|
||||
json.principals = [
|
||||
...serializedUsers,
|
||||
...serializedGroups,
|
||||
...serializedManagedGroups,
|
||||
];
|
||||
|
||||
if (!json.principals.length) delete json.principals;
|
||||
|
||||
// default grant scope
|
||||
|
||||
@@ -10,9 +10,9 @@ module('Unit | Abilities | Role', function (hooks) {
|
||||
const model = {
|
||||
authorized_actions: ['set-grants'],
|
||||
};
|
||||
assert.ok(service.can('setGrants role', model));
|
||||
assert.true(service.can('setGrants role', model));
|
||||
model.authorized_actions = [];
|
||||
assert.notOk(service.can('setGrants role', model));
|
||||
assert.false(service.can('setGrants role', model));
|
||||
});
|
||||
|
||||
test('it reflects when a given role may add principals based on authorized_actions', function (assert) {
|
||||
@@ -21,9 +21,9 @@ module('Unit | Abilities | Role', function (hooks) {
|
||||
const model = {
|
||||
authorized_actions: ['add-principals'],
|
||||
};
|
||||
assert.ok(service.can('addPrincipals role', model));
|
||||
assert.true(service.can('addPrincipals role', model));
|
||||
model.authorized_actions = [];
|
||||
assert.notOk(service.can('addPrincipals role', model));
|
||||
assert.false(service.can('addPrincipals role', model));
|
||||
});
|
||||
|
||||
test('it reflects when a given role may remove principals based on authorized_actions', function (assert) {
|
||||
@@ -32,8 +32,47 @@ module('Unit | Abilities | Role', function (hooks) {
|
||||
const model = {
|
||||
authorized_actions: ['remove-principals'],
|
||||
};
|
||||
assert.ok(service.can('removePrincipals role', model));
|
||||
assert.true(service.can('removePrincipals role', model));
|
||||
model.authorized_actions = [];
|
||||
assert.notOk(service.can('removePrincipals role', model));
|
||||
assert.false(service.can('removePrincipals role', model));
|
||||
});
|
||||
|
||||
test('it proxies role principal read ability to the appropriate ability type', function (assert) {
|
||||
assert.expect(6);
|
||||
const service = this.owner.lookup('service:can');
|
||||
const user = {
|
||||
constructor: { modelName: 'user' },
|
||||
authorized_actions: [],
|
||||
};
|
||||
const group = {
|
||||
constructor: { modelName: 'group' },
|
||||
authorized_actions: [],
|
||||
};
|
||||
const managedGroup = {
|
||||
constructor: { modelName: 'managed-group' },
|
||||
authorized_actions: [],
|
||||
};
|
||||
assert.false(service.can('readPrincipal role', user));
|
||||
assert.false(service.can('readPrincipal role', group));
|
||||
assert.false(service.can('readPrincipal role', managedGroup));
|
||||
user.authorized_actions.push('read');
|
||||
group.authorized_actions.push('read');
|
||||
managedGroup.authorized_actions.push('read');
|
||||
assert.true(service.can('readPrincipal role', user));
|
||||
assert.true(service.can('readPrincipal role', group));
|
||||
assert.true(service.can('readPrincipal role', managedGroup));
|
||||
});
|
||||
|
||||
test('it throws an error for unknown principal types on principal read check', function (assert) {
|
||||
assert.expect(2);
|
||||
const service = this.owner.lookup('service:can');
|
||||
const account = {
|
||||
constructor: { modelName: 'account' },
|
||||
authorized_actions: ['read'],
|
||||
};
|
||||
assert.throws(() => {
|
||||
service.can('readPrincipal role', account);
|
||||
});
|
||||
assert.true(service.can('read account', account));
|
||||
});
|
||||
});
|
||||
@@ -22,6 +22,22 @@ module('Unit | Model | credential', function (hooks) {
|
||||
assert.false(modelC.isStatic);
|
||||
});
|
||||
|
||||
test('it has isJSON property and returns the expected values', async function (assert) {
|
||||
assert.expect(3);
|
||||
const store = this.owner.lookup('service:store');
|
||||
const modelA = store.createRecord('credential', { type: 'json' });
|
||||
const modelB = store.createRecord('credential', {
|
||||
type: 'username_password',
|
||||
});
|
||||
const modelC = store.createRecord('credential', {
|
||||
type: 'ssh_private_key',
|
||||
});
|
||||
|
||||
assert.true(modelA.isJSON);
|
||||
assert.false(modelB.isJSON);
|
||||
assert.false(modelC.isJSON);
|
||||
});
|
||||
|
||||
test('it has isUnknown property and returns the expected values', async function (assert) {
|
||||
assert.expect(3);
|
||||
const store = this.owner.lookup('service:store');
|
||||
|
||||
@@ -64,6 +64,7 @@ module('Unit | Model | role', function (hooks) {
|
||||
principals: [
|
||||
{ id: '1', type: 'user' },
|
||||
{ id: '3', type: 'group' },
|
||||
{ id: '5', type: 'managed group' },
|
||||
],
|
||||
version: 1,
|
||||
scope: {
|
||||
@@ -84,7 +85,7 @@ module('Unit | Model | role', function (hooks) {
|
||||
(schema, request) => {
|
||||
const body = JSON.parse(request.requestBody);
|
||||
assert.deepEqual(body, {
|
||||
principal_ids: ['1', '3'],
|
||||
principal_ids: ['1', '3', '5'],
|
||||
version: 1,
|
||||
});
|
||||
return { id: '123abc' };
|
||||
@@ -101,6 +102,7 @@ module('Unit | Model | role', function (hooks) {
|
||||
principals: [
|
||||
{ id: '1', type: 'user' },
|
||||
{ id: '3', type: 'group' },
|
||||
{ id: '5', type: 'managed group' },
|
||||
],
|
||||
version: 1,
|
||||
scope: {
|
||||
@@ -111,7 +113,7 @@ module('Unit | Model | role', function (hooks) {
|
||||
},
|
||||
});
|
||||
const model = store.peekRecord('role', '123abc');
|
||||
await model.removePrincipals(['1', '3']);
|
||||
await model.removePrincipals(['1', '3', '5']);
|
||||
});
|
||||
|
||||
test('it has a `removePrincipal` method that deletes a single principal using `removePrincipals` method', async function (assert) {
|
||||
@@ -138,6 +140,7 @@ module('Unit | Model | role', function (hooks) {
|
||||
principals: [
|
||||
{ id: '1', type: 'user' },
|
||||
{ id: '3', type: 'group' },
|
||||
{ id: '5', type: 'managed group' },
|
||||
],
|
||||
version: 1,
|
||||
scope: {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
|
||||
import { TYPE_TARGET_TCP, TYPE_TARGET_SSH } from 'api/models/target';
|
||||
|
||||
module('Unit | Model | target', function (hooks) {
|
||||
setupTest(hooks);
|
||||
@@ -383,7 +384,7 @@ module('Unit | Model | target', function (hooks) {
|
||||
assert.expect(2);
|
||||
const store = this.owner.lookup('service:store');
|
||||
const modelSSH = store.createRecord('target', {
|
||||
type: 'ssh',
|
||||
type: TYPE_TARGET_SSH,
|
||||
});
|
||||
assert.true(modelSSH.isSSH);
|
||||
assert.false(modelSSH.isTCP);
|
||||
@@ -393,7 +394,7 @@ module('Unit | Model | target', function (hooks) {
|
||||
assert.expect(2);
|
||||
const store = this.owner.lookup('service:store');
|
||||
const modelTCP = store.createRecord('target', {
|
||||
type: 'tcp',
|
||||
type: TYPE_TARGET_TCP,
|
||||
});
|
||||
assert.true(modelTCP.isTCP);
|
||||
assert.false(modelTCP.isSSH);
|
||||
|
||||
@@ -182,7 +182,6 @@ module('Unit | Serializer | auth method', function (hooks) {
|
||||
signing_algorithms,
|
||||
idp_ca_certs,
|
||||
} = record;
|
||||
console.log(record);
|
||||
assert.deepEqual(account_claim_maps, [
|
||||
{ from: 'from', to: 'to' },
|
||||
{ from: 'foo', to: 'bar' },
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
import { TYPE_TARGET_TCP, TYPE_TARGET_SSH } from 'api/models/target';
|
||||
|
||||
module('Unit | Serializer | target', function (hooks) {
|
||||
setupTest(hooks);
|
||||
@@ -26,7 +27,7 @@ module('Unit | Serializer | target', function (hooks) {
|
||||
},
|
||||
default_port: 1234,
|
||||
version: 1,
|
||||
type: 'tcp',
|
||||
type: TYPE_TARGET_TCP,
|
||||
});
|
||||
const snapshot = record._createSnapshot();
|
||||
snapshot.adapterOptions = {};
|
||||
@@ -35,7 +36,7 @@ module('Unit | Serializer | target', function (hooks) {
|
||||
name: 'User',
|
||||
description: 'Description',
|
||||
version: 1,
|
||||
type: 'tcp',
|
||||
type: TYPE_TARGET_TCP,
|
||||
scope_id: 'org_1',
|
||||
session_max_seconds: 28800,
|
||||
session_connection_limit: null,
|
||||
@@ -124,7 +125,7 @@ module('Unit | Serializer | target', function (hooks) {
|
||||
name: 'User',
|
||||
description: 'Description',
|
||||
version: 1,
|
||||
type: 'ssh',
|
||||
type: TYPE_TARGET_SSH,
|
||||
worker_filter: 'worker',
|
||||
default_port: 1234,
|
||||
scope: {
|
||||
@@ -137,7 +138,7 @@ module('Unit | Serializer | target', function (hooks) {
|
||||
name: 'User',
|
||||
description: 'Description',
|
||||
version: 1,
|
||||
type: 'ssh',
|
||||
type: TYPE_TARGET_SSH,
|
||||
scope_id: 'org_1',
|
||||
session_max_seconds: 28800,
|
||||
session_connection_limit: null,
|
||||
|
||||
@@ -41,7 +41,6 @@ descriptions:
|
||||
neither-list-nor-create: You do not have permission to list or create {resource}.
|
||||
no-results: We found no matching records.
|
||||
choose-a-provider: There are several built-in plugins for the most common providers. You can choose from one of the included plugins or use a custom plugin.
|
||||
choose-a-provider-azure: Boundary does not yet support rotating credentials.
|
||||
provider: This is the provider for this host catalog. The format of the host set filters are specific to each provider.
|
||||
questions:
|
||||
delete-confirm: Are you sure you want to delete this resource?
|
||||
|
||||
@@ -150,6 +150,7 @@ secret:
|
||||
help: The client secret value generated by Azure for Boundary to use for authentication.
|
||||
disable_credential_rotation:
|
||||
label: Disable credential rotation
|
||||
help: Azure host catalogs do not yet support credential rotation.
|
||||
dns_names:
|
||||
label: Latest DNS Names
|
||||
host:
|
||||
@@ -163,4 +164,4 @@ tags:
|
||||
label: Tags
|
||||
secret-editor:
|
||||
details: This secret is saved but won't be displayed
|
||||
edit: Click to replace this secret
|
||||
edit: Click to replace this secret
|
||||
@@ -93,13 +93,13 @@ host-catalog:
|
||||
delete: Delete Host Catalog
|
||||
help:
|
||||
static: Provide a list of hosts and host sets with static addresses
|
||||
dynamic: Automate discovery of target hosts and services on Azure or AWS
|
||||
plugin: Automate discovery of target hosts and services on Azure or AWS
|
||||
types:
|
||||
static: Static
|
||||
unknown: Plugin
|
||||
aws: AWS
|
||||
azure: Azure
|
||||
dynamic: Dynamic
|
||||
plugin: Dynamic
|
||||
form:
|
||||
aws_region:
|
||||
label: AWS Region
|
||||
@@ -271,6 +271,7 @@ role:
|
||||
types:
|
||||
user: User
|
||||
group: Group
|
||||
managed-group: Managed Group
|
||||
actions:
|
||||
add-principals: Add Principals
|
||||
grant:
|
||||
@@ -304,11 +305,15 @@ target:
|
||||
add-brokered-credential-sources: Add Brokered Credentials
|
||||
add-injected-application-credential-sources: Add Injected Application Credentials
|
||||
types:
|
||||
tcp: TCP
|
||||
tcp: Generic TCP
|
||||
ssh: SSH
|
||||
help:
|
||||
tcp: Connections are made over TCP to support a broad range of protocols, including SSH, RDP, databases, and more. This is the most general-purpose type.
|
||||
ssh: Protocol-aware connections are made over SSH. Use when requiring SSH connections to remote hosts.
|
||||
tcp: Generic TCP supports a broad range of connection types.
|
||||
ssh: Protocol-aware SSH with support for credential injection.
|
||||
form:
|
||||
type:
|
||||
label: Type
|
||||
help: Target type is the protocol with which end users should connect to this target. Choose Generic TCP for broad support of common protocols including RDP, K8s, many databases, and more.
|
||||
host-source:
|
||||
title: Host Source
|
||||
title_plural: Host Sources
|
||||
@@ -455,6 +460,9 @@ credential:
|
||||
private_key_passphrase:
|
||||
label: Passphrase
|
||||
help: Optional.
|
||||
json:
|
||||
label: JSON
|
||||
help: This is the JSON blob that provides credentials to the host. After this data is saved, you will not be able to edit it again without replacing it.
|
||||
scope:
|
||||
title: Scope
|
||||
title_plural: Scopes
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import RoseFormRadioRadioComponent from '../radio';
|
||||
import { deprecate } from '@ember/debug';
|
||||
|
||||
export default class RoseFormRadioCardComponent extends RoseFormRadioRadioComponent {
|
||||
// =attributes
|
||||
layout = this.args.layout;
|
||||
className = `rose-form-radio-card ${this.layout}`;
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
deprecate(
|
||||
`Rose Form/Radio/Card component is deprecated, use HDS Form/RadioCard instead. For more info, refer to `,
|
||||
false,
|
||||
{
|
||||
id: 'rose.form.radio.card',
|
||||
until: '4.8.0',
|
||||
for: 'rose',
|
||||
since: {
|
||||
enabled: '3.28.8',
|
||||
},
|
||||
url: 'https://boundary-ui-storybook.vercel.app/?path=/docs/rose-form-radio-radio-card--card',
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,12 @@
|
||||
import { Meta, Story, Preview } from '@storybook/addon-docs';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
|
||||
<Meta title="Rose/Form/Radio/Card" component="RadioCard" />
|
||||
<Meta title="Rose/Form/Radio/Card(removed)" component="RadioCard" />
|
||||
|
||||
# HDS::Form::RadioCard (Recommended)
|
||||
# RadioCard (removed)
|
||||
|
||||
> **_NOTE:_** Replaces the deprecated Rose/Form/Radio/Card.
|
||||
|
||||
Refer to the [HDS Form::RadioCard
|
||||
documentation](https://design-system-components-hashicorp.vercel.app/components/form/radio-card)
|
||||
for a complete overview
|
||||
Rose/Form/Select has been removed in favor of
|
||||
[HDS::Form::RadioCard](https://design-system-components-hashicorp.vercel.app/components/form/radio-card).
|
||||
|
||||
```handlebars
|
||||
<Hds::Form::RadioCard @checked={{true}} as |R|>
|
||||
@@ -44,78 +41,3 @@ export const HDSTemplate = (args) => ({
|
||||
{HDSTemplate.bind({})}
|
||||
</Story>
|
||||
</Preview>
|
||||
|
||||
# Rose/Form/Radio/Card (Deprecated)
|
||||
|
||||
Rose/Form/Radio/Card has been deprecated in favor of [HDS::Form::RadioCard](https://design-system-components-hashicorp.vercel.app/components/form/radio-card)
|
||||
|
||||
### <Rose::Form::Radio::Card> usage:
|
||||
|
||||
# Form Radiofield in a wide format
|
||||
|
||||
Renders a form radiofield in a Wide format.
|
||||
|
||||
```handlebars
|
||||
<Rose::Form::Radio::Card
|
||||
@label='Yes'
|
||||
@error={{false}}
|
||||
@value='yes-agree'
|
||||
@selectedValue='yes-agree'
|
||||
@disabled={{false}}
|
||||
@changed={{fn}}
|
||||
@layout='wide'
|
||||
/>
|
||||
```
|
||||
|
||||
Renders a form radiofield in a tile format.
|
||||
|
||||
```handlebars
|
||||
<Rose::Form::Radio::Card
|
||||
@label='Yes'
|
||||
@error={{false}}
|
||||
@value='yes-agree'
|
||||
@selectedValue='yes-agree'
|
||||
@disabled={{false}}
|
||||
@changed={{fn}}
|
||||
@layout='tile'
|
||||
/>
|
||||
```
|
||||
|
||||
export const Template = (args) => ({
|
||||
template: hbs`
|
||||
<Rose::Form::Radio::Card
|
||||
@label={{label}}
|
||||
@error={{error}}
|
||||
@value={{value}}
|
||||
@selectedValue={{selectedValue}}
|
||||
@disabled={{disabled}}
|
||||
@icon={{icon}}
|
||||
@helperText={{helperText}}
|
||||
@layout={{Layout}}
|
||||
/>`,
|
||||
context: args,
|
||||
});
|
||||
|
||||
<Preview>
|
||||
<Story
|
||||
name="Rose Card"
|
||||
height="10rem"
|
||||
args={{
|
||||
label: 'Azure',
|
||||
error: false,
|
||||
value: 'value and selectedValue must match to select radiofield',
|
||||
selectedValue: 'value and selectedValue must match to select radiofield',
|
||||
helperText: 'Record financial transactions using',
|
||||
disabled: false,
|
||||
}}
|
||||
argTypes={{
|
||||
Layout: {
|
||||
control: 'inline-radio',
|
||||
options: ['wide', 'tile'],
|
||||
name: 'layout',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{Template.bind({})}
|
||||
</Story>
|
||||
</Preview>
|
||||
@@ -8,13 +8,5 @@
|
||||
changed=@changed
|
||||
inline=@inline
|
||||
)
|
||||
card=(component
|
||||
'rose/form/radio/card'
|
||||
name=@name
|
||||
selectedValue=@selectedValue
|
||||
disabled=@disabled
|
||||
changed=@changed
|
||||
layout=@layout
|
||||
)
|
||||
)
|
||||
}}
|
||||
@@ -75,9 +75,9 @@ export const Template = (args) => ({
|
||||
</Story>
|
||||
</Preview>
|
||||
|
||||
# HDS::Form::RadioCard::Group (Recommended)
|
||||
# HDS::Form::RadioCard::Group
|
||||
|
||||
> **_NOTE:_** Replaces the deprecated Rose/Form/Radio/Group/Card.
|
||||
> **_NOTE:_** Replaces the removed Rose/Form/Radio/Group/Card.
|
||||
|
||||
```handlebars
|
||||
<Hds::Form::RadioCard::Group
|
||||
@@ -144,83 +144,3 @@ export const HDSTemplate = (args) => ({
|
||||
{HDSTemplate.bind({})}
|
||||
</Story>
|
||||
</Preview>
|
||||
|
||||
# Rose::Form::Radio::Group::Card (Deprecated)
|
||||
|
||||
```handlebars
|
||||
<Rose::Form::Radio::Group
|
||||
@disabled={{false}}
|
||||
@name={{name}}
|
||||
@selectedValue='disagree'
|
||||
@changed={{fn}}
|
||||
@inline={{false}}
|
||||
as |radioGroup|
|
||||
>
|
||||
<radioGroup.Card
|
||||
@label='Agree'
|
||||
@value='agree'
|
||||
@layout='tile'
|
||||
@helperText='dwsd'
|
||||
/>
|
||||
<radioGroup.Card
|
||||
@label='disagree'
|
||||
@value='disagree'
|
||||
@layout='tile'
|
||||
@helperText='dwsd'
|
||||
/>
|
||||
<radioGroup.Card
|
||||
@label='maybe'
|
||||
@value='maybe'
|
||||
@layout='tile'
|
||||
@helperText='dwsd'
|
||||
/>
|
||||
</Rose::Form::Radio::Group>
|
||||
```
|
||||
|
||||
export const RoseTemplate = (args) => ({
|
||||
template: hbs`
|
||||
<Rose::Form::Radio::Group
|
||||
@disabled={{disabled}}
|
||||
@name={{name}}
|
||||
@selectedValue={{selectedValue}}
|
||||
@changed={{changed}}
|
||||
as |radioGroup|
|
||||
>
|
||||
<radioGroup.card
|
||||
@label={{firstFieldLabel}}
|
||||
@value={{firstFieldValue}}
|
||||
@icon={{firstFieldIcon}}
|
||||
/>
|
||||
<radioGroup.card @label={{secondFieldLabel}} @value={{secondFieldValue}} />
|
||||
</Rose::Form::Radio::Group>`,
|
||||
context: args,
|
||||
});
|
||||
|
||||
<Preview>
|
||||
<Story
|
||||
name="Rose Basic"
|
||||
height="10rem"
|
||||
args={{
|
||||
name: 'mustagree',
|
||||
disabled: false,
|
||||
selectedValue: 'disagree',
|
||||
firstFieldLabel: 'Agree',
|
||||
firstFieldValue: 'agree',
|
||||
secondFieldLabel: 'Disagree',
|
||||
secondFieldValue: 'disagree',
|
||||
firstFieldIcon: 'flight-icons/svg/org-16',
|
||||
}}
|
||||
argTypes={{
|
||||
firstFieldIcon: {
|
||||
control: 'select',
|
||||
options: [
|
||||
'flight-icons/svg/org-16',
|
||||
'flight-icons/svg/clipboard-copy-16',
|
||||
],
|
||||
},
|
||||
changed: { action: 'changed' },
|
||||
}}
|
||||
>
|
||||
{RoseTemplate.bind({})}
|
||||
</Story>
|
||||
</Preview>
|
||||
@@ -9,8 +9,8 @@ import 'codemirror/addon/edit/matchbrackets';
|
||||
import 'codemirror/addon/edit/closebrackets';
|
||||
import 'codemirror/addon/selection/active-line';
|
||||
import 'codemirror/addon/display/autorefresh';
|
||||
import 'codemirror/addon/lint/lint'
|
||||
import 'codemirror/addon/lint/json-lint'
|
||||
import 'codemirror/addon/lint/lint';
|
||||
import 'codemirror/addon/lint/json-lint';
|
||||
|
||||
// Here we define default options for the editor.
|
||||
// These should follow the codemirror configuration types
|
||||
|
||||
@@ -6,4 +6,3 @@
|
||||
@import 'select';
|
||||
@import 'radio';
|
||||
@import 'fieldset';
|
||||
@import 'radio-card';
|
||||
@@ -1,240 +0,0 @@
|
||||
@use '../../variables/sizing';
|
||||
@use '../../utilities/type';
|
||||
@use '../../variables/typography';
|
||||
|
||||
$size-xxl: sizing.rems(xxl); // 48
|
||||
$size-xl: sizing.rems(xl); // 36
|
||||
$size-l: sizing.rems(l); // 24
|
||||
$size-m: sizing.rems(m); // 16
|
||||
$size-s: sizing.rems(s); // 12
|
||||
$size-xs: sizing.rems(xs); // 8
|
||||
$size-xxs: sizing.rems(xxs); // 4
|
||||
$size-xxxs: sizing.rems(xxxs); // 2
|
||||
$size-xxxxs: sizing.rems(xxxxs); // 1
|
||||
|
||||
$radio-svg: (
|
||||
'default':
|
||||
"<svg viewBox='0 0 24 24' fill='%23aaaaaa' xmlns='http://www.w3.org/2000/svg'><path fill-rule='evenodd' clip-rule='evenodd' d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z' /></svg>",
|
||||
'hover':
|
||||
"<svg viewBox='0 0 24 24' fill='%236f7682' xmlns='http://www.w3.org/2000/svg'><path fill-rule='evenodd' clip-rule='evenodd' d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z' /></svg>",
|
||||
'focus':
|
||||
"<svg viewBox='0 0 24 24' fill='%231563ff' xmlns='http://www.w3.org/2000/svg'><path fill-rule='evenodd' clip-rule='evenodd' d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z' /></svg>",
|
||||
'checked':
|
||||
"<svg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'><path fill-rule='evenodd' clip-rule='evenodd' d='M12 7c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5zm0-5C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z' fill='%231563ff'/></svg>",
|
||||
'disabled':
|
||||
"<svg viewBox='0 0 24 24' fill='%23bac1cc' xmlns='http://www.w3.org/2000/svg'><path fill-rule='evenodd' clip-rule='evenodd' d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z' /></svg>",
|
||||
'disabled-checked':
|
||||
"<svg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'><path fill-rule='evenodd' clip-rule='evenodd' d='M12 7c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5zm0-5C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z' fill='%23bac1cc'/></svg>",
|
||||
'error':
|
||||
"<svg viewBox='0 0 24 24' fill='%23c73445' xmlns='http://www.w3.org/2000/svg'><path fill-rule='evenodd' clip-rule='evenodd' d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z' /></svg>",
|
||||
'error-checked':
|
||||
"<svg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'><path fill-rule='evenodd' clip-rule='evenodd' d='M12 7c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5zm0-5C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z' fill='%23c73445'/></svg>",
|
||||
);
|
||||
|
||||
.rose-form-radio-card {
|
||||
$card-wide-width: $size-m * 10;
|
||||
$card-tile-width: $size-m * 7.25;
|
||||
$border-width: $size-xxxs * 2;
|
||||
|
||||
&.wide {
|
||||
width: ($size-m) * 20;
|
||||
}
|
||||
|
||||
&.tile {
|
||||
width: ($size-m) * 15;
|
||||
|
||||
.rose-icon {
|
||||
--size: #{$size-xl};
|
||||
|
||||
padding: $size-xxxs;
|
||||
}
|
||||
|
||||
.rose-form-radio-label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
height: $size-m * 9;
|
||||
|
||||
&::after {
|
||||
left: $card-tile-width - $border-width;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: $size-m $size-xs $size-m 0;
|
||||
|
||||
.radio-card-overlay {
|
||||
position: absolute;
|
||||
height: $size-l + $size-xxs;
|
||||
background-color: var(--ui-gray-subtler-5);
|
||||
width: 100%;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
border-bottom-left-radius: $size-xxxs;
|
||||
border-bottom-right-radius: $size-xxxs;
|
||||
}
|
||||
|
||||
.rose-form-radio-label {
|
||||
@include type.type(s, bold);
|
||||
|
||||
width: ($size-m) * 23;
|
||||
height: 100%;
|
||||
display: block;
|
||||
position: relative;
|
||||
text-align: left;
|
||||
border: $size-xxxxs solid var(--ui-gray-subtler-2);
|
||||
box-shadow: 0 $size-xxs $size-xxs
|
||||
rgba(var(--stark-components), var(--opacity-2));
|
||||
border-radius: $size-xxs;
|
||||
padding: $size-s $size-m ($size-xl + $size-m);
|
||||
box-sizing: border-box;
|
||||
|
||||
.rose-form-helper-text {
|
||||
@include type.type(xs, normal);
|
||||
|
||||
padding-top: $size-xs;
|
||||
}
|
||||
|
||||
&::after {
|
||||
position: absolute;
|
||||
bottom: $size-xxs + $size-xxxxs;
|
||||
right: $card-wide-width - $border-width;
|
||||
display: block;
|
||||
width: $size-m;
|
||||
height: $size-m;
|
||||
transform: scale(2);
|
||||
pointer-events: none;
|
||||
content: ' ';
|
||||
background: no-repeat 50%/50% 50%;
|
||||
background-image: url($data-svg-prefix + map-get($radio-svg, 'default'));
|
||||
}
|
||||
}
|
||||
|
||||
.rose-form-radio-field {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
bottom: $size-xs;
|
||||
opacity: 0;
|
||||
|
||||
&:not(:disabled) {
|
||||
&:focus {
|
||||
~ .rose-form-radio-label {
|
||||
&::after {
|
||||
background-image: url($data-svg-prefix + map-get(
|
||||
$radio-svg,
|
||||
'focus'
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
~ .rose-form-radio-label {
|
||||
border: $size-xxxxs solid var(--ui-gray);
|
||||
}
|
||||
}
|
||||
|
||||
&:checked {
|
||||
~ .rose-form-radio-label {
|
||||
&::after {
|
||||
background-image: url($data-svg-prefix + map-get(
|
||||
$radio-svg,
|
||||
'checked'
|
||||
));
|
||||
}
|
||||
|
||||
.radio-card-overlay {
|
||||
background-color: var(--action-subtler-1h);
|
||||
|
||||
.ember-application.rose-theme-light & {
|
||||
background-color: var(--action-subtler-1h);
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
background-color: var(--action-subtler-1);
|
||||
}
|
||||
|
||||
.ember-application.rose-theme-dark & {
|
||||
background-color: var(--action-subtler-1);
|
||||
}
|
||||
}
|
||||
|
||||
border: $size-xxxxs solid var(--info-subtler-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
~ .rose-form-radio-label {
|
||||
border: $size-xxxxs solid var(--ui-border-subtler-1);
|
||||
background-color: var(--ui-gray-subtler-6);
|
||||
color: var(--ui-gray-starker-1);
|
||||
|
||||
&::after {
|
||||
background-image: url($data-svg-prefix + map-get(
|
||||
$radio-svg,
|
||||
'disabled'
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
&:checked {
|
||||
~ .rose-form-radio-label {
|
||||
&::after {
|
||||
background-image: url($data-svg-prefix + map-get(
|
||||
$radio-svg,
|
||||
'disabled-checked'
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.error {
|
||||
.rose-form-radio-field {
|
||||
&:not(:disabled, :focus) {
|
||||
~ .rose-form-radio-label {
|
||||
color: var(--ui-gray);
|
||||
border: $size-xxxxs solid var(--failure);
|
||||
|
||||
.radio-card-overlay {
|
||||
background-color: var(--failure-subtler-2);
|
||||
}
|
||||
|
||||
.rose-form-helper-text.error {
|
||||
color: var(--ui-gray);
|
||||
}
|
||||
|
||||
&::after {
|
||||
background-image: url($data-svg-prefix + map-get(
|
||||
$radio-svg,
|
||||
'error'
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
&:checked {
|
||||
~ .rose-form-radio-label {
|
||||
color: var(--ui-gray);
|
||||
border: $size-xxxxs solid var(--failure);
|
||||
|
||||
.radio-card-overlay {
|
||||
background-color: var(--failure-subtler-2);
|
||||
}
|
||||
|
||||
&::after {
|
||||
background-image: url($data-svg-prefix + map-get(
|
||||
$radio-svg,
|
||||
'error-checked'
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,7 +42,7 @@
|
||||
"ember-cli-htmlbars": "^6.1.0",
|
||||
"ember-cli-sass": "^11.0.1",
|
||||
"ember-composable-helpers": "^5.0.0",
|
||||
"ember-focus-trap": "^0.7.0",
|
||||
"ember-focus-trap": "^1.0.1",
|
||||
"ember-named-blocks-polyfill": "^0.2.5",
|
||||
"ember-radio-button": "github:yapplabs/ember-radio-button.git#b2ab65f",
|
||||
"ember-uuid": "^2.1.0",
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { render, find } from '@ember/test-helpers';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
|
||||
module('Integration | Component | rose/form/radio/card', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
test('it renders', async function (assert) {
|
||||
await render(hbs`<Rose::Form::Radio::Card />`);
|
||||
assert.ok(await find('input'));
|
||||
assert.strictEqual(await find('input').type, 'radio');
|
||||
});
|
||||
|
||||
test('it renders in a tile format when tile layout is applied', async function (assert) {
|
||||
await render(hbs`<Rose::Form::Radio::Card @layout="tile"/>`);
|
||||
assert.ok(await find('.tile'));
|
||||
});
|
||||
|
||||
test('it supports an icon', async function (assert) {
|
||||
await render(
|
||||
hbs`<Rose::Form::Radio::Card @icon="flight-icons/svg/user-circle-16" />`
|
||||
);
|
||||
assert.ok(find('svg'));
|
||||
});
|
||||
|
||||
test('it is checked when @value and @selectedValue values match', async function (assert) {
|
||||
await render(
|
||||
hbs`<Rose::Form::Radio::Card @value="tree" @selectedValue="tree"/>`
|
||||
);
|
||||
assert.true(await find('input').checked);
|
||||
});
|
||||
|
||||
test('it is not disabled by default', async function (assert) {
|
||||
await render(hbs`<Rose::Form::Radio::Card />`);
|
||||
assert.false(await find('input').disabled);
|
||||
});
|
||||
|
||||
test('it is not in error state by default', async function (assert) {
|
||||
await render(hbs`<Rose::Form::Radio::Card />`);
|
||||
assert.notOk(await find('.error'));
|
||||
});
|
||||
|
||||
test('it is disabled when @disabled={{true}}', async function (assert) {
|
||||
await render(hbs`<Rose::Form::Radio::Card @disabled={{true}}/>`);
|
||||
assert.true(await find('input').disabled);
|
||||
});
|
||||
|
||||
test('it is in error state when @error={{true}}', async function (assert) {
|
||||
await render(hbs`<Rose::Form::Radio::Card @error={{true}} />`);
|
||||
assert.ok(await find('.error'));
|
||||
});
|
||||
});
|
||||
+2
-1
@@ -105,7 +105,8 @@
|
||||
"async": "^2.6.4",
|
||||
"terser": "^5.14.2",
|
||||
"jpeg-js": "^0.4.4",
|
||||
"got": "^11.8.5"
|
||||
"got": "^11.8.5",
|
||||
"loader-utils": "^2.0.4"
|
||||
},
|
||||
"config": {
|
||||
"commitizen": {
|
||||
|
||||
@@ -32,6 +32,7 @@ module.exports = {
|
||||
'.eslintrc.js',
|
||||
'.prettierrc.js',
|
||||
'.template-lintrc.js',
|
||||
'.stylelintrc.js',
|
||||
'ember-cli-build.js',
|
||||
'testem.js',
|
||||
'blueprints/*/index.js',
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
files:
|
||||
include: 'app/styles/**/*.s+(a|c)ss'
|
||||
rules:
|
||||
leading-zero: 0
|
||||
mixins-before-declarations: 0
|
||||
nesting-depth: 0
|
||||
placeholder-in-extend: 0
|
||||
property-sort-order: 0
|
||||
@@ -0,0 +1,33 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
extends: ['stylelint-config-standard-scss', 'stylelint-config-prettier-scss'],
|
||||
rules: {
|
||||
// We oftentimes want to have long form properties for readability
|
||||
'declaration-block-no-redundant-longhand-properties': null,
|
||||
|
||||
// This is just a personal preference unless the quotes are needed
|
||||
// to escape special characters
|
||||
'function-url-quotes': null,
|
||||
|
||||
// This is often limited by the DOM and we want to order our declarations by our own standard
|
||||
'no-descending-specificity': null,
|
||||
|
||||
// -Webkit animation CSS for older browsers. Need a decision to remove `-webkit` syntax in
|
||||
// favor of normal CSS
|
||||
'property-no-vendor-prefix': null,
|
||||
'at-rule-no-vendor-prefix': null,
|
||||
|
||||
// As a team we want to stick to using kebab-case as our selector class pattern, but there are a couple
|
||||
// of exceptions in which we overrride CSS from addons like CodeMirror and HDS, in which they use
|
||||
// pascal case and a double-kebab-case. This regex checks for all of those 3 patterns and won't fail
|
||||
// lint check, but will fail if its any other pattern.
|
||||
'selector-class-pattern': [
|
||||
'(^([a-z][a-z0-9]*)(-{1,2}[a-z0-9]+)*$)|(^CodeMirror)',
|
||||
{
|
||||
message: (selector) =>
|
||||
`Expected class selector "${selector}" to be kebab-case`,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
import CredentialAbility from 'api/abilities/credential';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class OverrideCredentialAbility extends CredentialAbility {
|
||||
// =service
|
||||
@service features;
|
||||
|
||||
/**
|
||||
* This override ensures that JSON credentials may be read only if the
|
||||
* json-credentials feature flag is enabled. All other types are subject
|
||||
* to the standard logic found in the api addon.
|
||||
*/
|
||||
get canRead() {
|
||||
return this.features.isEnabled('json-credentials') || !this.model.isJSON
|
||||
? super.canRead
|
||||
: false;
|
||||
}
|
||||
}
|
||||
@@ -70,14 +70,13 @@
|
||||
</Hds::Form::RadioCard::Group>
|
||||
{{else}}
|
||||
<InfoField
|
||||
@value={{t (concat 'resources.credential-store.help.' @model.type)}}
|
||||
@value={{t (concat 'resources.credential-store.types.' @model.type)}}
|
||||
@icon='keychain'
|
||||
as |F|
|
||||
>
|
||||
<F.Label>{{t 'form.type.label'}}</F.Label>
|
||||
<F.HelperText>{{t
|
||||
(concat 'resources.credential-store.types.' @model.type)
|
||||
}}
|
||||
<F.HelperText>
|
||||
{{t (concat 'resources.credential-store.help.' @model.type)}}
|
||||
</F.HelperText>
|
||||
</InfoField>
|
||||
{{/if}}
|
||||
|
||||
@@ -70,14 +70,13 @@
|
||||
</Hds::Form::RadioCard::Group>
|
||||
{{else}}
|
||||
<InfoField
|
||||
@value={{t (concat 'resources.credential-store.help.' @model.type)}}
|
||||
@value={{t (concat 'resources.credential-store.types.' @model.type)}}
|
||||
@icon='vault'
|
||||
as |F|
|
||||
>
|
||||
<F.Label>{{t 'form.type.label'}}</F.Label>
|
||||
<F.HelperText>{{t
|
||||
(concat 'resources.credential-store.types.' @model.type)
|
||||
}}
|
||||
<F.HelperText>
|
||||
{{t (concat 'resources.credential-store.help.' @model.type)}}
|
||||
</F.HelperText>
|
||||
</InfoField>
|
||||
{{/if}}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
{{component
|
||||
(concat 'form/credential/' @model.type)
|
||||
model=@model
|
||||
types=this.credentialTypes
|
||||
submit=@submit
|
||||
cancel=@cancel
|
||||
changeType=@changeType
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import Component from '@glimmer/component';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { types } from 'api/models/credential';
|
||||
|
||||
export default class FormCredentialComponent extends Component {
|
||||
// =services
|
||||
@service features;
|
||||
|
||||
// =attributes
|
||||
/**
|
||||
* Returns an array of available credential types the user can create
|
||||
* @type {object}
|
||||
*/
|
||||
get credentialTypes() {
|
||||
return this.features.isEnabled('json-credentials')
|
||||
? types
|
||||
: types.filter((type) => type !== 'json');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
<Rose::Form
|
||||
@onSubmit={{@submit}}
|
||||
@cancel={{@cancel}}
|
||||
@disabled={{@model.isSaving}}
|
||||
@showEditToggle={{if @model.isNew false true}}
|
||||
as |form|
|
||||
>
|
||||
|
||||
<form.input
|
||||
@name='name'
|
||||
@value={{@model.name}}
|
||||
@label={{t 'form.name.label'}}
|
||||
@error={{@model.errors.name}}
|
||||
@helperText={{t 'form.name.help'}}
|
||||
as |field|
|
||||
>
|
||||
{{#if @model.errors.name}}
|
||||
<field.errors as |errors|>
|
||||
{{#each @model.errors.name as |error|}}
|
||||
<errors.message>{{error.message}}</errors.message>
|
||||
{{/each}}
|
||||
</field.errors>
|
||||
{{/if}}
|
||||
</form.input>
|
||||
|
||||
<form.textarea
|
||||
@name='description'
|
||||
@value={{@model.description}}
|
||||
@label={{t 'form.description.label'}}
|
||||
@error={{@model.errors.description}}
|
||||
@helperText={{t 'form.description.help'}}
|
||||
as |field|
|
||||
>
|
||||
{{#if @model.errors.description}}
|
||||
<field.errors as |errors|>
|
||||
{{#each @model.errors.description as |error|}}
|
||||
<errors.message>{{error.message}}</errors.message>
|
||||
{{/each}}
|
||||
</field.errors>
|
||||
{{/if}}
|
||||
</form.textarea>
|
||||
|
||||
{{#if @model.isNew}}
|
||||
<form.fieldset>
|
||||
<:body>
|
||||
<Hds::Form::RadioCard::Group as |G|>
|
||||
<G.Legend>{{t 'form.type.label'}}</G.Legend>
|
||||
{{#each @types as |credentialType|}}
|
||||
<G.RadioCard
|
||||
@name={{credentialType}}
|
||||
@value={{credentialType}}
|
||||
@checked={{eq credentialType @model.type}}
|
||||
@maxWidth='20rem'
|
||||
{{on 'input' (fn @changeType credentialType)}}
|
||||
as |R|
|
||||
>
|
||||
<R.Label>{{t
|
||||
(concat 'resources.credential.types.' credentialType)
|
||||
}}</R.Label>
|
||||
<R.Description>{{t
|
||||
(concat 'resources.credential.help.' credentialType)
|
||||
}}</R.Description>
|
||||
</G.RadioCard>
|
||||
{{/each}}
|
||||
</Hds::Form::RadioCard::Group>
|
||||
</:body>
|
||||
</form.fieldset>
|
||||
|
||||
<form.fieldset>
|
||||
<:title>{{t 'resources.credential.form.json.label'}}</:title>
|
||||
<:description>{{t 'resources.credential.form.json.help'}}</:description>
|
||||
<:body>
|
||||
<Form::Field::JsonSecret
|
||||
@value={{@model.json_object}}
|
||||
@onInput={{fn (mut @model.json_object)}}
|
||||
@showEditButton={{false}}
|
||||
/>
|
||||
</:body>
|
||||
</form.fieldset>
|
||||
{{else}}
|
||||
<InfoField
|
||||
@value={{t (concat 'resources.credential.types.' @model.type)}}
|
||||
as |F|
|
||||
>
|
||||
<F.Label>{{t 'form.type.label'}}</F.Label>
|
||||
<F.HelperText>{{t (concat 'resources.credential.help.' @model.type)}}
|
||||
</F.HelperText>
|
||||
</InfoField>
|
||||
|
||||
<form.fieldset>
|
||||
<:title>{{t 'resources.credential.form.json.label'}}</:title>
|
||||
<:description>{{t 'resources.credential.form.json.help'}}</:description>
|
||||
<:body>
|
||||
<Form::Field::JsonSecret
|
||||
@disabled={{form.disabled}}
|
||||
@value={{@model.json_object}}
|
||||
@onInput={{fn (mut @model.json_object)}}
|
||||
@showEditButton={{true}}
|
||||
/>
|
||||
</:body>
|
||||
</form.fieldset>
|
||||
{{/if}}
|
||||
|
||||
{{#if (can 'save model' @model)}}
|
||||
<form.actions
|
||||
@enableEditText={{t 'actions.edit-form'}}
|
||||
@submitText={{t 'actions.save'}}
|
||||
@cancelText={{t 'actions.cancel'}}
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
</Rose::Form>
|
||||
@@ -12,8 +12,6 @@
|
||||
@label={{t 'form.name.label'}}
|
||||
@error={{@model.errors.name}}
|
||||
@helperText={{t 'form.name.help'}}
|
||||
disabled={{@model.isSaving}}
|
||||
readonly={{false}}
|
||||
as |field|
|
||||
>
|
||||
{{#if @model.errors.name}}
|
||||
@@ -31,8 +29,6 @@
|
||||
@label={{t 'form.description.label'}}
|
||||
@error={{@model.errors.description}}
|
||||
@helperText={{t 'form.description.help'}}
|
||||
disabled={{@model.isSaving}}
|
||||
readonly={{false}}
|
||||
as |field|
|
||||
>
|
||||
{{#if @model.errors.description}}
|
||||
@@ -44,9 +40,9 @@
|
||||
{{/if}}
|
||||
</form.textarea>
|
||||
{{#if @model.isNew}}
|
||||
<Hds::Form::RadioCard::Group @name='types' as |G|>
|
||||
<Hds::Form::RadioCard::Group @name='type' as |G|>
|
||||
<G.Legend>{{t 'form.type.label'}}</G.Legend>
|
||||
{{#each this.credentialTypes as |credentialType|}}
|
||||
{{#each @types as |credentialType|}}
|
||||
<G.RadioCard
|
||||
@name={{credentialType}}
|
||||
@value={{credentialType}}
|
||||
@@ -67,11 +63,11 @@
|
||||
</Hds::Form::RadioCard::Group>
|
||||
{{else}}
|
||||
<InfoField
|
||||
@value={{t (concat 'resources.credential.help.' @model.type)}}
|
||||
@value={{t (concat 'resources.credential.types.' @model.type)}}
|
||||
as |F|
|
||||
>
|
||||
<F.Label>{{t 'form.type.label'}}</F.Label>
|
||||
<F.HelperText>{{t (concat 'resources.credential.types.' @model.type)}}
|
||||
<F.HelperText>{{t (concat 'resources.credential.help.' @model.type)}}
|
||||
</F.HelperText>
|
||||
</InfoField>
|
||||
{{/if}}
|
||||
@@ -82,8 +78,6 @@
|
||||
@label={{t 'resources.credential.form.username.label'}}
|
||||
@helperText={{t 'resources.credential.form.username.help'}}
|
||||
@error={{@model.errors.username}}
|
||||
disabled={{@model.isSaving}}
|
||||
readonly={{false}}
|
||||
as |field|
|
||||
>
|
||||
{{#if @model.errors.username}}
|
||||
@@ -102,8 +96,6 @@
|
||||
@label={{t 'resources.credential.form.private_key.label'}}
|
||||
@helperText={{t 'resources.credential.form.private_key.help'}}
|
||||
@error={{@model.errors.private_key}}
|
||||
disabled={{@model.isSaving}}
|
||||
readonly={{false}}
|
||||
as |field|
|
||||
>
|
||||
{{#if @model.errors.private_key}}
|
||||
@@ -121,8 +113,6 @@
|
||||
@label={{t 'resources.credential.form.private_key_passphrase.label'}}
|
||||
@helperText={{t 'resources.credential.form.private_key_passphrase.help'}}
|
||||
@error={{@model.errors.private_key_passphrase}}
|
||||
disabled={{@model.isSaving}}
|
||||
readonly={{false}}
|
||||
autocomplete='new-password'
|
||||
as |field|
|
||||
>
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import Component from '@glimmer/component';
|
||||
import { types } from 'api/models/credential';
|
||||
|
||||
export default class FormSSHPrivateKeyCredentialComponent extends Component {
|
||||
// =attributes
|
||||
/**
|
||||
* @type {Array}
|
||||
*/
|
||||
credentialTypes = types;
|
||||
}
|
||||
@@ -12,8 +12,6 @@
|
||||
@label={{t 'form.name.label'}}
|
||||
@error={{@model.errors.name}}
|
||||
@helperText={{t 'form.name.help'}}
|
||||
disabled={{@model.isSaving}}
|
||||
readonly={{false}}
|
||||
as |field|
|
||||
>
|
||||
{{#if @model.errors.name}}
|
||||
@@ -31,8 +29,6 @@
|
||||
@label={{t 'form.description.label'}}
|
||||
@error={{@model.errors.description}}
|
||||
@helperText={{t 'form.description.help'}}
|
||||
disabled={{@model.isSaving}}
|
||||
readonly={{false}}
|
||||
as |field|
|
||||
>
|
||||
{{#if @model.errors.description}}
|
||||
@@ -44,9 +40,9 @@
|
||||
{{/if}}
|
||||
</form.textarea>
|
||||
{{#if @model.isNew}}
|
||||
<Hds::Form::RadioCard::Group @name='types' as |G|>
|
||||
<Hds::Form::RadioCard::Group @name='type' as |G|>
|
||||
<G.Legend>{{t 'form.type.label'}}</G.Legend>
|
||||
{{#each this.credentialTypes as |credentialType|}}
|
||||
{{#each @types as |credentialType|}}
|
||||
<G.RadioCard
|
||||
@name={{credentialType}}
|
||||
@value={{credentialType}}
|
||||
@@ -67,11 +63,11 @@
|
||||
</Hds::Form::RadioCard::Group>
|
||||
{{else}}
|
||||
<InfoField
|
||||
@value={{t (concat 'resources.credential.help.' @model.type)}}
|
||||
@value={{t (concat 'resources.credential.types.' @model.type)}}
|
||||
as |F|
|
||||
>
|
||||
<F.Label>{{t 'form.type.label'}}</F.Label>
|
||||
<F.HelperText>{{t (concat 'resources.credential.types.' @model.type)}}
|
||||
<F.HelperText>{{t (concat 'resources.credential.help.' @model.type)}}
|
||||
</F.HelperText>
|
||||
</InfoField>
|
||||
{{/if}}
|
||||
@@ -82,8 +78,6 @@
|
||||
@label={{t 'resources.credential.form.username.label'}}
|
||||
@helperText={{t 'resources.credential.form.username.help'}}
|
||||
@error={{@model.errors.username}}
|
||||
disabled={{@model.isSaving}}
|
||||
readonly={{false}}
|
||||
as |field|
|
||||
>
|
||||
{{#if @model.errors.username}}
|
||||
@@ -103,8 +97,6 @@
|
||||
@label={{t 'resources.credential.form.password.label'}}
|
||||
@helperText={{t 'resources.credential.form.password.help'}}
|
||||
@error={{@model.errors.password}}
|
||||
disabled={{@model.isSaving}}
|
||||
readonly={{false}}
|
||||
autocomplete='new-password'
|
||||
as |field|
|
||||
>
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import Component from '@glimmer/component';
|
||||
import { types } from 'api/models/credential';
|
||||
|
||||
export default class FormUserNamePasswordCredentialComponent extends Component {
|
||||
// =attributes
|
||||
/**
|
||||
* @type {Array}
|
||||
*/
|
||||
credentialTypes = types;
|
||||
}
|
||||
@@ -41,4 +41,4 @@ export default class FormFieldJsonSecretComponent extends Component {
|
||||
enableEditing() {
|
||||
if (!this.args.disabled) this.editing = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -44,62 +44,47 @@
|
||||
</form.textarea>
|
||||
|
||||
{{#if @model.isNew}}
|
||||
<form.fieldset>
|
||||
<:title>{{t 'form.type.label'}}</:title>
|
||||
<:body>
|
||||
<form.radioGroup
|
||||
@name='types'
|
||||
@selectedValue={{@model.type}}
|
||||
@changed={{@changeType}}
|
||||
as |radioGroup|
|
||||
|
||||
<Hds::Form::RadioCard::Group @name='type' as |G|>
|
||||
<G.Legend>{{t 'form.type.label'}}</G.Legend>
|
||||
{{#each this.hostCatalogTypes as |hostCatalogType|}}
|
||||
<G.RadioCard
|
||||
@value={{hostCatalogType}}
|
||||
@maxWidth='20rem'
|
||||
@checked={{eq hostCatalogType @model.type}}
|
||||
{{on 'input' (fn @changeType hostCatalogType)}}
|
||||
as |R|
|
||||
>
|
||||
<radioGroup.card
|
||||
@label={{t 'resources.host-catalog.types.static'}}
|
||||
@value='static'
|
||||
@helperText={{t 'resources.host-catalog.help.static'}}
|
||||
@layout='wide'
|
||||
/>
|
||||
<radioGroup.card
|
||||
@label={{t 'resources.host-catalog.types.dynamic'}}
|
||||
@value='plugin'
|
||||
@helperText={{t 'resources.host-catalog.help.dynamic'}}
|
||||
@layout='wide'
|
||||
/>
|
||||
</form.radioGroup>
|
||||
</:body>
|
||||
</form.fieldset>
|
||||
<R.Label>{{t
|
||||
(concat 'resources.host-catalog.types.' hostCatalogType)
|
||||
}}</R.Label>
|
||||
<R.Description>{{t
|
||||
(concat 'resources.host-catalog.help.' hostCatalogType)
|
||||
}}</R.Description>
|
||||
</G.RadioCard>
|
||||
{{/each}}
|
||||
</Hds::Form::RadioCard::Group>
|
||||
<Hds::Form::RadioCard::Group @name='type' @alignment='center' as |G|>
|
||||
<G.Legend>{{t 'titles.provider'}}</G.Legend>
|
||||
<G.Legend>{{t 'titles.choose-a-provider'}}</G.Legend>
|
||||
<G.HelperText>{{t 'descriptions.choose-a-provider'}}</G.HelperText>
|
||||
|
||||
<Rose::Separator />
|
||||
<form.fieldset>
|
||||
<:title>{{t 'titles.provider'}}</:title>
|
||||
</form.fieldset>
|
||||
|
||||
<form.fieldset>
|
||||
<:title>{{t 'titles.choose-a-provider'}}</:title>
|
||||
<:description>{{t 'descriptions.choose-a-provider'}}</:description>
|
||||
|
||||
<:body>
|
||||
<form.radioGroup
|
||||
@name='provider'
|
||||
@selectedValue={{@model.compositeType}}
|
||||
@changed={{@changeType}}
|
||||
as |radioGroup|
|
||||
{{#each-in this.mapResourceTypeWithIcon as |pluginType icon|}}
|
||||
<G.RadioCard
|
||||
@value={{pluginType}}
|
||||
@maxWidth='20rem'
|
||||
@checked={{eq pluginType @model.compositeType}}
|
||||
{{on 'input' (fn @changeType pluginType)}}
|
||||
as |R|
|
||||
>
|
||||
<radioGroup.card
|
||||
@label={{t 'resources.host-catalog.types.aws'}}
|
||||
@value='aws'
|
||||
@layout='tile'
|
||||
@icon='flight-icons/svg/aws-color-16'
|
||||
/>
|
||||
<radioGroup.card
|
||||
@label={{t 'resources.host-catalog.types.azure'}}
|
||||
@value='azure'
|
||||
@layout='tile'
|
||||
@icon='flight-icons/svg/azure-color-16'
|
||||
/>
|
||||
</form.radioGroup>
|
||||
</:body>
|
||||
</form.fieldset>
|
||||
<R.Label>{{t
|
||||
(concat 'resources.host-catalog.types.' pluginType)
|
||||
}}</R.Label>
|
||||
<R.Icon @name={{icon}} />
|
||||
|
||||
</G.RadioCard>
|
||||
{{/each-in}}
|
||||
</Hds::Form::RadioCard::Group>
|
||||
{{else}}
|
||||
<InfoField
|
||||
@value={{t 'resources.host-catalog.types.aws'}}
|
||||
|
||||
@@ -1,7 +1,23 @@
|
||||
import Component from '@glimmer/component';
|
||||
import { action } from '@ember/object';
|
||||
import { types, pluginTypes } from 'api/models/host-catalog';
|
||||
|
||||
//Note: this is a temporary solution till we have resource type helper in place
|
||||
const icons = ['aws-color', 'azure-color'];
|
||||
|
||||
export default class FormHostCatalogAwsComponent extends Component {
|
||||
// =properties
|
||||
hostCatalogTypes = types;
|
||||
/**
|
||||
* maps resource type with icon
|
||||
* @type {object}
|
||||
*/
|
||||
get mapResourceTypeWithIcon() {
|
||||
return pluginTypes.reduce(
|
||||
(obj, plugin, i) => ({ ...obj, [plugin]: icons[i] }),
|
||||
{}
|
||||
);
|
||||
}
|
||||
// =actions
|
||||
|
||||
@action
|
||||
|
||||
@@ -44,67 +44,47 @@
|
||||
</form.textarea>
|
||||
|
||||
{{#if @model.isNew}}
|
||||
<form.fieldset>
|
||||
<:title>{{t 'form.type.label'}}</:title>
|
||||
<:body>
|
||||
<form.radioGroup
|
||||
@name='types'
|
||||
@selectedValue={{@model.type}}
|
||||
@changed={{@changeType}}
|
||||
as |radioGroup|
|
||||
<Hds::Form::RadioCard::Group @name='type' as |G|>
|
||||
<G.Legend>{{t 'form.type.label'}}</G.Legend>
|
||||
{{#each this.hostCatalogTypes as |hostCatalogType|}}
|
||||
<G.RadioCard
|
||||
@value={{hostCatalogType}}
|
||||
@maxWidth='20rem'
|
||||
@checked={{eq hostCatalogType @model.type}}
|
||||
{{on 'input' (fn @changeType hostCatalogType)}}
|
||||
as |R|
|
||||
>
|
||||
<radioGroup.card
|
||||
@label={{t 'resources.host-catalog.types.static'}}
|
||||
@value='static'
|
||||
@helperText={{t 'resources.host-catalog.help.static'}}
|
||||
@layout='wide'
|
||||
/>
|
||||
<radioGroup.card
|
||||
@label={{t 'resources.host-catalog.types.dynamic'}}
|
||||
@value='plugin'
|
||||
@helperText={{t 'resources.host-catalog.help.dynamic'}}
|
||||
@layout='wide'
|
||||
/>
|
||||
</form.radioGroup>
|
||||
</:body>
|
||||
</form.fieldset>
|
||||
<Rose::Separator />
|
||||
|
||||
<form.fieldset>
|
||||
<:title>{{t 'titles.provider'}}</:title>
|
||||
</form.fieldset>
|
||||
<form.fieldset>
|
||||
<:title>{{t 'titles.choose-a-provider'}}</:title>
|
||||
<:description>
|
||||
{{concat
|
||||
<R.Label>{{t
|
||||
(concat 'resources.host-catalog.types.' hostCatalogType)
|
||||
}}</R.Label>
|
||||
<R.Description>{{t
|
||||
(concat 'resources.host-catalog.help.' hostCatalogType)
|
||||
}}</R.Description>
|
||||
</G.RadioCard>
|
||||
{{/each}}
|
||||
</Hds::Form::RadioCard::Group>
|
||||
<Hds::Form::RadioCard::Group @name='type' @alignment='center' as |G|>
|
||||
<G.Legend>{{t 'titles.provider'}}</G.Legend>
|
||||
<G.Legend>{{t 'titles.choose-a-provider'}}</G.Legend>
|
||||
<G.HelperText>{{concat
|
||||
(t 'descriptions.choose-a-provider')
|
||||
' '
|
||||
(t 'descriptions.choose-a-provider-azure')
|
||||
}}
|
||||
</:description>
|
||||
}}</G.HelperText>
|
||||
|
||||
<:body>
|
||||
<form.radioGroup
|
||||
@name='provider'
|
||||
@selectedValue={{@model.compositeType}}
|
||||
@changed={{@changeType}}
|
||||
as |radioGroup|
|
||||
{{#each-in this.mapResourceTypeWithIcon as |pluginType icon|}}
|
||||
<G.RadioCard
|
||||
@value={{pluginType}}
|
||||
@maxWidth='20rem'
|
||||
@checked={{eq pluginType @model.compositeType}}
|
||||
{{on 'input' (fn @changeType pluginType)}}
|
||||
as |R|
|
||||
>
|
||||
<radioGroup.card
|
||||
@label={{t 'resources.host-catalog.types.aws'}}
|
||||
@value='aws'
|
||||
@layout='tile'
|
||||
@icon='flight-icons/svg/aws-color-16'
|
||||
/>
|
||||
<radioGroup.card
|
||||
@label={{t 'resources.host-catalog.types.azure'}}
|
||||
@value='azure'
|
||||
@layout='tile'
|
||||
@icon='flight-icons/svg/azure-color-16'
|
||||
/>
|
||||
</form.radioGroup>
|
||||
</:body>
|
||||
</form.fieldset>
|
||||
<R.Label>{{t
|
||||
(concat 'resources.host-catalog.types.' pluginType)
|
||||
}}</R.Label>
|
||||
<R.Icon @name={{icon}} />
|
||||
</G.RadioCard>
|
||||
{{/each-in}}
|
||||
</Hds::Form::RadioCard::Group>
|
||||
{{else}}
|
||||
<InfoField
|
||||
@value={{t 'resources.host-catalog.types.azure'}}
|
||||
@@ -162,6 +142,14 @@
|
||||
readonly={{false}}
|
||||
/>
|
||||
|
||||
<form.checkbox
|
||||
@name='disable_credential_rotation'
|
||||
@checked={{@model.disable_credential_rotation}}
|
||||
@helperText={{t 'form.disable_credential_rotation.help'}}
|
||||
@label={{t 'form.disable_credential_rotation.label'}}
|
||||
@disabled={{true}}
|
||||
/>
|
||||
|
||||
{{#if (can 'save model' @model)}}
|
||||
<form.actions
|
||||
@enableEditText={{t 'actions.edit-form'}}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import Component from '@glimmer/component';
|
||||
import { types, pluginTypes } from 'api/models/host-catalog';
|
||||
|
||||
//Note: this is a temporary solution till we have resource type helper in place
|
||||
const icons = ['aws-color', 'azure-color'];
|
||||
|
||||
export default class FormAzureHostCatalogAwsComponent extends Component {
|
||||
// =properties
|
||||
hostCatalogTypes = types;
|
||||
/**
|
||||
* maps resource type with icon
|
||||
* @type {object}
|
||||
*/
|
||||
get mapResourceTypeWithIcon() {
|
||||
return pluginTypes.reduce(
|
||||
(obj, plugin, i) => ({ ...obj, [plugin]: icons[i] }),
|
||||
{}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -46,60 +46,51 @@
|
||||
</form.textarea>
|
||||
|
||||
{{#if @model.isNew}}
|
||||
<form.fieldset>
|
||||
<:title>{{t 'form.type.label'}}</:title>
|
||||
<:body>
|
||||
<form.radioGroup
|
||||
@name='types'
|
||||
@selectedValue={{@model.type}}
|
||||
@changed={{@changeType}}
|
||||
as |radioGroup|
|
||||
|
||||
<Hds::Form::RadioCard::Group @name='type' as |G|>
|
||||
<G.Legend>{{t 'form.type.label'}}</G.Legend>
|
||||
{{#each this.hostCatalogTypes as |hostCatalogType|}}
|
||||
<G.RadioCard
|
||||
@value={{hostCatalogType}}
|
||||
@maxWidth='20rem'
|
||||
@checked={{eq hostCatalogType @model.type}}
|
||||
{{on 'input' (fn @changeType hostCatalogType)}}
|
||||
as |R|
|
||||
>
|
||||
<radioGroup.card
|
||||
@label={{t 'resources.host-catalog.types.static'}}
|
||||
@value='static'
|
||||
@helperText={{t 'resources.host-catalog.help.static'}}
|
||||
@layout='wide'
|
||||
/>
|
||||
<radioGroup.card
|
||||
@label={{t 'resources.host-catalog.types.dynamic'}}
|
||||
@value='plugin'
|
||||
@helperText={{t 'resources.host-catalog.help.dynamic'}}
|
||||
@layout='wide'
|
||||
/>
|
||||
</form.radioGroup>
|
||||
</:body>
|
||||
</form.fieldset>
|
||||
<R.Label>{{t
|
||||
(concat 'resources.host-catalog.types.' hostCatalogType)
|
||||
}}</R.Label>
|
||||
<R.Description>{{t
|
||||
(concat 'resources.host-catalog.help.' hostCatalogType)
|
||||
}}</R.Description>
|
||||
</G.RadioCard>
|
||||
{{/each}}
|
||||
</Hds::Form::RadioCard::Group>
|
||||
|
||||
{{#if @model.isPlugin}}
|
||||
<Rose::Separator />
|
||||
<form.fieldset>
|
||||
<:title>{{t 'titles.provider'}}</:title>
|
||||
</form.fieldset>
|
||||
|
||||
<form.fieldset>
|
||||
<:title>{{t 'titles.choose-a-provider'}}</:title>
|
||||
<:description>{{t 'descriptions.choose-a-provider'}}</:description>
|
||||
<:body>
|
||||
<form.radioGroup
|
||||
@name='provider'
|
||||
@selectedValue={{@model.compositeType}}
|
||||
@changed={{@changeType}}
|
||||
as |radioGroup|
|
||||
<Hds::Form::RadioCard::Group @name='type' as |G|>
|
||||
<G.Legend>{{t 'titles.provider'}}</G.Legend>
|
||||
<G.Legend>{{t 'titles.choose-a-provider'}}</G.Legend>
|
||||
<G.HelperText>{{t 'descriptions.choose-a-provider'}}</G.HelperText>
|
||||
|
||||
{{#each-in this.mapResourceTypeWithIcon as |pluginType|}}
|
||||
<G.RadioCard
|
||||
@value={{pluginType}}
|
||||
@checked={{eq pluginType @model.type}}
|
||||
@maxWidth='20rem'
|
||||
{{on 'input' (fn @changeType pluginType)}}
|
||||
as |R|
|
||||
>
|
||||
<radioGroup.card
|
||||
@label={{t 'resources.host-catalog.types.aws'}}
|
||||
@value='aws'
|
||||
@layout='tile'
|
||||
/>
|
||||
<radioGroup.card
|
||||
@label={{t 'resources.host-catalog.types.azure'}}
|
||||
@value='azure'
|
||||
@layout='tile'
|
||||
/>
|
||||
</form.radioGroup>
|
||||
</:body>
|
||||
</form.fieldset>
|
||||
<R.Label>{{t
|
||||
(concat 'resources.host-catalog.types.' pluginType)
|
||||
}}</R.Label>
|
||||
<R.Description>{{t
|
||||
(concat 'resources.host-catalog.help.' pluginType)
|
||||
}}</R.Description>
|
||||
</G.RadioCard>
|
||||
{{/each-in}}
|
||||
</Hds::Form::RadioCard::Group>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import Component from '@glimmer/component';
|
||||
import { types, pluginTypes } from 'api/models/host-catalog';
|
||||
|
||||
//Note: this is a temporary solution till we have resource type helper in place
|
||||
const icons = ['aws-color', 'azure-color'];
|
||||
|
||||
export default class FormStaticHostCatalogAwsComponent extends Component {
|
||||
// =properties
|
||||
hostCatalogTypes = types;
|
||||
/**
|
||||
* maps resource type with icon
|
||||
* @type {object}
|
||||
*/
|
||||
get mapResourceTypeWithIcon() {
|
||||
return pluginTypes.reduce(
|
||||
(obj, plugin, i) => ({ ...obj, [plugin]: icons[i] }),
|
||||
{}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -53,26 +53,17 @@
|
||||
@disabled={{true}}
|
||||
/>
|
||||
|
||||
<form.fieldset>
|
||||
<:title>{{t 'titles.provider'}}</:title>
|
||||
<:description>{{t 'descriptions.provider'}}</:description>
|
||||
|
||||
<:body>
|
||||
<form.radioGroup
|
||||
@name='provider'
|
||||
@selectedValue={{@model.compositeType}}
|
||||
@disabled={{true}}
|
||||
as |radioGroup|
|
||||
>
|
||||
<radioGroup.card
|
||||
@label={{t 'resources.host-catalog.types.aws'}}
|
||||
@value={{@model.compositeType}}
|
||||
@layout='tile'
|
||||
@icon='flight-icons/svg/aws-color-16'
|
||||
/>
|
||||
</form.radioGroup>
|
||||
</:body>
|
||||
</form.fieldset>
|
||||
<InfoField
|
||||
@value={{t 'descriptions.provider'}}
|
||||
@icon='aws-color'
|
||||
disabled={{form.disabled}}
|
||||
as |F|
|
||||
>
|
||||
<F.Label>{{t 'titles.provider'}}</F.Label>
|
||||
<F.HelperText>
|
||||
{{t 'resources.host-catalog.types.aws'}}
|
||||
</F.HelperText>
|
||||
</InfoField>
|
||||
|
||||
<Rose::Form::Fieldset
|
||||
@helperText={{t 'resources.host-set.form.preferred_endpoints.help'}}
|
||||
|
||||
@@ -53,26 +53,17 @@
|
||||
@disabled={{true}}
|
||||
/>
|
||||
|
||||
<form.fieldset>
|
||||
<:title>{{t 'titles.provider'}}</:title>
|
||||
<:description>{{t 'descriptions.provider'}}</:description>
|
||||
|
||||
<:body>
|
||||
<form.radioGroup
|
||||
@name='provider'
|
||||
@selectedValue={{@model.compositeType}}
|
||||
@disabled={{true}}
|
||||
as |radioGroup|
|
||||
>
|
||||
<radioGroup.card
|
||||
@label={{t 'resources.host-catalog.types.azure'}}
|
||||
@value={{@model.compositeType}}
|
||||
@layout='tile'
|
||||
@icon='flight-icons/svg/azure-color-16'
|
||||
/>
|
||||
</form.radioGroup>
|
||||
</:body>
|
||||
</form.fieldset>
|
||||
<InfoField
|
||||
@value={{t 'descriptions.provider'}}
|
||||
@icon='azure-color'
|
||||
disabled={{form.disabled}}
|
||||
as |F|
|
||||
>
|
||||
<F.Label>{{t 'titles.provider'}}</F.Label>
|
||||
<F.HelperText>
|
||||
{{t 'resources.host-catalog.types.azure'}}
|
||||
</F.HelperText>
|
||||
</InfoField>
|
||||
|
||||
<Rose::Form::Fieldset
|
||||
@helperText={{t 'resources.host-set.form.preferred_endpoints.help'}}
|
||||
|
||||
@@ -52,24 +52,17 @@
|
||||
{{/if}}
|
||||
</form.textarea>
|
||||
|
||||
<form.fieldset>
|
||||
<:title>{{t 'titles.provider'}}</:title>
|
||||
<:body>
|
||||
<form.radioGroup
|
||||
@name='provider'
|
||||
@selectedValue={{@model.compositeType}}
|
||||
@disabled={{true}}
|
||||
as |radioGroup|
|
||||
>
|
||||
<radioGroup.card
|
||||
@label={{t 'resources.host-catalog.types.aws'}}
|
||||
@value={{@model.compositeType}}
|
||||
@layout='tile'
|
||||
@icon={{concat 'flight-icons/svg/' @model.compositeType '-color-16'}}
|
||||
/>
|
||||
</form.radioGroup>
|
||||
</:body>
|
||||
</form.fieldset>
|
||||
<InfoField
|
||||
@value={{t 'descriptions.provider'}}
|
||||
@icon='aws-color'
|
||||
disabled={{form.disabled}}
|
||||
as |F|
|
||||
>
|
||||
<F.Label>{{t 'titles.provider'}}</F.Label>
|
||||
<F.HelperText>
|
||||
{{t 'resources.host-catalog.types.aws'}}
|
||||
</F.HelperText>
|
||||
</InfoField>
|
||||
|
||||
<form.fieldset>
|
||||
<:title>{{t 'form.ip_addresses.label'}}</:title>
|
||||
|
||||
@@ -52,25 +52,17 @@
|
||||
{{/if}}
|
||||
</form.textarea>
|
||||
|
||||
<form.fieldset>
|
||||
<:title>{{t 'titles.provider'}}</:title>
|
||||
<:body>
|
||||
<form.radioGroup
|
||||
@name='provider'
|
||||
@selectedValue={{@model.compositeType}}
|
||||
@disabled={{true}}
|
||||
as |radioGroup|
|
||||
>
|
||||
<radioGroup.card
|
||||
@label={{t 'resources.host-catalog.types.azure'}}
|
||||
@value={{@model.compositeType}}
|
||||
@layout='tile'
|
||||
@icon={{concat 'flight-icons/svg/' @model.compositeType '-color-16'}}
|
||||
/>
|
||||
</form.radioGroup>
|
||||
</:body>
|
||||
</form.fieldset>
|
||||
|
||||
<InfoField
|
||||
@value={{t 'descriptions.provider'}}
|
||||
@icon='azure-color'
|
||||
disabled={{form.disabled}}
|
||||
as |F|
|
||||
>
|
||||
<F.Label>{{t 'titles.provider'}}</F.Label>
|
||||
<F.HelperText>
|
||||
{{t 'resources.host-catalog.types.azure'}}
|
||||
</F.HelperText>
|
||||
</InfoField>
|
||||
<form.fieldset>
|
||||
<:title>{{t 'form.ip_addresses.label'}}</:title>
|
||||
<:body>
|
||||
|
||||
@@ -5,26 +5,33 @@
|
||||
@onSubmit={{fn @submit this.path}}
|
||||
as |form|
|
||||
>
|
||||
<form.radioGroup
|
||||
@name='paths'
|
||||
@selectedValue={{this.path}}
|
||||
@changed={{this.toggleRadio}}
|
||||
as |radioGroup|
|
||||
>
|
||||
<div class='choose-path-cards'>
|
||||
<radioGroup.card
|
||||
@label={{t 'onboarding.quick-start.titles.quick-setup'}}
|
||||
@value='guided'
|
||||
@helperText={{t 'onboarding.quick-start.descriptions.quick-setup'}}
|
||||
@layout='tile'
|
||||
/>
|
||||
<radioGroup.card
|
||||
@label={{t 'onboarding.quick-start.titles.manual'}}
|
||||
@value='manual'
|
||||
@helperText={{t 'onboarding.quick-start.descriptions.manual'}}
|
||||
@layout='tile'
|
||||
/>
|
||||
</div>
|
||||
</form.radioGroup>
|
||||
|
||||
<Hds::Form::RadioCard::Group @name='path' as |G|>
|
||||
<G.RadioCard
|
||||
@maxWidth='20rem'
|
||||
@checked={{eq this.path 'guided'}}
|
||||
@value='guided'
|
||||
{{on 'input' (fn this.toggleRadio 'guided')}}
|
||||
as |R|
|
||||
>
|
||||
<R.Label>{{t 'onboarding.quick-start.titles.quick-setup'}}</R.Label>
|
||||
<R.Description>{{t
|
||||
'onboarding.quick-start.descriptions.quick-setup'
|
||||
}}</R.Description>
|
||||
</G.RadioCard>
|
||||
<G.RadioCard
|
||||
@maxWidth='20rem'
|
||||
@checked={{eq this.path 'manual'}}
|
||||
@value='manual'
|
||||
{{on 'input' (fn this.toggleRadio 'manual')}}
|
||||
as |R|
|
||||
>
|
||||
<R.Label>{{t 'onboarding.quick-start.titles.manual'}}</R.Label>
|
||||
<R.Description>{{t
|
||||
'onboarding.quick-start.descriptions.manual'
|
||||
}}</R.Description>
|
||||
</G.RadioCard>
|
||||
</Hds::Form::RadioCard::Group>
|
||||
|
||||
<form.actions @submitText={{t 'actions.next'}} @showCancel={{false}} />
|
||||
</Rose::Form>
|
||||
@@ -6,44 +6,47 @@
|
||||
as |form|
|
||||
>
|
||||
|
||||
<form.input
|
||||
@name='name'
|
||||
@type='name'
|
||||
<Hds::Form::TextInput::Field
|
||||
@isRequired={{false}}
|
||||
@isOptional={{true}}
|
||||
@value={{@model.name}}
|
||||
@label={{t 'form.name.label'}}
|
||||
@helperText={{t 'form.name.help'}}
|
||||
@error={{@model.errors.name}}
|
||||
readonly={{false}}
|
||||
as |field|
|
||||
@isInvalid={{@model.errors.name}}
|
||||
@type='text'
|
||||
name='name'
|
||||
disabled={{form.disabled}}
|
||||
{{on 'input' (set-from-event @model 'name')}}
|
||||
as |F|
|
||||
>
|
||||
<F.Label>{{t 'form.name.label'}}</F.Label>
|
||||
<F.HelperText>{{t 'form.name.help'}}</F.HelperText>
|
||||
{{#if @model.errors.name}}
|
||||
<field.errors as |errors|>
|
||||
<F.Error as |E|>
|
||||
{{#each @model.errors.name as |error|}}
|
||||
<errors.message>{{error.message}}</errors.message>
|
||||
<E.Message>{{error.message}}</E.Message>
|
||||
{{/each}}
|
||||
</field.errors>
|
||||
</F.Error>
|
||||
{{/if}}
|
||||
</form.input>
|
||||
</Hds::Form::TextInput::Field>
|
||||
|
||||
<form.textarea
|
||||
@name='description'
|
||||
@type='description'
|
||||
<Hds::Form::Textarea::Field
|
||||
@isRequired={{false}}
|
||||
@isOptional={{true}}
|
||||
@value={{@model.description}}
|
||||
@label={{t 'form.description.label'}}
|
||||
@helperText={{t 'form.description.help'}}
|
||||
@error={{@model.errors.description}}
|
||||
disabled={{@model.isSaving}}
|
||||
readonly={{false}}
|
||||
as |field|
|
||||
@isInvalid={{@model.errors.description}}
|
||||
name='description'
|
||||
disabled={{form.disabled}}
|
||||
as |F|
|
||||
>
|
||||
<F.Label>{{t 'form.description.label'}}</F.Label>
|
||||
<F.HelperText>{{t 'form.description.help'}}</F.HelperText>
|
||||
{{#if @model.errors.description}}
|
||||
<field.errors as |errors|>
|
||||
<F.Error as |E|>
|
||||
{{#each @model.errors.description as |error|}}
|
||||
<errors.message>{{error.message}}</errors.message>
|
||||
<E.Message>{{error.message}}</E.Message>
|
||||
{{/each}}
|
||||
</field.errors>
|
||||
</F.Error>
|
||||
{{/if}}
|
||||
</form.textarea>
|
||||
</Hds::Form::Textarea::Field>
|
||||
|
||||
{{#unless @model.isNew}}
|
||||
<fieldset
|
||||
|
||||
@@ -42,34 +42,35 @@
|
||||
</form.textarea>
|
||||
|
||||
{{#if (and @model.isNew (feature-flag 'ssh-target'))}}
|
||||
<Hds::Form::RadioCard::Group @name='types' @alignment='center' as |G|>
|
||||
<G.Legend>{{t 'form.type.label'}}</G.Legend>
|
||||
{{#each-in this.mapResourceTypeWithIcon as |targetType icon|}}
|
||||
<Hds::Form::RadioCard::Group @name='type' @alignment='center' as |G|>
|
||||
<G.Legend>{{t 'resources.target.form.type.label'}}</G.Legend>
|
||||
<G.HelperText>{{t 'resources.target.form.type.help'}}</G.HelperText>
|
||||
{{#each this.typeMetas as |type|}}
|
||||
<G.RadioCard
|
||||
@name={{targetType}}
|
||||
@value={{targetType}}
|
||||
@value={{type.type}}
|
||||
@maxWidth='20rem'
|
||||
@checked={{eq targetType @model.type}}
|
||||
{{on 'input' (fn @changeType targetType)}}
|
||||
@checked={{eq type.type @model.type}}
|
||||
{{on 'input' (fn @changeType type.type)}}
|
||||
as |R|
|
||||
>
|
||||
<R.Label>{{t (concat 'resources.target.types.' targetType)}}</R.Label>
|
||||
<R.Label>{{t (concat 'resources.target.types.' type.type)}}</R.Label>
|
||||
<R.Description>{{t
|
||||
(concat 'resources.target.help.' targetType)
|
||||
(concat 'resources.target.help.' type.type)
|
||||
}}</R.Description>
|
||||
<R.Icon @name={{icon}} />
|
||||
<R.Icon @name={{type.icon}} />
|
||||
</G.RadioCard>
|
||||
{{/each-in}}
|
||||
{{/each}}
|
||||
</Hds::Form::RadioCard::Group>
|
||||
|
||||
<br />
|
||||
{{else}}
|
||||
<InfoField
|
||||
@value={{t (concat 'resources.target.help.' @model.type)}}
|
||||
@value={{t (concat 'resources.target.types.' @model.type)}}
|
||||
@icon={{this.icon}}
|
||||
as |F|
|
||||
>
|
||||
<F.Label>{{t 'form.type.label'}}</F.Label>
|
||||
<F.HelperText>{{t (concat 'resources.target.types.' @model.type)}}
|
||||
<F.HelperText>
|
||||
{{t (concat 'resources.target.help.' @model.type)}}
|
||||
</F.HelperText>
|
||||
</InfoField>
|
||||
{{/if}}
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import Component from '@glimmer/component';
|
||||
import { types } from 'api/models/target';
|
||||
import { TYPES_TARGET } from 'api/models/target';
|
||||
|
||||
//Note: this is a temporary solution till we have resource type helper in place
|
||||
const icons = ['network', 'terminal'];
|
||||
// NOTE: this is all a temporary solution till we have a resource type helper.
|
||||
const types = [...TYPES_TARGET].reverse();
|
||||
const icons = {
|
||||
ssh: 'terminal-screen',
|
||||
tcp: 'network',
|
||||
};
|
||||
|
||||
export default class FormTargetComponent extends Component {
|
||||
// =properties
|
||||
@@ -10,8 +14,11 @@ export default class FormTargetComponent extends Component {
|
||||
* maps resource type with icon
|
||||
* @type {object}
|
||||
*/
|
||||
get mapResourceTypeWithIcon() {
|
||||
return types.reduce((obj, type, i) => ({ ...obj, [type]: icons[i] }), {});
|
||||
get typeMetas() {
|
||||
return types.map((type) => ({
|
||||
type,
|
||||
icon: icons[type],
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -20,10 +27,6 @@ export default class FormTargetComponent extends Component {
|
||||
* @type {string}
|
||||
*/
|
||||
get icon() {
|
||||
if (this.args.model.type === 'tcp') {
|
||||
return icons[0];
|
||||
} else {
|
||||
return icons[1];
|
||||
}
|
||||
return icons[this.args.model.type];
|
||||
}
|
||||
}
|
||||
@@ -108,10 +108,19 @@ ${listenerText}
|
||||
* @type {string}
|
||||
*/
|
||||
get installBoundaryText() {
|
||||
return `curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo apt-key add - ;\\
|
||||
const configPath = this.configFilePath || '<path>';
|
||||
const ossContent = `curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo apt-key add - ;\\
|
||||
sudo apt-add-repository "deb [arch=amd64] https://apt.releases.hashicorp.com $(lsb_release -cs) main" ;\\
|
||||
sudo apt-get update && sudo apt-get install boundary ;\\
|
||||
boundary server -config="${this.configFilePath || '<path>'}/pki-worker.hcl"`;
|
||||
boundary server -config="${configPath}/pki-worker.hcl"`;
|
||||
const hcpContent = `wget -q https://releases.hashicorp.com/boundary-worker/0.11.2+hcp/boundary-worker_0.11.2+hcp_linux_amd64.zip ;\\
|
||||
sudo apt-get update && sudo apt-get install unzip ;\\
|
||||
unzip *.zip ;\\
|
||||
./boundary-worker server -config="${configPath}/pki-worker.hcl"`;
|
||||
|
||||
return this.features.isEnabled('byow-pki-hcp-cluster-id')
|
||||
? hcpContent
|
||||
: ossContent;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,6 +3,7 @@ import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import { loading } from 'ember-loading';
|
||||
import { notifyError } from 'core/decorators/notify';
|
||||
import { TYPE_TARGET_TCP } from 'api/models/target';
|
||||
|
||||
export default class OnboardingQuickSetupCreateResourcesRoute extends Route {
|
||||
// =services
|
||||
@@ -40,7 +41,7 @@ export default class OnboardingQuickSetupCreateResourcesRoute extends Route {
|
||||
description: 'Sample host created by quick setup',
|
||||
}),
|
||||
target: this.store.createRecord('target', {
|
||||
type: 'tcp',
|
||||
type: TYPE_TARGET_TCP,
|
||||
name: 'EC2 Instances',
|
||||
description: 'Sample target created by quick setup',
|
||||
}),
|
||||
|
||||
@@ -29,15 +29,23 @@ export default class ScopesScopeCredentialStoresCredentialStoreCredentialsNewRou
|
||||
'scopes.scope.credential-stores.credential-store'
|
||||
);
|
||||
let name, description;
|
||||
/**
|
||||
* this sets the credential attribute json_object to a string that
|
||||
* replicates an empty json blob as CodeMirror expects a string
|
||||
*/
|
||||
const json_object = '{}';
|
||||
|
||||
if (this.currentModel?.isNew) {
|
||||
({ name, description } = this.currentModel);
|
||||
this.currentModel.rollbackAttributes();
|
||||
}
|
||||
|
||||
return this.store.createRecord('credential', {
|
||||
type,
|
||||
credential_store_id,
|
||||
name,
|
||||
description,
|
||||
json_object,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -8,60 +8,32 @@ import { notifySuccess, notifyError } from 'core/decorators/notify';
|
||||
export default class ScopesScopeRolesRolePrincipalsRoute extends Route {
|
||||
// =services
|
||||
@service intl;
|
||||
|
||||
@service resourceFilterStore;
|
||||
|
||||
// =methods
|
||||
|
||||
/**
|
||||
* Returns users and groups associated with this role.
|
||||
* Returns users, groups and managed-groups associated with this role.
|
||||
* @param {object} params
|
||||
* @return {Promise{RoleModel}}
|
||||
* @return {Promise{role, principals}}
|
||||
*/
|
||||
async model() {
|
||||
const role = this.modelFor('scopes.scope.roles.role');
|
||||
// Fetch user and group principals.
|
||||
const users = await role.users;
|
||||
const groups = await role.groups;
|
||||
const managedGroups = await role.managedGroups;
|
||||
// Merge polymorphic principals.
|
||||
const principals = [...users, ...groups, ...managedGroups];
|
||||
|
||||
// Gather user and group IDs as separate arrays, since these
|
||||
// will be queried in separate API queries.
|
||||
const userIDs = role.principals
|
||||
.filter(({ type }) => type === 'user')
|
||||
.map(({ principal_id }) => principal_id);
|
||||
const groupIDs = role.principals
|
||||
.filter(({ type }) => type === 'group')
|
||||
.map(({ principal_id }) => principal_id);
|
||||
|
||||
// Query for users.
|
||||
const users = userIDs?.length
|
||||
? (
|
||||
await this.resourceFilterStore.queryBy(
|
||||
'user',
|
||||
{ id: userIDs },
|
||||
{ scope_id: 'global', recursive: true }
|
||||
)
|
||||
).map((model) => model)
|
||||
: [];
|
||||
// Query for groups.
|
||||
const groups = groupIDs?.length
|
||||
? (
|
||||
await this.resourceFilterStore.queryBy(
|
||||
'group',
|
||||
{ id: groupIDs },
|
||||
{ scope_id: 'global', recursive: true }
|
||||
)
|
||||
).map((model) => model)
|
||||
: [];
|
||||
|
||||
return {
|
||||
role,
|
||||
principals: users.concat(groups),
|
||||
};
|
||||
return { role, principals };
|
||||
}
|
||||
|
||||
// =actions
|
||||
|
||||
/**
|
||||
* Remove a principal from the current role and redirect to principals index.
|
||||
* @param {UserModel, GroupModel} principal
|
||||
* @param {UserModel, GroupModel, ManagedGroupModel} principal
|
||||
*/
|
||||
@action
|
||||
@loading
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import Route from '@ember/routing/route';
|
||||
import { action } from '@ember/object';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { TYPE_TARGET_TCP, TYPE_TARGET_SSH } from 'api/models/target';
|
||||
|
||||
export default class ScopesScopeTargetsNewRoute extends Route {
|
||||
// =services
|
||||
|
||||
@service store;
|
||||
@service router;
|
||||
@service features;
|
||||
|
||||
// =methods
|
||||
|
||||
@@ -23,9 +25,16 @@ export default class ScopesScopeTargetsNewRoute extends Route {
|
||||
* @return {TargetModel}
|
||||
*/
|
||||
|
||||
model({ type = 'tcp' }) {
|
||||
const scopeModel = this.modelFor('scopes.scope');
|
||||
model({ type }) {
|
||||
let name, description;
|
||||
const scopeModel = this.modelFor('scopes.scope');
|
||||
|
||||
// default type is SSH if the feature is enabled, otherwise TCP
|
||||
if (!type)
|
||||
type = this.features.isEnabled('ssh-target')
|
||||
? TYPE_TARGET_SSH
|
||||
: TYPE_TARGET_TCP;
|
||||
|
||||
if (this.currentModel?.isNew) {
|
||||
({ name, description } = this.currentModel);
|
||||
this.currentModel.rollbackAttributes();
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// This file is essentially a rip of the default ember notify styles with some minor tweaks for positioning.
|
||||
|
||||
// sass-lint:disable no-vendor-prefixes
|
||||
@-webkit-keyframes 'ember-notify-show' {
|
||||
@-webkit-keyframes ember-notify-show {
|
||||
from {
|
||||
left: -310px;
|
||||
}
|
||||
@@ -10,7 +9,7 @@
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
@keyframes 'ember-notify-show' {
|
||||
@keyframes ember-notify-show {
|
||||
from {
|
||||
left: -310px;
|
||||
}
|
||||
@@ -19,7 +18,7 @@
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
@-webkit-keyframes 'ember-notify-hide' {
|
||||
@-webkit-keyframes ember-notify-hide {
|
||||
from {
|
||||
left: 0;
|
||||
}
|
||||
@@ -28,7 +27,7 @@
|
||||
left: -310px;
|
||||
}
|
||||
}
|
||||
@keyframes 'ember-notify-hide' {
|
||||
@keyframes ember-notify-hide {
|
||||
from {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// sass-lint:disable no-color-literals class-name-format
|
||||
@use 'rose/variables/sizing';
|
||||
@use 'rose/variables/media';
|
||||
@use 'rose/utilities/type';
|
||||
@@ -194,11 +193,11 @@
|
||||
}
|
||||
|
||||
mark {
|
||||
animation: fadeBackground 0.75s linear;
|
||||
animation: fade-background 0.75s linear;
|
||||
background-color: transparent;
|
||||
color: inherit;
|
||||
|
||||
@keyframes fadeBackground {
|
||||
@keyframes fade-background {
|
||||
0% {
|
||||
background-color: var(--action-subtler-1);
|
||||
}
|
||||
@@ -222,7 +221,7 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
// Spinning animation for refresh button icon
|
||||
// Spinning animation for refresh button icon
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.is-loading {
|
||||
.rose-icon {
|
||||
@@ -239,7 +238,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
//onboarding quick setup styles
|
||||
// onboarding quick setup styles
|
||||
.choose-path {
|
||||
display: block;
|
||||
width: sizing.rems(xl) * 15;
|
||||
@@ -416,7 +415,7 @@
|
||||
.worker-auth-token {
|
||||
align-items: flex-end;
|
||||
display: flex;
|
||||
gap: sizing.rems(m); // sass-lint:disable-line no-misspelled-properties
|
||||
gap: sizing.rems(m);
|
||||
}
|
||||
|
||||
.worker-success {
|
||||
@@ -427,7 +426,7 @@
|
||||
> div {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: sizing.rems(xs); // sass-lint:disable-line no-misspelled-properties
|
||||
gap: sizing.rems(xs);
|
||||
|
||||
p {
|
||||
margin-bottom: sizing.rems(xxs);
|
||||
@@ -458,7 +457,7 @@
|
||||
|
||||
.info-field {
|
||||
.hds-form-field--layout-vertical {
|
||||
grid-template-areas: 'label' 'helper-text' 'text-input';
|
||||
grid-template-areas: 'label' 'text-input' 'helper-text';
|
||||
|
||||
.hds-form-label {
|
||||
font-size: sizing.rems(m);
|
||||
@@ -466,26 +465,25 @@
|
||||
line-height: sizing.rems(m) + sizing.rems(xxs);
|
||||
}
|
||||
|
||||
.hds-form-helper-text {
|
||||
color: var(--token-form-label-color);
|
||||
font-size: sizing.rems(m);
|
||||
grid-area: helper-text;
|
||||
}
|
||||
|
||||
.hds-form-text-input {
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
color: var(--token-form-label-color);
|
||||
grid-area: text-input;
|
||||
line-height: sizing.rems(m) + sizing.rems(xxs);
|
||||
margin-top: sizing.rems(xs);
|
||||
font-size: sizing.rems(m);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.hds-form-helper-text {
|
||||
color: var(--token-form-label-color);
|
||||
grid-area: helper-text;
|
||||
}
|
||||
}
|
||||
|
||||
&.has-icon {
|
||||
.flight-icon {
|
||||
position: absolute;
|
||||
transform: translatey(sizing.rems(xl) - sizing.rems(s));
|
||||
transform: translateY(sizing.rems(xl) - sizing.rems(xs));
|
||||
}
|
||||
|
||||
.hds-form-helper-text,
|
||||
@@ -508,28 +506,25 @@
|
||||
border-radius: sizing.rems(xxs);
|
||||
height: var(--secret-editor-height);
|
||||
margin-bottom: sizing.rems(l);
|
||||
width: 100%;
|
||||
|
||||
.secret-editor-json {
|
||||
.CodeMirror {
|
||||
border-radius: sizing.rems(xxs);
|
||||
height: var(--secret-editor-height);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.CodeMirror-gutters {
|
||||
background: var(--secret-editor-gutter);
|
||||
}
|
||||
|
||||
// override CodeMirror padding to add padding to left of line numbers
|
||||
.CodeMirror-linenumber {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.secret-editor-skeleton-wrapper {
|
||||
// using ui-gray directly so it does not change when
|
||||
// user is in dark mode since the secret editor
|
||||
// does not change theme colors between light and dark
|
||||
color: #{color(ui-gray)};
|
||||
color: var(--token-color-palette-neutral-300);
|
||||
display: flex;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
@@ -581,6 +576,7 @@
|
||||
left: 50%;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
|
||||
// centers message with editor body and gutter
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 1;
|
||||
@@ -591,6 +587,7 @@
|
||||
border: sizing.rems(xxxxs) solid var(--ui-gray);
|
||||
border-radius: sizing.rems(xxxs);
|
||||
box-shadow: 0 sizing.rems(xxs) sizing.rems(xxs) var(--button-shadow);
|
||||
|
||||
// using ui-gray directly so it does not change when
|
||||
// user is in dark mode since the secret editor
|
||||
// does not change theme colors between light and dark
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
}}
|
||||
<body.row as |row|>
|
||||
<row.headerCell>
|
||||
{{#if brokeredCredentialSource.isUnknown}}
|
||||
{{#if (cannot 'read credential' brokeredCredentialSource)}}
|
||||
{{brokeredCredentialSource.displayName}}
|
||||
{{else}}
|
||||
{{#if brokeredCredentialSource.isStatic}}
|
||||
|
||||
@@ -21,7 +21,8 @@
|
||||
"lint:hbs": "ember-template-lint .",
|
||||
"lint:hbs:fix": "ember-template-lint . --fix",
|
||||
"lint:js": "eslint .",
|
||||
"lint:sass": "sass-lint -v -q",
|
||||
"lint:sass": "stylelint 'app/styles/**/*.scss'",
|
||||
"lint:sass:fix": "stylelint 'app/styles/**/*.scss' --fix",
|
||||
"format": "npm-run-all format:*",
|
||||
"format:hbs": "prettier --write 'app/**/*.hbs'",
|
||||
"format:js": "prettier --write '{app,config,stories,tests}/**/*.js' *.js",
|
||||
@@ -91,8 +92,9 @@
|
||||
"qunit": "^2.17.2",
|
||||
"qunit-dom": "^2.0.0",
|
||||
"rose": "*",
|
||||
"sass": "^1.55.0",
|
||||
"sass-lint": "^1.13.1",
|
||||
"stylelint": "^14.14.0",
|
||||
"stylelint-config-prettier-scss": "^0.0.1",
|
||||
"stylelint-config-standard-scss": "^6.0.0",
|
||||
"sinon": "^14.0.0",
|
||||
"webpack": "^5"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { visit, currentURL, find, click, fillIn } from '@ember/test-helpers';
|
||||
import { visit, currentURL, click, fillIn } from '@ember/test-helpers';
|
||||
import { setupApplicationTest } from 'ember-qunit';
|
||||
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
|
||||
import { authenticateSession } from 'ember-simple-auth/test-support';
|
||||
@@ -14,6 +14,8 @@ module(
|
||||
let getCredentialsCount;
|
||||
let getUsernamePasswordCredentialCount;
|
||||
let getUsernameKeyPairCredentialCount;
|
||||
let getJsonCredentialCount;
|
||||
let featuresService;
|
||||
|
||||
const instances = {
|
||||
scopes: {
|
||||
@@ -33,6 +35,7 @@ module(
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
// Generate resources
|
||||
featuresService = this.owner.lookup('service:features');
|
||||
instances.scopes.org = this.server.create('scope', {
|
||||
type: 'org',
|
||||
scope: { id: 'global', type: 'global' },
|
||||
@@ -50,7 +53,7 @@ module(
|
||||
urls.credentialStores = `${urls.projectScope}/credential-stores`;
|
||||
urls.staticCredentialStore = `${urls.credentialStores}/${instances.staticCredentialStore.id}`;
|
||||
urls.credentials = `${urls.staticCredentialStore}/credentials`;
|
||||
urls.newCredential = `${urls.staticCredentialStore}/credentials/new`;
|
||||
urls.newCredential = `${urls.credentials}/new`;
|
||||
// Generate resource counter
|
||||
getCredentialsCount = () => {
|
||||
return this.server.schema.credentials.all().models.length;
|
||||
@@ -64,17 +67,23 @@ module(
|
||||
return this.server.schema.credentials.where({ type: 'ssh_private_key' })
|
||||
.length;
|
||||
};
|
||||
getJsonCredentialCount = () => {
|
||||
return this.server.schema.credentials.where({ type: 'json' }).length;
|
||||
};
|
||||
authenticateSession({});
|
||||
});
|
||||
|
||||
test('Users can create a new username & password credential', async function (assert) {
|
||||
test('users can create a new username & password credential', async function (assert) {
|
||||
assert.expect(2);
|
||||
const credentialsCount = getCredentialsCount();
|
||||
const usernamePasswordCredentialCount =
|
||||
getUsernamePasswordCredentialCount();
|
||||
await visit(urls.newCredential);
|
||||
await visit(urls.credentials);
|
||||
|
||||
await click(`[href="${urls.newCredential}"]`);
|
||||
await fillIn('[name="name"]', 'random string');
|
||||
await click('[type="submit"]');
|
||||
|
||||
assert.strictEqual(getCredentialsCount(), credentialsCount + 1);
|
||||
assert.strictEqual(
|
||||
getUsernamePasswordCredentialCount(),
|
||||
@@ -82,15 +91,18 @@ module(
|
||||
);
|
||||
});
|
||||
|
||||
test('Users can create a new username & key pair credential', async function (assert) {
|
||||
test('users can create a new username & key pair credential', async function (assert) {
|
||||
assert.expect(2);
|
||||
const credentialsCount = getCredentialsCount();
|
||||
const usernameKeyPairCredentialCount =
|
||||
getUsernameKeyPairCredentialCount();
|
||||
await visit(urls.newCredential);
|
||||
await visit(urls.credentials);
|
||||
|
||||
await click(`[href="${urls.newCredential}"]`);
|
||||
await fillIn('[name="name"]', 'random string');
|
||||
await click('[value="ssh_private_key"]');
|
||||
await click('[type="submit"]');
|
||||
|
||||
assert.strictEqual(getCredentialsCount(), credentialsCount + 1);
|
||||
assert.strictEqual(
|
||||
getUsernameKeyPairCredentialCount(),
|
||||
@@ -98,42 +110,85 @@ module(
|
||||
);
|
||||
});
|
||||
|
||||
test('Users can cancel create new username & password credential', async function (assert) {
|
||||
test('users can create a new json credential', async function (assert) {
|
||||
assert.expect(2);
|
||||
const credentialsCount = getCredentialsCount();
|
||||
await visit(urls.newCredential);
|
||||
const jsonCredentialCount = getJsonCredentialCount();
|
||||
await visit(urls.credentials);
|
||||
|
||||
await click(`[href="${urls.newCredential}"]`);
|
||||
await fillIn('[name="name"]', 'random string');
|
||||
await click('[value="json"]');
|
||||
await click('[type="submit"]');
|
||||
|
||||
assert.strictEqual(getCredentialsCount(), credentialsCount + 1);
|
||||
assert.strictEqual(getJsonCredentialCount(), jsonCredentialCount + 1);
|
||||
});
|
||||
|
||||
test('users can cancel create new username & password credential', async function (assert) {
|
||||
assert.expect(2);
|
||||
const credentialsCount = getCredentialsCount();
|
||||
await visit(urls.credentials);
|
||||
|
||||
await click(`[href="${urls.newCredential}"]`);
|
||||
await fillIn('[name="name"]', 'random string');
|
||||
await click('.rose-form-actions [type="button"]');
|
||||
|
||||
assert.strictEqual(currentURL(), urls.credentials);
|
||||
assert.strictEqual(getCredentialsCount(), credentialsCount);
|
||||
});
|
||||
|
||||
test('Users can cancel create new username & key pair credential', async function (assert) {
|
||||
test('users can cancel create new username & key pair credential', async function (assert) {
|
||||
assert.expect(2);
|
||||
const credentialsCount = getCredentialsCount();
|
||||
await visit(urls.newCredential);
|
||||
await visit(urls.credentials);
|
||||
|
||||
await click(`[href="${urls.newCredential}"]`);
|
||||
await fillIn('[name="name"]', 'random string');
|
||||
await click('[value="ssh_private_key"]');
|
||||
await click('.rose-form-actions [type="button"]');
|
||||
|
||||
assert.strictEqual(currentURL(), urls.credentials);
|
||||
assert.strictEqual(getCredentialsCount(), credentialsCount);
|
||||
});
|
||||
|
||||
test('Users cannot navigate to new credential route without proper authorization', async function (assert) {
|
||||
test('users can cancel create new json credential', async function (assert) {
|
||||
assert.expect(2);
|
||||
const credentialsCount = getCredentialsCount();
|
||||
await visit(urls.credentials);
|
||||
|
||||
await click(`[href="${urls.newCredential}"]`);
|
||||
await fillIn('[name="name"]', 'random string');
|
||||
await click('[value="json"]');
|
||||
await click('.rose-form-actions [type="button"]');
|
||||
|
||||
assert.strictEqual(currentURL(), urls.credentials);
|
||||
assert.strictEqual(getCredentialsCount(), credentialsCount);
|
||||
});
|
||||
|
||||
test('users cannot navigate to new credential route without proper authorization', async function (assert) {
|
||||
assert.expect(2);
|
||||
instances.staticCredentialStore.authorized_collection_actions.credentials =
|
||||
[];
|
||||
await visit(urls.staticCredentialStore);
|
||||
assert.notOk(
|
||||
instances.staticCredentialStore.authorized_collection_actions.credentials.filter(
|
||||
(item) => {
|
||||
item !== 'create';
|
||||
}
|
||||
);
|
||||
await visit(urls.credentialStores);
|
||||
|
||||
await click(`[href="${urls.staticCredentialStore}"]`);
|
||||
|
||||
assert.false(
|
||||
instances.staticCredentialStore.authorized_collection_actions.credentials.includes(
|
||||
'create'
|
||||
)
|
||||
);
|
||||
assert.notOk(find(`[href="${urls.newCredential}"]`));
|
||||
assert.dom('.rose-layout-page-actions a').doesNotExist();
|
||||
});
|
||||
|
||||
test('saving a new username & password credential with invalid fields displays error messages', async function (assert) {
|
||||
assert.expect(2);
|
||||
await visit(urls.credentials);
|
||||
this.server.post('/credentials', () => {
|
||||
return new Response(
|
||||
400,
|
||||
@@ -154,20 +209,21 @@ module(
|
||||
}
|
||||
);
|
||||
});
|
||||
await visit(urls.newCredential);
|
||||
|
||||
await click(`[href="${urls.newCredential}"]`);
|
||||
await click('[type="submit"]');
|
||||
assert.ok(
|
||||
find('[role="alert"]').textContent.trim(),
|
||||
'Error in provided request.'
|
||||
);
|
||||
assert.ok(
|
||||
find('.rose-form-error-message').textContent.trim(),
|
||||
'Field required for creating a username-password credential.'
|
||||
);
|
||||
|
||||
assert
|
||||
.dom('.rose-notification-body')
|
||||
.hasText('Error in provided request.');
|
||||
assert
|
||||
.dom('.rose-form-error-message')
|
||||
.hasText('Field required for creating a username-password credential.');
|
||||
});
|
||||
|
||||
test('saving a new username & key pair credential with invalid fields displays error messages', async function (assert) {
|
||||
assert.expect(2);
|
||||
await visit(urls.credentials);
|
||||
this.server.post('/credentials', () => {
|
||||
return new Response(
|
||||
400,
|
||||
@@ -188,17 +244,70 @@ module(
|
||||
}
|
||||
);
|
||||
});
|
||||
await visit(urls.newCredential);
|
||||
|
||||
await click(`[href="${urls.newCredential}"]`);
|
||||
await click('[value="ssh_private_key"]');
|
||||
await click('[type="submit"]');
|
||||
assert.ok(
|
||||
find('[role="alert"]').textContent.trim(),
|
||||
'Error in provided request.'
|
||||
);
|
||||
assert.ok(
|
||||
find('.rose-form-error-message').textContent.trim(),
|
||||
'Field required for creating a username-key-pair credential.'
|
||||
|
||||
assert
|
||||
.dom('.rose-notification-body')
|
||||
.hasText('Error in provided request.');
|
||||
assert
|
||||
.dom('.rose-form-error-message')
|
||||
.hasText('Field required for creating a username-key-pair credential.');
|
||||
});
|
||||
|
||||
test('saving a new json credential with invalid fields displays error messages', async function (assert) {
|
||||
assert.expect(1);
|
||||
await visit(urls.credentials);
|
||||
this.server.post('/credentials', () => {
|
||||
return new Response(
|
||||
400,
|
||||
{},
|
||||
{
|
||||
status: 400,
|
||||
code: 'invalid_argument',
|
||||
message: 'Error in provided request.',
|
||||
details: {
|
||||
request_fields: [
|
||||
{
|
||||
name: 'attributes.json_object',
|
||||
description: 'Field required for creating a json credential.',
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
await click(`[href="${urls.newCredential}"]`);
|
||||
await click('[value="json"]');
|
||||
await click('[type="submit"]');
|
||||
|
||||
assert
|
||||
.dom('.rose-notification-body')
|
||||
.hasText('Error in provided request.');
|
||||
});
|
||||
|
||||
test('cannot navigate to json credential when feature is disabled', async function (assert) {
|
||||
featuresService.disable('json-credentials');
|
||||
assert.expect(4);
|
||||
await visit(urls.credentials);
|
||||
|
||||
await click(`[href="${urls.newCredential}"]`);
|
||||
|
||||
assert.true(
|
||||
instances.staticCredentialStore.authorized_collection_actions.credentials.includes(
|
||||
'create'
|
||||
)
|
||||
);
|
||||
assert.dom('.hds-form-radio-card').exists({ count: 2 });
|
||||
assert
|
||||
.dom('.hds-form-radio-card:first-child input')
|
||||
.hasAttribute('value', 'username_password');
|
||||
assert
|
||||
.dom('.hds-form-radio-card:nth-child(2) input')
|
||||
.hasAttribute('value', 'ssh_private_key');
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -13,6 +13,7 @@ module(
|
||||
|
||||
let getUsernamePasswordCredentialCount;
|
||||
let getUsernameKeyPairCredentialCount;
|
||||
let getJSONCredentialCount;
|
||||
|
||||
const instances = {
|
||||
scopes: {
|
||||
@@ -28,6 +29,7 @@ module(
|
||||
credentials: null,
|
||||
usernamePasswordCredential: null,
|
||||
usernameKeyPairCredential: null,
|
||||
jsonCredential: null,
|
||||
};
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
@@ -54,6 +56,11 @@ module(
|
||||
credentialStore: instances.staticCredentialStore,
|
||||
type: 'ssh_private_key',
|
||||
});
|
||||
instances.jsonCredential = this.server.create('credential', {
|
||||
scope: instances.scopes.project,
|
||||
credentialStore: instances.staticCredentialStore,
|
||||
type: 'json',
|
||||
});
|
||||
// Generate route URLs for resources
|
||||
urls.projectScope = `/scopes/${instances.scopes.project.id}`;
|
||||
urls.credentialStores = `${urls.projectScope}/credential-stores`;
|
||||
@@ -61,6 +68,7 @@ module(
|
||||
urls.credentials = `${urls.staticCredentialStore}/credentials`;
|
||||
urls.usernamePasswordCredential = `${urls.credentials}/${instances.usernamePasswordCredential.id}`;
|
||||
urls.usernameKeyPairCredential = `${urls.credentials}/${instances.usernameKeyPairCredential.id}`;
|
||||
urls.jsonCredential = `${urls.credentials}/${instances.jsonCredential.id}`;
|
||||
// Generate resource counter
|
||||
getUsernamePasswordCredentialCount = () => {
|
||||
return this.server.schema.credentials.where({
|
||||
@@ -71,6 +79,9 @@ module(
|
||||
return this.server.schema.credentials.where({ type: 'ssh_private_key' })
|
||||
.length;
|
||||
};
|
||||
getJSONCredentialCount = () => {
|
||||
return this.server.schema.credentials.where({ type: 'json' }).length;
|
||||
};
|
||||
authenticateSession({});
|
||||
});
|
||||
|
||||
@@ -100,6 +111,15 @@ module(
|
||||
);
|
||||
});
|
||||
|
||||
test('can delete JSON credential', async function (assert) {
|
||||
assert.expect(2);
|
||||
const jsonCredentialCount = getJSONCredentialCount();
|
||||
await visit(urls.jsonCredential);
|
||||
await click('.rose-layout-page-actions .rose-dropdown-button-danger');
|
||||
assert.strictEqual(currentURL(), urls.credentials);
|
||||
assert.strictEqual(getJSONCredentialCount(), jsonCredentialCount - 1);
|
||||
});
|
||||
|
||||
test('cannot delete a username & password credential without proper authorization', async function (assert) {
|
||||
assert.expect(3);
|
||||
const usernamePasswordCredentialCount =
|
||||
@@ -138,6 +158,21 @@ module(
|
||||
);
|
||||
});
|
||||
|
||||
test('cannot delete a JSON credential without proper authorization', async function (assert) {
|
||||
assert.expect(3);
|
||||
const jsonCredentialCount = getJSONCredentialCount();
|
||||
instances.jsonCredential.authorized_actions =
|
||||
instances.jsonCredential.authorized_actions.filter(
|
||||
(item) => item !== 'delete'
|
||||
);
|
||||
await visit(urls.jsonCredential);
|
||||
assert.strictEqual(currentURL(), urls.jsonCredential);
|
||||
assert
|
||||
.dom('.rose-layout-page-actions .rose-dropdown-button-danger')
|
||||
.doesNotExist();
|
||||
assert.strictEqual(getJSONCredentialCount(), jsonCredentialCount);
|
||||
});
|
||||
|
||||
test('can accept delete username & password credential via dialog', async function (assert) {
|
||||
assert.expect(2);
|
||||
const confirmService = this.owner.lookup('service:confirm');
|
||||
@@ -170,6 +205,18 @@ module(
|
||||
);
|
||||
});
|
||||
|
||||
test('can accept delete JSON credential via dialog', async function (assert) {
|
||||
assert.expect(2);
|
||||
const confirmService = this.owner.lookup('service:confirm');
|
||||
confirmService.enabled = true;
|
||||
const jsonCredentialCount = getJSONCredentialCount();
|
||||
await visit(urls.jsonCredential);
|
||||
await click('.rose-layout-page-actions .rose-dropdown-button-danger');
|
||||
await click('.rose-dialog footer .rose-button-primary');
|
||||
assert.strictEqual(currentURL(), urls.credentials);
|
||||
assert.strictEqual(getJSONCredentialCount(), jsonCredentialCount - 1);
|
||||
});
|
||||
|
||||
test('can cancel delete username & password credential via dialog', async function (assert) {
|
||||
assert.expect(2);
|
||||
const confirmService = this.owner.lookup('service:confirm');
|
||||
@@ -202,6 +249,18 @@ module(
|
||||
);
|
||||
});
|
||||
|
||||
test('can cancel delete JSON credential via dialog', async function (assert) {
|
||||
assert.expect(2);
|
||||
const confirmService = this.owner.lookup('service:confirm');
|
||||
confirmService.enabled = true;
|
||||
const jsonCredentialCount = getJSONCredentialCount();
|
||||
await visit(urls.jsonCredential);
|
||||
await click('.rose-layout-page-actions .rose-dropdown-button-danger');
|
||||
await click('.rose-dialog footer .rose-button-secondary');
|
||||
assert.strictEqual(currentURL(), urls.jsonCredential);
|
||||
assert.strictEqual(getJSONCredentialCount(), jsonCredentialCount);
|
||||
});
|
||||
|
||||
test('deleting a username & password credential which errors displays error message', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.del('/credentials/:id', () => {
|
||||
@@ -237,5 +296,23 @@ module(
|
||||
await click('.rose-layout-page-actions .rose-dropdown-button-danger');
|
||||
assert.ok(find('[role="alert"]').textContent.trim(), 'Oops.');
|
||||
});
|
||||
|
||||
test('deleting a JSON credential which errors displays error message', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.del('/credentials/:id', () => {
|
||||
return new Response(
|
||||
490,
|
||||
{},
|
||||
{
|
||||
status: 490,
|
||||
code: 'error',
|
||||
message: 'Oops.',
|
||||
}
|
||||
);
|
||||
});
|
||||
await visit(urls.jsonCredential);
|
||||
await click('.rose-layout-page-actions .rose-dropdown-button-danger');
|
||||
assert.ok(find('[role="alert"]').textContent.trim(), 'Oops.');
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -16,6 +16,7 @@ module('Acceptance | credential-stores | credentials | list', function (hooks) {
|
||||
staticCredentialStore: null,
|
||||
usernamePasswordCredential: null,
|
||||
usernameKeyPairCredential: null,
|
||||
jsonCredential: null,
|
||||
};
|
||||
|
||||
const urls = {
|
||||
@@ -50,6 +51,11 @@ module('Acceptance | credential-stores | credentials | list', function (hooks) {
|
||||
credentialStore: instances.staticCredentialStore,
|
||||
type: 'ssh_private_key',
|
||||
});
|
||||
instances.jsonCredential = this.server.create('credential', {
|
||||
scope: instances.scopes.project,
|
||||
credentialStore: instances.staticCredentialStore,
|
||||
type: 'json',
|
||||
});
|
||||
// Generate route URLs for resources
|
||||
urls.projectScope = `/scopes/${instances.scopes.project.id}`;
|
||||
urls.credentialStores = `${urls.projectScope}/credential-stores`;
|
||||
@@ -75,7 +81,7 @@ module('Acceptance | credential-stores | credentials | list', function (hooks) {
|
||||
.isVisible({ count: this.server.schema.credentials.all().models.length });
|
||||
});
|
||||
|
||||
test('User cannot navigate to index without either list or create action', async function (assert) {
|
||||
test('User cannot navigate to Credentials tab without either list or create action', async function (assert) {
|
||||
assert.expect(3);
|
||||
instances.staticCredentialStore.authorized_collection_actions.credentials =
|
||||
[];
|
||||
@@ -93,7 +99,7 @@ module('Acceptance | credential-stores | credentials | list', function (hooks) {
|
||||
assert.dom('.rose-nav-tabs a:nth-child(2)').doesNotExist();
|
||||
});
|
||||
|
||||
test('User can navigate to index with only create action', async function (assert) {
|
||||
test('User can navigate to new credential screen with only create action', async function (assert) {
|
||||
assert.expect(2);
|
||||
instances.staticCredentialStore.authorized_collection_actions.credentials =
|
||||
['create'];
|
||||
|
||||
@@ -17,6 +17,7 @@ module('Acceptance | credential-stores | credentials | read', function (hooks) {
|
||||
staticCredentialStore: null,
|
||||
usernamePasswordCredential: null,
|
||||
usernameKeyPairCredential: null,
|
||||
jsonCredential: null,
|
||||
};
|
||||
|
||||
const urls = {
|
||||
@@ -26,6 +27,7 @@ module('Acceptance | credential-stores | credentials | read', function (hooks) {
|
||||
credentials: null,
|
||||
usernamePasswordCredential: null,
|
||||
usernameKeyPairCredential: null,
|
||||
jsonCredential: null,
|
||||
unknownCredential: null,
|
||||
};
|
||||
|
||||
@@ -53,6 +55,11 @@ module('Acceptance | credential-stores | credentials | read', function (hooks) {
|
||||
credentialStore: instances.staticCredentialStore,
|
||||
type: 'ssh_private_key',
|
||||
});
|
||||
instances.jsonCredential = this.server.create('credential', {
|
||||
scope: instances.scopes.project,
|
||||
credentialStore: instances.staticCredentialStore,
|
||||
type: 'json',
|
||||
});
|
||||
// Generate route URLs for resources
|
||||
urls.projectScope = `/scopes/${instances.scopes.project.id}`;
|
||||
urls.credentialStores = `${urls.projectScope}/credential-stores`;
|
||||
@@ -60,6 +67,7 @@ module('Acceptance | credential-stores | credentials | read', function (hooks) {
|
||||
urls.credentials = `${urls.staticCredentialStore}/credentials`;
|
||||
urls.usernamePasswordCredential = `${urls.credentials}/${instances.usernamePasswordCredential.id}`;
|
||||
urls.usernameKeyPairCredential = `${urls.credentials}/${instances.usernameKeyPairCredential.id}`;
|
||||
urls.jsonCredential = `${urls.credentials}/${instances.jsonCredential.id}`;
|
||||
urls.unknownCredential = `${urls.credentials}/foo`;
|
||||
authenticateSession({});
|
||||
});
|
||||
@@ -86,6 +94,17 @@ module('Acceptance | credential-stores | credentials | read', function (hooks) {
|
||||
assert.strictEqual(currentURL(), urls.usernameKeyPairCredential);
|
||||
});
|
||||
|
||||
test('visiting JSON credential', async function (assert) {
|
||||
assert.expect(2);
|
||||
await visit(urls.staticCredentialStore);
|
||||
await click(`[href="${urls.credentials}"]`);
|
||||
await a11yAudit();
|
||||
assert.strictEqual(currentURL(), urls.credentials);
|
||||
await click(`[href="${urls.jsonCredential}"]`);
|
||||
await a11yAudit();
|
||||
assert.strictEqual(currentURL(), urls.jsonCredential);
|
||||
});
|
||||
|
||||
test('cannot navigate to a username & password credential form without proper authorization', async function (assert) {
|
||||
assert.expect(1);
|
||||
instances.usernamePasswordCredential.authorized_actions =
|
||||
@@ -106,6 +125,24 @@ module('Acceptance | credential-stores | credentials | read', function (hooks) {
|
||||
assert.dom('.rose-table-body tr:nth-child(2) a').doesNotExist();
|
||||
});
|
||||
|
||||
test('cannot navigate to a JSON credential form without proper authorization', async function (assert) {
|
||||
assert.expect(1);
|
||||
instances.jsonCredential.authorized_actions =
|
||||
instances.jsonCredential.authorized_actions.filter(
|
||||
(item) => item != 'read'
|
||||
);
|
||||
await visit(urls.credentials);
|
||||
assert.dom('.rose-table-body tr:nth-child(3) a').doesNotExist();
|
||||
});
|
||||
|
||||
test('cannot navigate to a JSON credential form when feature not enabled', async function (assert) {
|
||||
assert.expect(1);
|
||||
const featuresService = this.owner.lookup('service:features');
|
||||
featuresService.disable('json-credentials');
|
||||
await visit(urls.credentials);
|
||||
assert.dom('.rose-table-body tr:nth-child(3) a').doesNotExist();
|
||||
});
|
||||
|
||||
test('visiting an unknown credential displays 404 message', async function (assert) {
|
||||
assert.expect(1);
|
||||
await visit(urls.unknownCredential);
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { visit, currentURL, find, click, fillIn } from '@ember/test-helpers';
|
||||
import {
|
||||
visit,
|
||||
currentURL,
|
||||
find,
|
||||
click,
|
||||
fillIn,
|
||||
waitUntil,
|
||||
} from '@ember/test-helpers';
|
||||
import { setupApplicationTest } from 'ember-qunit';
|
||||
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
|
||||
import { Response } from 'miragejs';
|
||||
@@ -20,6 +27,7 @@ module(
|
||||
staticCredentialStore: null,
|
||||
usernamePasswordCredential: null,
|
||||
usernameKeyPairCredential: null,
|
||||
jsonCredential: null,
|
||||
};
|
||||
|
||||
const urls = {
|
||||
@@ -29,6 +37,7 @@ module(
|
||||
credentials: null,
|
||||
usernamePasswordCredential: null,
|
||||
usernameKeyPairCredential: null,
|
||||
jsonCredential: null,
|
||||
};
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
@@ -56,6 +65,11 @@ module(
|
||||
credentialStore: instances.staticCredentialStore,
|
||||
type: 'ssh_private_key',
|
||||
});
|
||||
instances.jsonCredential = this.server.create('credential', {
|
||||
scope: instances.scopes.project,
|
||||
credentialStore: instances.staticCredentialStore,
|
||||
type: 'json',
|
||||
});
|
||||
// Generate route URLs for resources
|
||||
urls.projectScope = `/scopes/${instances.scopes.project.id}`;
|
||||
urls.credentialStores = `${urls.projectScope}/credential-stores`;
|
||||
@@ -63,6 +77,7 @@ module(
|
||||
urls.credentials = `${urls.staticCredentialStore}/credentials`;
|
||||
urls.usernamePasswordCredential = `${urls.credentials}/${instances.usernamePasswordCredential.id}`;
|
||||
urls.usernameKeyPairCredential = `${urls.credentials}/${instances.usernameKeyPairCredential.id}`;
|
||||
urls.jsonCredential = `${urls.credentials}/${instances.jsonCredential.id}`;
|
||||
authenticateSession({});
|
||||
});
|
||||
|
||||
@@ -98,6 +113,21 @@ module(
|
||||
);
|
||||
});
|
||||
|
||||
test('can save changes to existing JSON credential', async function (assert) {
|
||||
assert.expect(3);
|
||||
const mockInput = 'random string';
|
||||
assert.notEqual(instances.jsonCredential.name, mockInput);
|
||||
await visit(urls.jsonCredential);
|
||||
await click('form [type="button"]', 'Activate edit mode');
|
||||
await fillIn('[name="name"]', mockInput);
|
||||
await click('.rose-form-actions [type="submit"]');
|
||||
assert.strictEqual(currentURL(), urls.jsonCredential);
|
||||
assert.strictEqual(
|
||||
this.server.schema.credentials.where({ type: 'json' }).models[0].name,
|
||||
mockInput
|
||||
);
|
||||
});
|
||||
|
||||
test('cannot make changes to an existing username & password credential without proper authorization', async function (assert) {
|
||||
assert.expect(1);
|
||||
instances.usernamePasswordCredential.authorized_actions =
|
||||
@@ -122,6 +152,18 @@ module(
|
||||
.doesNotExist();
|
||||
});
|
||||
|
||||
test('cannot make changes to an existing JSON credential without proper authorization', async function (assert) {
|
||||
assert.expect(1);
|
||||
instances.jsonCredential.authorized_actions =
|
||||
instances.jsonCredential.authorized_actions.filter(
|
||||
(item) => item !== 'update'
|
||||
);
|
||||
await visit(urls.jsonCredential);
|
||||
assert
|
||||
.dom('.rose-layout-page-actions .rose-button-secondary')
|
||||
.doesNotExist();
|
||||
});
|
||||
|
||||
test('can cancel changes to existing username & password credential', async function (assert) {
|
||||
assert.expect(2);
|
||||
const mockInput = 'random string';
|
||||
@@ -150,6 +192,20 @@ module(
|
||||
);
|
||||
});
|
||||
|
||||
test('can cancel changes to existing JSON credential', async function (assert) {
|
||||
assert.expect(2);
|
||||
const mockInput = 'random string';
|
||||
await visit(urls.jsonCredential);
|
||||
await click('form [type="button"]', 'Activate edit mode');
|
||||
await fillIn('[name="name"]', mockInput);
|
||||
await click('.rose-form-actions [type="button"]');
|
||||
assert.notEqual(instances.jsonCredential.name, mockInput);
|
||||
assert.strictEqual(
|
||||
find('[name="name"]').value,
|
||||
instances.jsonCredential.name
|
||||
);
|
||||
});
|
||||
|
||||
test('saving an existing username & password credential with invalid fields displays error message', async function (assert) {
|
||||
assert.expect(2);
|
||||
const mockInput = 'random string';
|
||||
@@ -222,6 +278,42 @@ module(
|
||||
);
|
||||
});
|
||||
|
||||
test('saving an existing JSON credential with invalid fields displays error message', async function (assert) {
|
||||
assert.expect(2);
|
||||
const mockInput = 'random string';
|
||||
this.server.patch('/credentials/:id', () => {
|
||||
return new Response(
|
||||
400,
|
||||
{},
|
||||
{
|
||||
status: 400,
|
||||
code: 'invalid_argument',
|
||||
message: 'Error in provided request.',
|
||||
details: {
|
||||
request_fields: [
|
||||
{
|
||||
name: 'name',
|
||||
description: 'Name is required.',
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
await visit(urls.jsonCredential);
|
||||
await click('form [type="button"]', 'Activate edit mode');
|
||||
await fillIn('[name="name"]', mockInput);
|
||||
await click('[type="submit"]');
|
||||
assert.ok(
|
||||
find('[role="alert"]').textContent.trim(),
|
||||
'Error in provided request.'
|
||||
);
|
||||
assert.ok(
|
||||
find('.rose-form-error-message').textContent.trim(),
|
||||
'Name is required.'
|
||||
);
|
||||
});
|
||||
|
||||
test('can discard unsaved username & password credential changes via dialog', async function (assert) {
|
||||
assert.expect(5);
|
||||
const mockInput = 'random string';
|
||||
@@ -236,7 +328,10 @@ module(
|
||||
await visit(urls.credentials);
|
||||
} catch (e) {
|
||||
assert.dom('.rose-dialog').isVisible();
|
||||
await click('.rose-dialog-footer button:first-child', 'Click Discard');
|
||||
await click(
|
||||
'.rose-dialog-footer button:first-of-type',
|
||||
'Click Discard'
|
||||
);
|
||||
assert.strictEqual(currentURL(), urls.credentials);
|
||||
assert.notEqual(
|
||||
this.server.schema.credentials.where({ type: 'username_password' })
|
||||
@@ -260,7 +355,10 @@ module(
|
||||
await visit(urls.credentials);
|
||||
} catch (e) {
|
||||
assert.dom('.rose-dialog').isVisible();
|
||||
await click('.rose-dialog-footer button:first-child', 'Click Discard');
|
||||
await click(
|
||||
'.rose-dialog-footer button:first-of-type',
|
||||
'Click Discard'
|
||||
);
|
||||
assert.strictEqual(currentURL(), urls.credentials);
|
||||
assert.notEqual(
|
||||
this.server.schema.credentials.where({ type: 'ssh_private_key' })
|
||||
@@ -270,6 +368,32 @@ module(
|
||||
}
|
||||
});
|
||||
|
||||
test('can discard unsaved JSON credential changes via dialog', async function (assert) {
|
||||
assert.expect(5);
|
||||
const mockInput = 'random string';
|
||||
const confirmService = this.owner.lookup('service:confirm');
|
||||
confirmService.enabled = true;
|
||||
assert.notEqual(instances.jsonCredential.name, mockInput);
|
||||
await visit(urls.jsonCredential);
|
||||
await click('form [type="button"]', 'Activate edit mode');
|
||||
await fillIn('[name="name"]', mockInput);
|
||||
assert.strictEqual(currentURL(), urls.jsonCredential);
|
||||
try {
|
||||
await visit(urls.credentials);
|
||||
} catch (e) {
|
||||
assert.dom('.rose-dialog').isVisible();
|
||||
await click(
|
||||
'.rose-dialog-footer button:first-of-type',
|
||||
'Click Discard'
|
||||
);
|
||||
assert.strictEqual(currentURL(), urls.credentials);
|
||||
assert.notEqual(
|
||||
this.server.schema.credentials.where({ type: 'json' }).models[0].name,
|
||||
mockInput
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('can cancel discard unsaved username & password credential changes via dialog', async function (assert) {
|
||||
assert.expect(6);
|
||||
const mockInput = 'random string';
|
||||
@@ -322,6 +446,31 @@ module(
|
||||
}
|
||||
});
|
||||
|
||||
test('can cancel discard unsaved JSON credential changes via dialog', async function (assert) {
|
||||
assert.expect(6);
|
||||
const mockInput = 'random string';
|
||||
const confirmService = this.owner.lookup('service:confirm');
|
||||
confirmService.enabled = true;
|
||||
assert.notEqual(instances.jsonCredential.name, mockInput);
|
||||
await visit(urls.jsonCredential);
|
||||
await click('form [type="button"]', 'Activate edit mode');
|
||||
const credentialName = find('[name="name"]').value;
|
||||
await fillIn('[name="name"]', mockInput);
|
||||
assert.strictEqual(currentURL(), urls.jsonCredential);
|
||||
try {
|
||||
await visit(urls.credentials);
|
||||
} catch (e) {
|
||||
assert.dom('.rose-dialog').isVisible();
|
||||
await click('.rose-dialog-footer button:last-child', 'Click Cancel');
|
||||
assert.strictEqual(currentURL(), urls.jsonCredential);
|
||||
assert.strictEqual(find('[name="name"]').value, mockInput);
|
||||
assert.strictEqual(
|
||||
this.server.schema.credentials.where({ type: 'json' }).models[0].name,
|
||||
credentialName
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('password field renders in edit mode only for a username & password credential', async function (assert) {
|
||||
assert.expect(3);
|
||||
await visit(urls.usernamePasswordCredential);
|
||||
@@ -341,5 +490,28 @@ module(
|
||||
assert.dom('[name="private_key"]').isVisible();
|
||||
assert.dom('[name="private_key_passphrase"]').isVisible();
|
||||
});
|
||||
|
||||
test('secret editor is in actionable state when entering edit mode of a JSON credential', async function (assert) {
|
||||
assert.expect(3);
|
||||
await visit(urls.jsonCredential);
|
||||
assert.dom('.secret-editor-skeleton-message button').doesNotExist();
|
||||
await click('form [type="button"]', 'Activate edit mode');
|
||||
assert.strictEqual(currentURL(), urls.jsonCredential);
|
||||
assert.dom('.secret-editor-skeleton-message button').isVisible();
|
||||
});
|
||||
|
||||
test('secret editor enters editing state when clicking edit button in the secret editor of a JSON credential', async function (assert) {
|
||||
assert.expect(4);
|
||||
await visit(urls.jsonCredential);
|
||||
await click('form [type="button"]', 'Activate edit mode');
|
||||
assert.strictEqual(currentURL(), urls.jsonCredential);
|
||||
assert.dom('.secret-editor-skeleton-message button').isVisible();
|
||||
await click(
|
||||
'.secret-editor-skeleton-message button',
|
||||
'Enter editing mode'
|
||||
);
|
||||
assert.dom('.secret-editor-skeleton-message button').doesNotExist();
|
||||
await waitUntil(() => assert.dom('.CodeMirror').isVisible());
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -32,7 +32,8 @@ module('Acceptance | host-catalogs | create', function (hooks) {
|
||||
hostCatalog: null,
|
||||
newHostCatalog: null,
|
||||
newStaticHostCatalog: null,
|
||||
newDynamicHostCatalog: null,
|
||||
newAWSDynamicHostCatalog: null,
|
||||
newAzureDynamicHostCatalog: null,
|
||||
};
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
@@ -67,7 +68,8 @@ module('Acceptance | host-catalogs | create', function (hooks) {
|
||||
urls.hostCatalog = `${urls.hostCatalogs}/${instances.hostCatalog.id}`;
|
||||
urls.newHostCatalog = `${urls.hostCatalogs}/new`;
|
||||
urls.newStaticHostCatalog = `${urls.newHostCatalog}?type=static`;
|
||||
urls.newDynamicHostCatalog = `${urls.newHostCatalog}?type=aws`;
|
||||
urls.newAWSDynamicHostCatalog = `${urls.newHostCatalog}?type=aws`;
|
||||
urls.newAzureDynamicHostCatalog = `${urls.newHostCatalog}?type=azure`;
|
||||
// Generate resource couner
|
||||
gethostCatalogCount = () =>
|
||||
this.server.schema.hostCatalogs.all().models.length;
|
||||
@@ -81,18 +83,29 @@ module('Acceptance | host-catalogs | create', function (hooks) {
|
||||
await visit(urls.newStaticHostCatalog);
|
||||
await fillIn('[name="name"]', 'random string');
|
||||
await fillIn('[name="description"]', 'random string');
|
||||
await fillIn('[name="types"]', 'static');
|
||||
await fillIn('[name="type"]', 'static');
|
||||
await click('[type="submit"]');
|
||||
assert.strictEqual(gethostCatalogCount(), count + 1);
|
||||
});
|
||||
|
||||
test('Users can create new dynamic host catalogs', async function (assert) {
|
||||
test('Users can create new dynamic aws host catalogs with aws provider', async function (assert) {
|
||||
assert.expect(1);
|
||||
const count = gethostCatalogCount();
|
||||
await visit(urls.newDynamicHostCatalog);
|
||||
await visit(urls.newAWSDynamicHostCatalog);
|
||||
await fillIn('[name="name"]', 'random string');
|
||||
await fillIn('[name="description"]', 'random string');
|
||||
await fillIn('[name="types"]', 'aws');
|
||||
await fillIn('[name="type"]', 'aws');
|
||||
await click('[type="submit"]');
|
||||
assert.strictEqual(gethostCatalogCount(), count + 1);
|
||||
});
|
||||
|
||||
test('Users can create new dynamic aws host catalogs with azure provider', async function (assert) {
|
||||
assert.expect(1);
|
||||
const count = gethostCatalogCount();
|
||||
await visit(urls.newAzureDynamicHostCatalog);
|
||||
await fillIn('[name="name"]', 'random string');
|
||||
await fillIn('[name="description"]', 'random string');
|
||||
await fillIn('[name="type"]', 'azure');
|
||||
await click('[type="submit"]');
|
||||
assert.strictEqual(gethostCatalogCount(), count + 1);
|
||||
});
|
||||
@@ -107,10 +120,20 @@ module('Acceptance | host-catalogs | create', function (hooks) {
|
||||
assert.strictEqual(gethostCatalogCount(), count);
|
||||
});
|
||||
|
||||
test('Users can cancel creation of new dynamic host catalogs', async function (assert) {
|
||||
test('Users can cancel creation of new dynamic host catalogs with AWS provider', async function (assert) {
|
||||
assert.expect(2);
|
||||
const count = gethostCatalogCount();
|
||||
await visit(urls.newDynamicHostCatalog);
|
||||
await visit(urls.newAWSDynamicHostCatalog);
|
||||
await fillIn('[name="name"]', 'random string');
|
||||
await click('.rose-form-actions [type="button"]');
|
||||
assert.strictEqual(currentURL(), urls.hostCatalogs);
|
||||
assert.strictEqual(gethostCatalogCount(), count);
|
||||
});
|
||||
|
||||
test('Users can cancel creation of new dynamic host catalogs with Azure provider', async function (assert) {
|
||||
assert.expect(2);
|
||||
const count = gethostCatalogCount();
|
||||
await visit(urls.newAzureDynamicHostCatalog);
|
||||
await fillIn('[name="name"]', 'random string');
|
||||
await click('.rose-form-actions [type="button"]');
|
||||
assert.strictEqual(currentURL(), urls.hostCatalogs);
|
||||
|
||||
@@ -97,9 +97,9 @@ module('Acceptance | host-catalogs | host sets | create', function (hooks) {
|
||||
const name = 'aws host set';
|
||||
await fillIn('[name="name"]', name);
|
||||
await fillIn('[name="preferred_endpoints"]', 'endpoint');
|
||||
await click('form fieldset:nth-of-type(2) [title="Add"]');
|
||||
await click('form fieldset:nth-of-type(1) [title="Add"]');
|
||||
await fillIn('[name="filters"]', 'filter');
|
||||
await click('form fieldset:nth-of-type(3) [title="Add"]');
|
||||
await click('form fieldset:nth-of-type(2) [title="Add"]');
|
||||
await fillIn('[name="sync_interval_seconds"]', 10);
|
||||
await click('[type="submit"]');
|
||||
|
||||
@@ -128,7 +128,7 @@ module('Acceptance | host-catalogs | host sets | create', function (hooks) {
|
||||
const name = 'azure host set';
|
||||
await fillIn('[name="name"]', name);
|
||||
await fillIn('[name="preferred_endpoints"]', 'endpoint');
|
||||
await click('form fieldset:nth-of-type(2) [title="Add"]');
|
||||
await click('form fieldset:nth-of-type(1) [title="Add"]');
|
||||
await fillIn('[name="filter"]', 'filter');
|
||||
await fillIn('[name="sync_interval_seconds"]', 10);
|
||||
await click('[type="submit"]');
|
||||
|
||||
@@ -147,20 +147,20 @@ module('Acceptance | host-catalogs | host sets | update', function (hooks) {
|
||||
await fillIn('[name="name"]', name);
|
||||
// Remove all the preferred endpoints
|
||||
await Promise.all(
|
||||
findAll('form fieldset:nth-of-type(2) [title="Remove"]').map((element) =>
|
||||
findAll('form fieldset:nth-of-type(1) [title="Remove"]').map((element) =>
|
||||
click(element)
|
||||
)
|
||||
);
|
||||
await fillIn('[name="preferred_endpoints"]', 'endpoint');
|
||||
await click('form fieldset:nth-of-type(2) [title="Add"]');
|
||||
await click('form fieldset:nth-of-type(1) [title="Add"]');
|
||||
// Remove all the filters
|
||||
await Promise.all(
|
||||
findAll('form fieldset:nth-of-type(3) [title="Remove"]').map((element) =>
|
||||
findAll('form fieldset:nth-of-type(2) [title="Remove"]').map((element) =>
|
||||
click(element)
|
||||
)
|
||||
);
|
||||
await fillIn('[name="filters"]', 'filter');
|
||||
await click('form fieldset:nth-of-type(3) [title="Add"]');
|
||||
await click('form fieldset:nth-of-type(2) [title="Add"]');
|
||||
await fillIn('[name="sync_interval_seconds"]', 10);
|
||||
await click('.rose-form-actions [type="submit"]');
|
||||
|
||||
@@ -198,12 +198,12 @@ module('Acceptance | host-catalogs | host sets | update', function (hooks) {
|
||||
await fillIn('[name="name"]', name);
|
||||
// Remove all the preferred endpoints
|
||||
await Promise.all(
|
||||
findAll('form fieldset:nth-of-type(2) [title="Remove"]').map((element) =>
|
||||
findAll('form fieldset:nth-of-type(1) [title="Remove"]').map((element) =>
|
||||
click(element)
|
||||
)
|
||||
);
|
||||
await fillIn('[name="preferred_endpoints"]', 'endpoint');
|
||||
await click('form fieldset:nth-of-type(2) [title="Add"]');
|
||||
await click('form fieldset:nth-of-type(1) [title="Add"]');
|
||||
await fillIn('[name="filter"]', 'filter');
|
||||
await fillIn('[name="sync_interval_seconds"]', 10);
|
||||
await click('.rose-form-actions [type="submit"]');
|
||||
|
||||
@@ -21,15 +21,15 @@ module('Acceptance | onboarding | choose-path', function (hooks) {
|
||||
test('defaults to the guided path', async function (assert) {
|
||||
assert.expect(2);
|
||||
await visit(urls.choosePath);
|
||||
assert.strictEqual(find('input[name=paths]:checked').value, 'guided');
|
||||
assert.strictEqual(find('input[name=path]:checked').value, 'guided');
|
||||
assert.false(find('input[value=manual]').checked);
|
||||
});
|
||||
|
||||
test('updates to the manual path when manual card clicked', async function (assert) {
|
||||
assert.expect(2);
|
||||
await visit(urls.choosePath);
|
||||
await click('[name="paths"][value="manual"]');
|
||||
assert.strictEqual(find('input[name=paths]:checked').value, 'manual');
|
||||
await click('[name="path"][value="manual"]');
|
||||
assert.strictEqual(find('input[name=path]:checked').value, 'manual');
|
||||
assert.false(find('input[value=guided]').checked);
|
||||
});
|
||||
|
||||
@@ -43,7 +43,7 @@ module('Acceptance | onboarding | choose-path', function (hooks) {
|
||||
test('guided path directs user to /global/new route', async function (assert) {
|
||||
assert.expect(1);
|
||||
await visit(urls.choosePath);
|
||||
await click('[name="paths"][value="manual"]');
|
||||
await click('[name="path"][value="manual"]');
|
||||
await click('[type="submit"]');
|
||||
assert.strictEqual(currentURL(), urls.newOrg);
|
||||
});
|
||||
|
||||
@@ -126,6 +126,6 @@ module('Acceptance | roles | create', function (hooks) {
|
||||
await click('form [type="submit"]');
|
||||
await a11yAudit();
|
||||
assert.ok(find('[role="alert"]'));
|
||||
assert.ok(find('.rose-form-error-message'));
|
||||
assert.ok(find('.hds-form-error__message'));
|
||||
});
|
||||
});
|
||||
@@ -46,7 +46,8 @@ module('Acceptance | roles | principals', function (hooks) {
|
||||
);
|
||||
principalsCount =
|
||||
this.server.db.roles[0].userIds.length +
|
||||
this.server.db.roles[0].groupIds.length;
|
||||
this.server.db.roles[0].groupIds.length +
|
||||
this.server.db.roles[0].managedGroupIds.length;
|
||||
urls.roles = `/scopes/${instances.scopes.org.id}/roles`;
|
||||
urls.role = `${urls.roles}/${instances.role.id}`;
|
||||
urls.rolePrincipals = `${urls.role}/principals`;
|
||||
@@ -111,7 +112,7 @@ module('Acceptance | roles | principals', function (hooks) {
|
||||
|
||||
test('select and save principals to add', async function (assert) {
|
||||
assert.expect(3);
|
||||
instances.role.update({ userIds: [], groupIds: [] });
|
||||
instances.role.update({ userIds: [], groupIds: [], managedGroupIds: [] });
|
||||
await visit(urls.rolePrincipals);
|
||||
assert.strictEqual(findAll('tbody tr').length, 0);
|
||||
await click('.rose-layout-page-actions a');
|
||||
@@ -154,7 +155,7 @@ module('Acceptance | roles | principals', function (hooks) {
|
||||
}
|
||||
);
|
||||
});
|
||||
instances.role.update({ userIds: [], groupIds: [] });
|
||||
instances.role.update({ userIds: [], groupIds: [], managedGroupIds: [] });
|
||||
await visit(urls.addPrincipals);
|
||||
await click('tbody label');
|
||||
await click('form [type="submit"]');
|
||||
|
||||
@@ -113,7 +113,7 @@ module('Acceptance | roles | update', function (hooks) {
|
||||
'Displays primary error message.'
|
||||
);
|
||||
assert.strictEqual(
|
||||
find('.rose-form-error-message').textContent.trim(),
|
||||
find('.hds-form-error__message').textContent.trim(),
|
||||
'Name is required.',
|
||||
'Displays field-level errors.'
|
||||
);
|
||||
|
||||
@@ -15,6 +15,7 @@ module('Acceptance | targets | brokered credential sources', function (hooks) {
|
||||
let credentialSourceCount;
|
||||
let randomlySelectedCredentialLibraries;
|
||||
let randomlySelectedCredentials;
|
||||
let featuresService;
|
||||
|
||||
const instances = {
|
||||
scopes: {
|
||||
@@ -37,11 +38,14 @@ module('Acceptance | targets | brokered credential sources', function (hooks) {
|
||||
target: null,
|
||||
credentialLibraries: null,
|
||||
credentialLibrary: null,
|
||||
credential: null,
|
||||
jsonCredential: null,
|
||||
addBrokeredCredentialSources: null,
|
||||
brokeredCredentialSources: null,
|
||||
};
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
featuresService = this.owner.lookup('service:features');
|
||||
// Generate resources
|
||||
instances.scopes.global = this.server.create('scope', { id: 'global' });
|
||||
instances.scopes.org = this.server.create('scope', {
|
||||
@@ -60,7 +64,7 @@ module('Acceptance | targets | brokered credential sources', function (hooks) {
|
||||
type: 'static',
|
||||
scope: instances.scopes.project,
|
||||
});
|
||||
instances.credentials = this.server.createList('credential', 2, {
|
||||
instances.credentials = this.server.createList('credential', 3, {
|
||||
scope: instances.scopes.project,
|
||||
credentialStore: instances.staticCredentialStore,
|
||||
});
|
||||
@@ -100,6 +104,7 @@ module('Acceptance | targets | brokered credential sources', function (hooks) {
|
||||
urls.brokeredCredentialSources = `${urls.target}/brokered-credential-sources`;
|
||||
urls.credentialLibrary = `${urls.projectScope}/credential-stores/${instances.credentialLibrary.credentialStoreId}/credential-libraries/${instances.credentialLibrary.id}`;
|
||||
urls.credential = `${urls.projectScope}/credential-stores/${instances.credential.credentialStoreId}/credentials/${instances.credential.id}`;
|
||||
urls.jsonCredential = `${urls.projectScope}/credential-stores/${instances.credentials[2].credentialStoreId}/credentials/${instances.credentials[2].id}`;
|
||||
urls.addBrokeredCredentialSources = `${urls.target}/add-brokered-credential-sources`;
|
||||
getCredentialLibraryCount = () =>
|
||||
this.server.schema.credentialLibraries.all().models.length;
|
||||
@@ -137,6 +142,21 @@ module('Acceptance | targets | brokered credential sources', function (hooks) {
|
||||
assert.strictEqual(currentURL(), urls.credential);
|
||||
});
|
||||
|
||||
test('cannot navigate to a json type credential when feature is disabled', async function (assert) {
|
||||
featuresService.disable('json-credentials');
|
||||
assert.expect(2);
|
||||
const jsonCredential = instances.credentials[2];
|
||||
instances.target.update({
|
||||
brokeredCredentialSourceIds: [...randomlySelectedCredentials],
|
||||
});
|
||||
await visit(urls.brokeredCredentialSources);
|
||||
|
||||
assert
|
||||
.dom('.rose-table-row:nth-child(3)')
|
||||
.includesText(jsonCredential.name);
|
||||
assert.dom(`[href="${urls.jsonCredential}"]`).doesNotExist();
|
||||
});
|
||||
|
||||
test('visiting add brokered credential sources', async function (assert) {
|
||||
assert.expect(1);
|
||||
await visit(urls.addBrokeredCredentialSources);
|
||||
@@ -157,7 +177,7 @@ module('Acceptance | targets | brokered credential sources', function (hooks) {
|
||||
test('displays list of brokered credential sources with only credential libraries available', async function (assert) {
|
||||
assert.expect(2);
|
||||
instances.target.update({
|
||||
brokeredCredentialSourceIds: [...randomlySelectedCredentialLibraries],
|
||||
brokeredCredentialSourceIds: [...randomlySelectedCredentials],
|
||||
});
|
||||
await visit(urls.addBrokeredCredentialSources);
|
||||
assert.strictEqual(findAll('tbody tr').length, getCredentialLibraryCount());
|
||||
@@ -300,12 +320,16 @@ module('Acceptance | targets | brokered credential sources', function (hooks) {
|
||||
brokeredCredentialSourceIds: [...randomlySelectedCredentialLibraries],
|
||||
});
|
||||
const credentialLibraryCount = getCredentialLibraryCount();
|
||||
const availableCredentialsCount = getCredentialCount();
|
||||
await visit(urls.brokeredCredentialSources);
|
||||
assert.strictEqual(findAll('tbody tr').length, credentialLibraryCount);
|
||||
await click('tbody tr .rose-dropdown-button-danger');
|
||||
assert.strictEqual(findAll('tbody tr').length, credentialLibraryCount - 1);
|
||||
await visit(urls.addBrokeredCredentialSources);
|
||||
assert.strictEqual(findAll('tbody tr').length, credentialLibraryCount + 1);
|
||||
assert.strictEqual(
|
||||
findAll('tbody tr').length,
|
||||
availableCredentialsCount + 1
|
||||
);
|
||||
});
|
||||
|
||||
test('can remove a username & password type credential', async function (assert) {
|
||||
@@ -314,12 +338,16 @@ module('Acceptance | targets | brokered credential sources', function (hooks) {
|
||||
brokeredCredentialSourceIds: [...randomlySelectedCredentials],
|
||||
});
|
||||
const credentialCount = getCredentialCount();
|
||||
const availableCredentialsCount = getCredentialLibraryCount();
|
||||
await visit(urls.brokeredCredentialSources);
|
||||
assert.strictEqual(findAll('tbody tr').length, credentialCount);
|
||||
await click('tbody tr .rose-dropdown-button-danger');
|
||||
assert.strictEqual(findAll('tbody tr').length, credentialCount - 1);
|
||||
await visit(urls.addBrokeredCredentialSources);
|
||||
assert.strictEqual(findAll('tbody tr').length, credentialCount + 1);
|
||||
assert.strictEqual(
|
||||
findAll('tbody tr').length,
|
||||
availableCredentialsCount + 1
|
||||
);
|
||||
});
|
||||
|
||||
test('cannot remove credential libraries without proper authorization', async function (assert) {
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
//currentSession,
|
||||
//invalidateSession,
|
||||
} from 'ember-simple-auth/test-support';
|
||||
import { TYPE_TARGET_TCP, TYPE_TARGET_SSH } from 'api/models/target';
|
||||
|
||||
module('Acceptance | targets | create', function (hooks) {
|
||||
setupApplicationTest(hooks);
|
||||
@@ -71,31 +72,32 @@ module('Acceptance | targets | create', function (hooks) {
|
||||
// Generate resource counter
|
||||
getTargetCount = () => this.server.schema.targets.all().models.length;
|
||||
getSSHTargetCount = () =>
|
||||
this.server.schema.targets.where({ type: 'ssh' }).models.length;
|
||||
this.server.schema.targets.where({ type: TYPE_TARGET_SSH }).models.length;
|
||||
getTCPTargetCount = () =>
|
||||
this.server.schema.targets.where({ type: 'tcp' }).models.length;
|
||||
this.server.schema.targets.where({ type: TYPE_TARGET_TCP }).models.length;
|
||||
authenticateSession({});
|
||||
});
|
||||
|
||||
test('defaults to a new TCP target when no query param provided', async function (assert) {
|
||||
test('defaults to type `ssh` when no query param provided', async function (assert) {
|
||||
assert.expect(1);
|
||||
await visit(urls.newTarget);
|
||||
assert.strictEqual(find('input[name=tcp]:checked').value, 'tcp');
|
||||
assert.strictEqual(find('[name="type"]:checked').value, TYPE_TARGET_SSH);
|
||||
});
|
||||
|
||||
test('can create new targets of type TCP', async function (assert) {
|
||||
test('can create type `ssh`', async function (assert) {
|
||||
assert.expect(4);
|
||||
const targetCount = getTargetCount();
|
||||
const tcpTargetCount = getTCPTargetCount();
|
||||
const sshTargetCount = getSSHTargetCount();
|
||||
await visit(urls.targets);
|
||||
|
||||
await click(`[href="${urls.newTarget}"]`);
|
||||
await click('[value="ssh"]');
|
||||
await fillIn('[name="name"]', 'random string');
|
||||
await fillIn('[name="worker_filter"]', 'random filter');
|
||||
await click('[type="submit"]');
|
||||
|
||||
assert.strictEqual(getSSHTargetCount(), sshTargetCount + 1);
|
||||
assert.strictEqual(getTargetCount(), targetCount + 1);
|
||||
assert.strictEqual(getTCPTargetCount(), tcpTargetCount + 1);
|
||||
assert.strictEqual(
|
||||
this.server.schema.targets.all().models[getTargetCount() - 1].name,
|
||||
'random string'
|
||||
@@ -107,20 +109,20 @@ module('Acceptance | targets | create', function (hooks) {
|
||||
);
|
||||
});
|
||||
|
||||
test('can create new targets of type SSH', async function (assert) {
|
||||
test('can create type `tcp`', async function (assert) {
|
||||
assert.expect(4);
|
||||
const targetCount = getTargetCount();
|
||||
const sshTargetCount = getSSHTargetCount();
|
||||
const tcpTargetCount = getTCPTargetCount();
|
||||
await visit(urls.targets);
|
||||
|
||||
await click(`[href="${urls.newTarget}"]`);
|
||||
await click('[value="tcp"]');
|
||||
await fillIn('[name="name"]', 'random string');
|
||||
await click('[value="ssh"]');
|
||||
await fillIn('[name="worker_filter"]', 'random filter');
|
||||
await click('[type="submit"]');
|
||||
|
||||
assert.strictEqual(getSSHTargetCount(), sshTargetCount + 1);
|
||||
assert.strictEqual(getTargetCount(), targetCount + 1);
|
||||
assert.strictEqual(getTCPTargetCount(), tcpTargetCount + 1);
|
||||
assert.strictEqual(
|
||||
this.server.schema.targets.all().models[getTargetCount() - 1].name,
|
||||
'random string'
|
||||
|
||||
@@ -5,6 +5,7 @@ import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
|
||||
import { Response } from 'miragejs';
|
||||
import a11yAudit from 'ember-a11y-testing/test-support/audit';
|
||||
import { authenticateSession } from 'ember-simple-auth/test-support';
|
||||
import { TYPE_TARGET_SSH } from 'api/models/target';
|
||||
|
||||
module(
|
||||
'Acceptance | targets | injected application credential sources',
|
||||
@@ -78,7 +79,7 @@ module(
|
||||
instances.credential = instances.credentials[0];
|
||||
instances.target = this.server.create('target', {
|
||||
scope: instances.scopes.project,
|
||||
type: 'ssh',
|
||||
type: TYPE_TARGET_SSH,
|
||||
});
|
||||
randomlySelectedCredentials = this.server.schema.credentials
|
||||
.all()
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
//currentSession,
|
||||
//invalidateSession,
|
||||
} from 'ember-simple-auth/test-support';
|
||||
import { TYPE_TARGET_TCP, TYPE_TARGET_SSH } from 'api/models/target';
|
||||
|
||||
module('Acceptance | targets | read', function (hooks) {
|
||||
setupApplicationTest(hooks);
|
||||
@@ -44,11 +45,11 @@ module('Acceptance | targets | read', function (hooks) {
|
||||
scope: { id: instances.scopes.org.id, type: 'org' },
|
||||
});
|
||||
instances.sshTarget = this.server.create('target', {
|
||||
type: 'ssh',
|
||||
type: TYPE_TARGET_SSH,
|
||||
scope: instances.scopes.project,
|
||||
});
|
||||
instances.tcpTarget = this.server.create('target', {
|
||||
type: 'tcp',
|
||||
type: TYPE_TARGET_TCP,
|
||||
scope: instances.scopes.project,
|
||||
});
|
||||
// Generate route URLs for resources
|
||||
|
||||
@@ -60,6 +60,26 @@ module('Acceptance | workers | create', function (hooks) {
|
||||
assert.dom(labels[2]).hasText('Initial Upstreams');
|
||||
});
|
||||
|
||||
test('download and install step shows correct oss instructions', async function (assert) {
|
||||
assert.expect(2);
|
||||
const featuresService = this.owner.lookup('service:features');
|
||||
featuresService.disable('byow-pki-hcp-cluster-id');
|
||||
await visit(newWorkerURL);
|
||||
const createSection = findAll('.worker-create-section');
|
||||
assert.dom(createSection[1]).includesText('curl -fsSL');
|
||||
assert.dom(createSection[1]).doesNotIncludeText('wget -q');
|
||||
});
|
||||
|
||||
test('download and install step shows correct hcp instructions', async function (assert) {
|
||||
assert.expect(2);
|
||||
const featuresService = this.owner.lookup('service:features');
|
||||
featuresService.enable('byow-pki-hcp-cluster-id');
|
||||
await visit(newWorkerURL);
|
||||
const createSection = findAll('.worker-create-section');
|
||||
assert.dom(createSection[1]).includesText('wget -q');
|
||||
assert.dom(createSection[1]).doesNotIncludeText('curl -fsSL');
|
||||
});
|
||||
|
||||
test('Users can navigate to new workers route with proper authorization', async function (assert) {
|
||||
assert.expect(2);
|
||||
await visit(newWorkerURL);
|
||||
|
||||
@@ -3,18 +3,21 @@ import { setupRenderingTest } from 'ember-qunit';
|
||||
import { render } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
|
||||
module('Integration | Component | form/field/json-secret/skeleton', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
module(
|
||||
'Integration | Component | form/field/json-secret/skeleton',
|
||||
function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
test('it renders', async function (assert) {
|
||||
assert.expect(2);
|
||||
await render(hbs`
|
||||
test('it renders', async function (assert) {
|
||||
assert.expect(2);
|
||||
await render(hbs`
|
||||
<Form::Field::JsonSecret::Skeleton>
|
||||
Skeleton Message
|
||||
</Form::Field::JsonSecret::Skeleton>
|
||||
`);
|
||||
|
||||
assert.dom('.secret-editor-skeleton-wrapper').isVisible();
|
||||
assert.dom('.secret-editor-skeleton-message').hasText('Skeleton Message');
|
||||
});
|
||||
});
|
||||
assert.dom('.secret-editor-skeleton-wrapper').isVisible();
|
||||
assert.dom('.secret-editor-skeleton-message').hasText('Skeleton Message');
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -26,7 +26,9 @@ module('Integration | Component | form/field/json-secret', function (hooks) {
|
||||
|
||||
assert.dom('.secret-editor').isVisible();
|
||||
assert.dom('.secret-editor-json').isVisible();
|
||||
assert.dom('.secret-editor-json .CodeMirror-line').hasText(this.model.json_object);
|
||||
assert
|
||||
.dom('.secret-editor-json .CodeMirror-line')
|
||||
.hasText(this.model.json_object);
|
||||
});
|
||||
|
||||
test('it renders the disabled editor', async function (assert) {
|
||||
@@ -42,7 +44,9 @@ module('Integration | Component | form/field/json-secret', function (hooks) {
|
||||
|
||||
assert.dom('.secret-editor').isVisible();
|
||||
assert.dom('.secret-editor-json').doesNotExist();
|
||||
assert.dom('.secret-editor-skeleton-wrapper').hasText('This secret is saved but won\'t be displayed');
|
||||
assert
|
||||
.dom('.secret-editor-skeleton-wrapper')
|
||||
.hasText("This secret is saved but won't be displayed");
|
||||
});
|
||||
|
||||
test('it renders the actionable editor', async function (assert) {
|
||||
@@ -58,7 +62,9 @@ module('Integration | Component | form/field/json-secret', function (hooks) {
|
||||
|
||||
assert.dom('.secret-editor').isVisible();
|
||||
assert.dom('.secret-editor-json').doesNotExist();
|
||||
assert.dom('.secret-editor-skeleton-wrapper').hasText('Click to replace this secret');
|
||||
assert
|
||||
.dom('.secret-editor-skeleton-wrapper')
|
||||
.hasText('Click to replace this secret');
|
||||
});
|
||||
|
||||
test('it renders the active editor from the actionable view', async function (assert) {
|
||||
@@ -74,9 +80,11 @@ module('Integration | Component | form/field/json-secret', function (hooks) {
|
||||
|
||||
assert.dom('.secret-editor').isVisible();
|
||||
assert.dom('.secret-editor-json').doesNotExist();
|
||||
assert.dom('.secret-editor-skeleton-wrapper').hasText('Click to replace this secret');
|
||||
assert
|
||||
.dom('.secret-editor-skeleton-wrapper')
|
||||
.hasText('Click to replace this secret');
|
||||
|
||||
await click('button')
|
||||
await click('button');
|
||||
assert.dom('.secret-editor-json').isVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -11,15 +11,15 @@ module('Integration | Component | info-field', function (hooks) {
|
||||
|
||||
await render(
|
||||
hbs`
|
||||
<InfoField @value="Description" as |F|>
|
||||
<InfoField @value="JSON" as |F|>
|
||||
<F.Label>Type</F.Label>
|
||||
<F.HelperText>JSON</F.HelperText>
|
||||
<F.HelperText>Description</F.HelperText>
|
||||
</InfoField>
|
||||
`
|
||||
);
|
||||
assert.dom('.info-field').isVisible();
|
||||
assert.dom('.info-field .hds-form-label').hasText('Type');
|
||||
assert.dom('.info-field .hds-form-helper-text').hasText('JSON');
|
||||
assert.dom('.info-field .hds-form-helper-text').hasText('Description');
|
||||
assert.dom('.info-field .hds-form-text-input').isVisible();
|
||||
});
|
||||
|
||||
@@ -28,9 +28,9 @@ module('Integration | Component | info-field', function (hooks) {
|
||||
|
||||
await render(
|
||||
hbs`
|
||||
<InfoField @value="Description" @icon='apple' as |F|>
|
||||
<InfoField @value="JSON" @icon='apple' as |F|>
|
||||
<F.Label>Type</F.Label>
|
||||
<F.HelperText>JSON</F.HelperText>
|
||||
<F.HelperText>Description</F.HelperText>
|
||||
</InfoField>
|
||||
`
|
||||
);
|
||||
@@ -38,7 +38,7 @@ module('Integration | Component | info-field', function (hooks) {
|
||||
assert.dom('.info-field .hds-form-label').hasText('Type');
|
||||
assert
|
||||
.dom('.info-field .hds-form-helper-text:last-of-type')
|
||||
.hasText('JSON');
|
||||
.hasText('Description');
|
||||
assert.dom('[data-test-icon="apple"]').isVisible();
|
||||
assert.dom('.info-field .hds-form-text-input').isVisible();
|
||||
});
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
|
||||
module('Unit | Abilities | credential', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
let features;
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
// Enable json-credentials feature by default
|
||||
features = this.owner.lookup('service:features');
|
||||
features.enable('json-credentials');
|
||||
});
|
||||
|
||||
test('can read credentials, including JSON credentials, when authorized and json-credentials feature is enabled', function (assert) {
|
||||
assert.expect(3);
|
||||
const service = this.owner.lookup('service:can');
|
||||
const store = this.owner.lookup('service:store');
|
||||
const credential = store.createRecord('credential', {
|
||||
authorized_actions: ['read'],
|
||||
type: 'json',
|
||||
});
|
||||
assert.true(service.can('read credential', credential));
|
||||
credential.type = 'username_password';
|
||||
assert.true(service.can('read credential', credential));
|
||||
credential.type = 'ssh_private_key';
|
||||
assert.true(service.can('read credential', credential));
|
||||
});
|
||||
|
||||
test('cannot read credentials, including JSON credentials, when unauthorized and json-credentials feature is enabled', function (assert) {
|
||||
assert.expect(3);
|
||||
const service = this.owner.lookup('service:can');
|
||||
const store = this.owner.lookup('service:store');
|
||||
const credential = store.createRecord('credential', {
|
||||
authorized_actions: [],
|
||||
type: 'json',
|
||||
});
|
||||
assert.false(service.can('read credential', credential));
|
||||
credential.type = 'username_password';
|
||||
assert.false(service.can('read credential', credential));
|
||||
credential.type = 'ssh_private_key';
|
||||
assert.false(service.can('read credential', credential));
|
||||
});
|
||||
|
||||
test('cannot read credentials, including JSON credentials, when unauthorized and json-credentials feature is disabled', function (assert) {
|
||||
assert.expect(3);
|
||||
const service = this.owner.lookup('service:can');
|
||||
const featuresService = this.owner.lookup('service:features');
|
||||
featuresService.disable('json-credentials');
|
||||
const store = this.owner.lookup('service:store');
|
||||
const credential = store.createRecord('credential', {
|
||||
authorized_actions: [],
|
||||
type: 'json',
|
||||
});
|
||||
assert.false(service.can('read credential', credential));
|
||||
credential.type = 'username_password';
|
||||
assert.false(service.can('read credential', credential));
|
||||
credential.type = 'ssh_private_key';
|
||||
assert.false(service.can('read credential', credential));
|
||||
});
|
||||
|
||||
test('can read credentials, excepting JSON credentials, when authorized and json-credentials feature is disabled', function (assert) {
|
||||
assert.expect(3);
|
||||
features.disable('json-credentials');
|
||||
const service = this.owner.lookup('service:can');
|
||||
const store = this.owner.lookup('service:store');
|
||||
const credential = store.createRecord('credential', {
|
||||
authorized_actions: ['read'],
|
||||
type: 'json',
|
||||
});
|
||||
assert.false(service.can('read credential', credential));
|
||||
credential.type = 'username_password';
|
||||
assert.true(service.can('read credential', credential));
|
||||
credential.type = 'ssh_private_key';
|
||||
assert.true(service.can('read credential', credential));
|
||||
});
|
||||
});
|
||||
@@ -30,7 +30,7 @@
|
||||
{{#if host.isStatic}}
|
||||
{{host.address}}
|
||||
{{else}}
|
||||
<Hds::Badge @text={{t 'resources.host-catalog.types.dynamic'}} />
|
||||
<Hds::Badge @text={{t 'resources.host-catalog.types.plugin'}} />
|
||||
{{/if}}
|
||||
</row.cell>
|
||||
<row.cell>
|
||||
|
||||
@@ -3262,9 +3262,9 @@ punycode@^2.1.0, punycode@^2.1.1:
|
||||
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
|
||||
|
||||
qs@~6.5.2:
|
||||
version "6.5.2"
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
|
||||
integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==
|
||||
version "6.5.3"
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.3.tgz#3aeeffc91967ef6e35c0e488ef46fb296ab76aad"
|
||||
integrity sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==
|
||||
|
||||
queue-microtask@^1.2.2:
|
||||
version "1.2.3"
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
authenticateSession,
|
||||
invalidateSession,
|
||||
} from 'ember-simple-auth/test-support';
|
||||
import { TYPE_TARGET_SSH } from 'api/models/target';
|
||||
|
||||
module('Acceptance | projects | targets', function (hooks) {
|
||||
setupApplicationTest(hooks);
|
||||
@@ -247,7 +248,7 @@ module('Acceptance | projects | targets', function (hooks) {
|
||||
test('displays the correct target type (SSH) in the success dialog body', async function (assert) {
|
||||
assert.expect(1);
|
||||
instances.target.update({
|
||||
type: 'ssh',
|
||||
type: TYPE_TARGET_SSH,
|
||||
});
|
||||
stubs.ipcService.withArgs('cliExists').returns(true);
|
||||
stubs.ipcService.withArgs('connect').returns({
|
||||
|
||||
Reference in new issue
Block a user