Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d1e15a7bb | ||
|
|
dd92321b81 | ||
|
|
dce45031cb | ||
|
|
2666ec8244 | ||
|
|
3674d07762 | ||
|
|
dccf06cb0c | ||
|
|
c64e8a2993 | ||
|
|
5ae490c28c | ||
|
|
bedcdee048 | ||
|
|
bd394d9517 | ||
|
|
a6bdc4fa24 | ||
|
|
3e9a06a5b5 | ||
|
|
19908768a4 | ||
|
|
4274a045f7 | ||
|
|
1270458430 | ||
|
|
4c342061d3 | ||
|
|
5ce83d9239 | ||
|
|
148f2db941 | ||
|
|
f65f483611 | ||
|
|
cc9003a118 | ||
|
|
20f8c99013 | ||
|
|
536ebc1c0f | ||
|
|
79a932bebb | ||
|
|
f246c24a08 | ||
|
|
d31cfca138 | ||
|
|
298e804500 | ||
|
|
f9cd1c331f | ||
|
|
a0fa66a14d | ||
|
|
086d9da028 | ||
|
|
9b20b150eb | ||
|
|
6bbddc56b1 | ||
|
|
0870a8f623 | ||
|
|
98c16ad422 | ||
|
|
9156c8612e | ||
|
|
a43756895d | ||
|
|
adc50a939f | ||
|
|
37cbb74c76 | ||
|
|
80b633a063 | ||
|
|
bd65c91ff9 | ||
|
|
c550faddb4 | ||
|
|
5e7c25a1fa | ||
|
|
ded8bdadd1 | ||
|
|
13da35c06b | ||
|
|
d52957d87b | ||
|
|
33d359e692 | ||
|
|
626bdc2c92 | ||
|
|
edba33bbda | ||
|
|
5d32d987bc | ||
|
|
6c59baa413 | ||
|
|
c2c0b38187 | ||
|
|
1d7892230c | ||
|
|
ece9c9b0c8 | ||
|
|
9a43c74e60 | ||
|
|
eb4ec5f6de | ||
|
|
a39b208b48 | ||
|
|
22353e7df6 | ||
|
|
713ffd8d1b | ||
|
|
6e75439652 | ||
|
|
be96daa499 | ||
|
|
9f11e8a25d | ||
|
|
8be4e93a3c | ||
|
|
a78522e09a | ||
|
|
a26662eca5 | ||
|
|
d72fa0d6e3 | ||
|
|
bd5e3da1f4 | ||
|
|
f74f4d4a20 | ||
|
|
9bd45164bf | ||
|
|
363879c3c3 | ||
|
|
0b4fd6765e | ||
|
|
c8839d6db1 | ||
|
|
ac5110d94a | ||
|
|
78bcc103c2 | ||
|
|
fe0cffdf0b | ||
|
|
79f4c2bc5d | ||
|
|
9843310dfb | ||
|
|
db75b2e292 | ||
|
|
ac1d98bbec | ||
|
|
0c6744581d | ||
|
|
fb345f045e | ||
|
|
b1085def48 | ||
|
|
499b0fde31 | ||
|
|
bbf9c95267 | ||
|
|
2dbcc1c0af | ||
|
|
6d88206491 | ||
|
|
de20df445c | ||
|
|
45ad7a8f3b |
No files matched your search
@@ -12,7 +12,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 # v4.0.0
|
||||
- uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
||||
id: node-modules-cache
|
||||
with:
|
||||
path: "**/node_modules"
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 # v4.0.0
|
||||
- uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
||||
id: node-modules-cache
|
||||
with:
|
||||
path: "**/node_modules"
|
||||
@@ -57,7 +57,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 # v4.0.0
|
||||
- uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
||||
id: node-modules-cache
|
||||
with:
|
||||
path: "**/node_modules"
|
||||
@@ -80,7 +80,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 # v4.0.0
|
||||
- uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
||||
id: node-modules-cache
|
||||
with:
|
||||
path: "**/node_modules"
|
||||
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@592977e6ae857384aa79bb31e7a1d62d63449ec5 # codeql-bundle-v2.16.3
|
||||
uses: github/codeql-action/init@423a04bb2cb7cd2643007122588f1387778f14d0 # codeql-bundle-v2.16.5
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -54,4 +54,4 @@ jobs:
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@592977e6ae857384aa79bb31e7a1d62d63449ec5 # codeql-bundle-v2.16.3
|
||||
uses: github/codeql-action/analyze@423a04bb2cb7cd2643007122588f1387778f14d0 # codeql-bundle-v2.16.5
|
||||
@@ -7,7 +7,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 # v4.0.0
|
||||
- uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
||||
id: node-modules-cache
|
||||
with:
|
||||
path: '**/node_modules'
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 # v4.0.0
|
||||
- uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
||||
id: node-modules-cache
|
||||
with:
|
||||
path: '**/node_modules'
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 # v4.0.0
|
||||
- uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
||||
id: node-modules-cache
|
||||
with:
|
||||
path: '**/node_modules'
|
||||
@@ -63,7 +63,7 @@ jobs:
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 # v4.0.0
|
||||
- uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
||||
id: node-modules-cache
|
||||
with:
|
||||
path: '**/node_modules'
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
|
||||
import sinon from 'sinon';
|
||||
import { typeOf } from '@ember/utils';
|
||||
import { resourceNames } from 'api/handlers/client-daemon-handler';
|
||||
import { singularize } from 'ember-inflector';
|
||||
|
||||
/**
|
||||
* This test helper can be used to help setup your sinon stubs in your tests.
|
||||
@@ -99,7 +101,8 @@ export default function setupStubs(hooks) {
|
||||
resource = type.resource;
|
||||
}
|
||||
|
||||
sinon.assert.match(ipcCall.args[1], { resource });
|
||||
const mappedResourceName = resourceNames[singularize(resource)];
|
||||
sinon.assert.match(ipcCall.args[1], { resource: mappedResourceName });
|
||||
});
|
||||
|
||||
sinon.restore();
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* This test helper can help set up and cleanup the database in your tests.
|
||||
*
|
||||
* ```js
|
||||
* import { setupIndexedDb } from 'api/test-support/helpers/indexed-db';
|
||||
*
|
||||
* module('Acceptance | my test', function(hooks) {
|
||||
* setupApplicationTest(hooks);
|
||||
* setupIndexedDb(hooks);
|
||||
*
|
||||
* // add your actual tests here
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @param hooks
|
||||
*/
|
||||
export function setupIndexedDb(hooks) {
|
||||
hooks.beforeEach(function () {
|
||||
const indexedDb = this.owner.lookup('service:indexed-db');
|
||||
indexedDb.setup(`test-indexed-db-${Date.now()}`);
|
||||
});
|
||||
|
||||
hooks.afterEach(async function () {
|
||||
const indexedDb = this.owner.lookup('service:indexed-db');
|
||||
|
||||
await indexedDb.db.delete();
|
||||
});
|
||||
}
|
||||
|
||||
export default setupIndexedDb;
|
||||
@@ -115,6 +115,7 @@ export default class ApplicationAdapter extends RESTAdapter.extend(
|
||||
* @return {Promise} promise
|
||||
*/
|
||||
async query(store, schema, query) {
|
||||
const queryObject = structuredClone(query);
|
||||
let result;
|
||||
let data = [];
|
||||
|
||||
@@ -128,12 +129,12 @@ export default class ApplicationAdapter extends RESTAdapter.extend(
|
||||
// Run this loop as long as the response_type is delta,
|
||||
// which indicates that there are more items in the list
|
||||
do {
|
||||
result = await super.query(store, schema, query);
|
||||
result = await super.query(store, schema, queryObject);
|
||||
//add the result items to a data array
|
||||
if (result && result.items) {
|
||||
data.push(...result.items);
|
||||
//pass in the list token for subsequent calls to fetch the remaining list items
|
||||
query.list_token = result.list_token;
|
||||
queryObject.list_token = result.list_token;
|
||||
}
|
||||
} while (result && result.response_type === 'delta');
|
||||
|
||||
@@ -211,7 +212,10 @@ export default class ApplicationAdapter extends RESTAdapter.extend(
|
||||
handleResponse(status, headers, payload, requestData) {
|
||||
if (this.isInvalid(status, headers, payload)) {
|
||||
const detailedMessage = this.generatedDetailedMessage(...arguments);
|
||||
const transformedPayload = this.transformValidationErrors(payload);
|
||||
const transformedPayload = this.transformValidationErrors(
|
||||
payload,
|
||||
status,
|
||||
);
|
||||
return new InvalidError(transformedPayload.errors, detailedMessage);
|
||||
}
|
||||
return super.handleResponse(status, headers, payload, requestData);
|
||||
@@ -269,17 +273,18 @@ export default class ApplicationAdapter extends RESTAdapter.extend(
|
||||
* error format.
|
||||
*
|
||||
* @param {object} payload
|
||||
* @param status
|
||||
* @return {object}
|
||||
*/
|
||||
transformValidationErrors(payload) {
|
||||
transformValidationErrors(payload, status) {
|
||||
const fieldErrors = get(payload, 'details.request_fields') || [];
|
||||
// Normalize the primary error message into "base", which is an error
|
||||
// that applies to the whole model instance
|
||||
const baseError = {
|
||||
isInvalid: true,
|
||||
status: payload.source,
|
||||
code: payload.code,
|
||||
detail: payload.message,
|
||||
status,
|
||||
code: payload?.kind,
|
||||
detail: payload?.message,
|
||||
source: { pointer: '/data' },
|
||||
};
|
||||
// Normalize field-specific errors, if any.
|
||||
@@ -314,6 +319,6 @@ export default class ApplicationAdapter extends RESTAdapter.extend(
|
||||
* @return {String} detailed error message
|
||||
*/
|
||||
generatedDetailedMessage(status, headers, payload /*, requestData*/) {
|
||||
return payload.message;
|
||||
return payload?.message;
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,12 @@ export default class GeneratedAliasModel extends BaseModel {
|
||||
})
|
||||
version;
|
||||
|
||||
@attr('string', {
|
||||
description:
|
||||
'This is the value referenced by the user that is resolved to the destination id.',
|
||||
})
|
||||
value;
|
||||
|
||||
@attr('string', {
|
||||
description: 'This is the id of the resource that this Alias points to.',
|
||||
})
|
||||
|
||||
@@ -73,6 +73,12 @@ export default class GeneratedStorageBucketModel extends BaseModel {
|
||||
})
|
||||
region;
|
||||
|
||||
@attr('string', {
|
||||
isNestedAttribute: true,
|
||||
description: 'The MinIO location server.',
|
||||
})
|
||||
endpoint_url;
|
||||
|
||||
//this field is not used in the UI, but this is kept here to replicate the API
|
||||
@attr('string', {
|
||||
readOnly: true,
|
||||
|
||||
@@ -108,4 +108,17 @@ export default class GeneratedTargetModel extends BaseModel {
|
||||
isNestedAttribute: true,
|
||||
})
|
||||
enable_session_recording;
|
||||
|
||||
@attr('array', {
|
||||
description:
|
||||
'with_aliases specify the aliases that should be created when the target is created. ',
|
||||
})
|
||||
with_aliases;
|
||||
|
||||
@attr('array', {
|
||||
readOnly: true,
|
||||
description:
|
||||
'with_aliases specify the aliases that should be created when the target is created. ',
|
||||
})
|
||||
aliases;
|
||||
}
|
||||
@@ -5,43 +5,29 @@
|
||||
|
||||
import { inject as service } from '@ember/service';
|
||||
import { getOwner, setOwner } from '@ember/application';
|
||||
import { pluralize } from 'ember-inflector';
|
||||
import { generateMQLExpression } from '../utils/mql-query';
|
||||
import { paginateResults } from '../utils/paginate-results';
|
||||
|
||||
/**
|
||||
* Not all types are yet supported by the client daemon so we'll
|
||||
* just whitelist the ones we need to start with. Keys are the resource
|
||||
* types and the values are the fields that are searchable by the client.
|
||||
* @type {{session: string[], target: string[]}}
|
||||
* @type {{session: string[], alias: string[], target: string[]}}
|
||||
*/
|
||||
const supportedTypes = {
|
||||
target: ['id', 'name', 'description', 'address', 'scope_id'],
|
||||
session: ['id', 'type', 'status', 'endpoint', 'scope_id', 'target_id'],
|
||||
alias: ['id', 'type', 'value', 'destination_id'],
|
||||
};
|
||||
|
||||
/**
|
||||
* Takes an array and the current page and pagesize to calculate the correct
|
||||
* number of results to return to the caller.
|
||||
*
|
||||
* @param array
|
||||
* @param page
|
||||
* @param pageSize
|
||||
* @returns {[*]}
|
||||
* Map the model resource name to the client daemon resource name
|
||||
* @type {{session: string, alias: string, target: string}}
|
||||
*/
|
||||
const paginateResults = (array, page, pageSize) => {
|
||||
const length = array?.length;
|
||||
if (!array || length === 0) {
|
||||
return [];
|
||||
}
|
||||
if (!page || !pageSize) {
|
||||
return array;
|
||||
}
|
||||
|
||||
const offset = (page - 1) * pageSize;
|
||||
const start = Math.min(length - 1, offset);
|
||||
const end = Math.min(length, offset + pageSize);
|
||||
|
||||
return array.slice(start, end);
|
||||
export const resourceNames = {
|
||||
target: 'targets',
|
||||
session: 'sessions',
|
||||
alias: 'resolvable-aliases',
|
||||
};
|
||||
|
||||
const fetchControllerData = async (context, next) => {
|
||||
@@ -112,7 +98,7 @@ export default class ClientDaemonHandler {
|
||||
query: searchQuery,
|
||||
auth_token_id,
|
||||
token,
|
||||
resource: pluralize(type),
|
||||
resource: resourceNames[type],
|
||||
};
|
||||
|
||||
let clientDaemonResults = {};
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
import { modelIndexes } from 'api/services/indexed-db';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { getOwner, setOwner } from '@ember/application';
|
||||
import { queryIndexedDb } from '../utils/indexed-db-query';
|
||||
import { paginateResults } from '../utils/paginate-results';
|
||||
import { hashCode } from '../utils/hash-code';
|
||||
|
||||
/**
|
||||
* Handler to sit in front of the API request layer
|
||||
* so we can handle any caching with indexedDb first
|
||||
*/
|
||||
export default class IndexedDbHandler {
|
||||
@service indexedDb;
|
||||
|
||||
constructor(context) {
|
||||
setOwner(this, getOwner(context));
|
||||
}
|
||||
|
||||
async request(context, next) {
|
||||
switch (context.request.op) {
|
||||
case 'query': {
|
||||
const { store, data } = context.request;
|
||||
// TODO: Remove storeToken option as this is a temporary fix for auth-methods.
|
||||
const {
|
||||
type,
|
||||
query,
|
||||
options: {
|
||||
pushToStore = true,
|
||||
peekIndexedDB = false,
|
||||
storeToken = true,
|
||||
} = {},
|
||||
} = data;
|
||||
const supportedModels = Object.keys(modelIndexes);
|
||||
const { db: indexedDb } = this.indexedDb ?? {};
|
||||
|
||||
// Go through normal flow if we don't yet support the model
|
||||
// or if we don't have an indexedDb instance
|
||||
if (!supportedModels.includes(type) || !indexedDb) {
|
||||
return next(context.request);
|
||||
}
|
||||
|
||||
let { page, pageSize, query: queryObj, ...remainingQuery } = query;
|
||||
|
||||
const adapter = store.adapterFor(type);
|
||||
const schema = store.modelFor(type);
|
||||
const serializer = store.serializerFor(type);
|
||||
|
||||
if (!peekIndexedDB) {
|
||||
const tokenKey = `${type}-${hashCode(remainingQuery)}`;
|
||||
|
||||
let listToken;
|
||||
if (storeToken) {
|
||||
listToken = await indexedDb.token.get(tokenKey);
|
||||
}
|
||||
|
||||
let payload;
|
||||
try {
|
||||
payload = await adapter.query(store, schema, {
|
||||
...remainingQuery,
|
||||
list_token: listToken?.token,
|
||||
});
|
||||
} catch (e) {
|
||||
// If we get an invalid list token, we'll delete the token and
|
||||
// clear the DB and try again without a token.
|
||||
if (
|
||||
e?.errors[0].status === 400 &&
|
||||
e?.errors[0].code === 'invalid list token'
|
||||
) {
|
||||
await indexedDb[type].clear();
|
||||
// Delete all tokens of the same resource type so we don't keep clearing the DB.
|
||||
// Even if other tokens may still be valid, we might have cleared data that they
|
||||
// depended on so we should clear all tokens of the same type when clearing the DB.
|
||||
const tokenKeys = await indexedDb.token
|
||||
.where(':id')
|
||||
.startsWith(type)
|
||||
.primaryKeys();
|
||||
|
||||
await indexedDb.token.bulkDelete(tokenKeys);
|
||||
|
||||
payload = await adapter.query(store, schema, {
|
||||
...remainingQuery,
|
||||
});
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// Store the token we just got back from the payload if it exists
|
||||
if (payload.list_token && storeToken) {
|
||||
await indexedDb.token.put({
|
||||
id: tokenKey,
|
||||
token: payload.list_token,
|
||||
});
|
||||
}
|
||||
|
||||
// Remove any records from the DB if the API indicates they've been deleted
|
||||
if (payload.removed_ids?.length > 0) {
|
||||
await indexedDb[type].bulkDelete(payload.removed_ids);
|
||||
}
|
||||
// This is a temporary fix of clearing the DB (specifically for auth-methods)
|
||||
// since we are not storing the token we do not get back a list of removed_ids
|
||||
// from the API call and we do not want deleted items from showing in list view.
|
||||
if (!storeToken) {
|
||||
await indexedDb[type].clear();
|
||||
}
|
||||
|
||||
const normalizedPayload = serializer.normalizeResponse(
|
||||
store,
|
||||
schema,
|
||||
payload,
|
||||
null,
|
||||
'query',
|
||||
);
|
||||
|
||||
const { data: payloadData } = normalizedPayload;
|
||||
|
||||
// Store the new data we just got back from the API refresh
|
||||
const items = payloadData.map((datum) =>
|
||||
this.indexedDb.normalizeData({
|
||||
data: datum,
|
||||
cleanData: true,
|
||||
schema,
|
||||
serializer,
|
||||
}),
|
||||
);
|
||||
|
||||
await indexedDb[type].bulkPut(items);
|
||||
}
|
||||
|
||||
const indexedDbResults = await queryIndexedDb(
|
||||
indexedDb,
|
||||
type,
|
||||
queryObj,
|
||||
);
|
||||
|
||||
const dbRecords = paginateResults(indexedDbResults, page, pageSize).map(
|
||||
(item) =>
|
||||
this.indexedDb.normalizeData({
|
||||
data: item,
|
||||
cleanData: false,
|
||||
schema,
|
||||
serializer,
|
||||
}),
|
||||
);
|
||||
|
||||
// Return the raw data if we don't push to the store.
|
||||
let records = dbRecords.map((record) => ({
|
||||
...record.attributes,
|
||||
id: record.id,
|
||||
}));
|
||||
|
||||
if (pushToStore) {
|
||||
records = store.push({ data: dbRecords });
|
||||
}
|
||||
|
||||
// Set a meta property on the array to store the total items of the results.
|
||||
// This isn't conventional but is better than returning an ArrayProxy
|
||||
// or EmberArray since the ember store query method asserts it has to be an array
|
||||
// so we can't just return an object.
|
||||
records.meta = { totalItems: indexedDbResults.length };
|
||||
return records;
|
||||
}
|
||||
default:
|
||||
return next(context.request);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
import { registerDeprecationHandler } from '@ember/debug';
|
||||
|
||||
const deprecationList = ['ember-data:deprecate-early-static'];
|
||||
|
||||
export function initialize() {
|
||||
registerDeprecationHandler((message, options, next) => {
|
||||
// TODO: Remove deprecation handling for `deprecate-early-static` warning
|
||||
// once we upgrade to ember-cli-mirage v3.0.0 which fixes this issue.
|
||||
// More info about this issue here: https://github.com/miragejs/ember-cli-mirage/issues/2441
|
||||
if (options?.id && deprecationList.includes(options.id)) {
|
||||
return;
|
||||
}
|
||||
if (options?.url) {
|
||||
message += options.url;
|
||||
}
|
||||
next(message, options);
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
initialize,
|
||||
};
|
||||
@@ -6,7 +6,13 @@
|
||||
import GeneratedCredentialStoreModel from '../generated/models/credential-store';
|
||||
import { equal } from '@ember/object/computed';
|
||||
|
||||
export const types = ['static', 'vault'];
|
||||
export const TYPE_CREDENTIAL_STORE_STATIC = 'static';
|
||||
export const TYPE_CREDENTIAL_STORE_VAULT = 'vault';
|
||||
|
||||
export const TYPES_CREDENTIAL_STORE = Object.freeze([
|
||||
TYPE_CREDENTIAL_STORE_STATIC,
|
||||
TYPE_CREDENTIAL_STORE_VAULT,
|
||||
]);
|
||||
|
||||
export default class CredentialStoreModel extends GeneratedCredentialStoreModel {
|
||||
// =attributes
|
||||
|
||||
@@ -5,8 +5,19 @@
|
||||
|
||||
import GeneratedHostCatalogModel from '../generated/models/host-catalog';
|
||||
|
||||
export const types = ['static', 'plugin'];
|
||||
export const pluginTypes = ['aws', 'azure'];
|
||||
export const TYPE_HOST_CATALOG_STATIC = 'static';
|
||||
export const TYPE_HOST_CATALOG_DYNAMIC = 'plugin';
|
||||
export const TYPES_HOST_CATALOG = Object.freeze([
|
||||
TYPE_HOST_CATALOG_STATIC,
|
||||
TYPE_HOST_CATALOG_DYNAMIC,
|
||||
]);
|
||||
|
||||
export const TYPE_HOST_CATALOG_PLUGIN_AWS = 'aws';
|
||||
export const TYPE_HOST_CATALOG_PLUGIN_AZURE = 'azure';
|
||||
export const TYPES_HOST_CATALOG_PLUGIN = [
|
||||
TYPE_HOST_CATALOG_PLUGIN_AWS,
|
||||
TYPE_HOST_CATALOG_PLUGIN_AZURE,
|
||||
];
|
||||
|
||||
export default class HostCatalogModel extends GeneratedHostCatalogModel {
|
||||
// =attributes
|
||||
@@ -32,7 +43,9 @@ export default class HostCatalogModel extends GeneratedHostCatalogModel {
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isUnknown() {
|
||||
return this.isPlugin && !pluginTypes.includes(this.plugin?.name);
|
||||
return (
|
||||
this.isPlugin && !TYPES_HOST_CATALOG_PLUGIN.includes(this.plugin?.name)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import GeneratedHostSetModel from '../generated/models/host-set';
|
||||
import { pluginTypes } from './host-catalog';
|
||||
import { TYPES_HOST_CATALOG_PLUGIN } from './host-catalog';
|
||||
|
||||
export default class HostSetModel extends GeneratedHostSetModel {
|
||||
// =attributes
|
||||
@@ -30,7 +30,9 @@ export default class HostSetModel extends GeneratedHostSetModel {
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isUnknown() {
|
||||
return this.isPlugin && !pluginTypes.includes(this.plugin?.name);
|
||||
return (
|
||||
this.isPlugin && !TYPES_HOST_CATALOG_PLUGIN.includes(this.plugin?.name)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import GeneratedHostModel from '../generated/models/host';
|
||||
import { attr } from '@ember-data/model';
|
||||
import { pluginTypes } from './host-catalog';
|
||||
import { TYPES_HOST_CATALOG_PLUGIN } from './host-catalog';
|
||||
|
||||
export default class HostModel extends GeneratedHostModel {
|
||||
// =attributes
|
||||
@@ -64,7 +64,9 @@ export default class HostModel extends GeneratedHostModel {
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isUnknown() {
|
||||
return this.isPlugin && !pluginTypes.includes(this.plugin?.name);
|
||||
return (
|
||||
this.isPlugin && !TYPES_HOST_CATALOG_PLUGIN.includes(this.plugin?.name)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,17 +6,11 @@
|
||||
import GeneratedRoleModel from '../generated/models/role';
|
||||
import { attr } from '@ember-data/model';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { all, resolve } from 'rsvp';
|
||||
import {
|
||||
TYPE_AUTH_METHOD_OIDC,
|
||||
TYPE_AUTH_METHOD_LDAP,
|
||||
} from 'api/models/auth-method';
|
||||
|
||||
export default class RoleModel extends GeneratedRoleModel {
|
||||
// =services
|
||||
|
||||
@service store;
|
||||
@service resourceFilterStore;
|
||||
|
||||
// =attributes
|
||||
|
||||
@@ -27,7 +21,7 @@ export default class RoleModel extends GeneratedRoleModel {
|
||||
* see obvious. Instead, the application layer is expected to load referenced
|
||||
* users and groups as needed.
|
||||
*/
|
||||
@attr('principal-array', {
|
||||
@attr('array', {
|
||||
readOnly: true,
|
||||
emptyArrayIfMissing: true,
|
||||
})
|
||||
@@ -55,7 +49,7 @@ export default class RoleModel extends GeneratedRoleModel {
|
||||
get userIDs() {
|
||||
return this.principals
|
||||
.filter(({ type }) => type === 'user')
|
||||
.map(({ principal_id }) => principal_id);
|
||||
.map(({ id }) => id);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -65,7 +59,7 @@ export default class RoleModel extends GeneratedRoleModel {
|
||||
get groupIDs() {
|
||||
return this.principals
|
||||
.filter(({ type }) => type === 'group')
|
||||
.map(({ principal_id }) => principal_id);
|
||||
.map(({ id }) => id);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -75,94 +69,9 @@ export default class RoleModel extends GeneratedRoleModel {
|
||||
get managedGroupIDs() {
|
||||
return this.principals
|
||||
.filter(({ type }) => type === 'managed group')
|
||||
.map(({ principal_id }) => principal_id);
|
||||
.map(({ id }) => 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: [TYPE_AUTH_METHOD_OIDC, TYPE_AUTH_METHOD_LDAP] },
|
||||
{ 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
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,13 +4,15 @@
|
||||
*/
|
||||
|
||||
import GeneratedScopeModel from '../generated/models/scope';
|
||||
import { computed } from '@ember/object';
|
||||
|
||||
export const scopeTypes = {
|
||||
global: 'global',
|
||||
org: 'org',
|
||||
project: 'project',
|
||||
};
|
||||
export const TYPE_SCOPE_GLOBAL = 'global';
|
||||
export const TYPE_SCOPE_ORG = 'org';
|
||||
export const TYPE_SCOPE_PROJECT = 'project';
|
||||
export const TYPES_SCOPE = Object.freeze([
|
||||
TYPE_SCOPE_GLOBAL,
|
||||
TYPE_SCOPE_ORG,
|
||||
TYPE_SCOPE_PROJECT,
|
||||
]);
|
||||
|
||||
export default class ScopeModel extends GeneratedScopeModel {
|
||||
// =attributes
|
||||
@@ -18,9 +20,8 @@ export default class ScopeModel extends GeneratedScopeModel {
|
||||
/**
|
||||
* @type {boolean}
|
||||
*/
|
||||
@computed('type')
|
||||
get isGlobal() {
|
||||
return this.type === scopeTypes.global;
|
||||
return this.type === TYPE_SCOPE_GLOBAL;
|
||||
}
|
||||
// There is only one global scope and it cannot be created by clients,
|
||||
// thus no set.
|
||||
@@ -28,23 +29,21 @@ export default class ScopeModel extends GeneratedScopeModel {
|
||||
/**
|
||||
* @type {boolean}
|
||||
*/
|
||||
@computed('type')
|
||||
get isOrg() {
|
||||
return this.type === scopeTypes.org;
|
||||
return this.type === TYPE_SCOPE_ORG;
|
||||
}
|
||||
set isOrg(value) {
|
||||
if (value) this.type = scopeTypes.org;
|
||||
if (value) this.type = TYPE_SCOPE_ORG;
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {boolean}
|
||||
*/
|
||||
@computed('type')
|
||||
get isProject() {
|
||||
return this.type === scopeTypes.project;
|
||||
return this.type === TYPE_SCOPE_PROJECT;
|
||||
}
|
||||
set isProject(value) {
|
||||
if (value) this.type = scopeTypes.project;
|
||||
if (value) this.type = TYPE_SCOPE_PROJECT;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -151,6 +151,17 @@ export default class SessionModel extends GeneratedSessionModel {
|
||||
return this.store.peekRecord('target', this.target_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* The user associated with this session (if loaded).
|
||||
* @type {UserModel}
|
||||
*/
|
||||
get user() {
|
||||
if (!this.user_id) {
|
||||
return null;
|
||||
}
|
||||
return this.store.peekRecord('user', this.user_id);
|
||||
}
|
||||
|
||||
// =methods
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,8 +9,10 @@ export const TYPE_STORAGE_BUCKET_PLUGIN = 'plugin';
|
||||
export const TYPES_STORAGE_BUCKET = Object.freeze([TYPE_STORAGE_BUCKET_PLUGIN]);
|
||||
|
||||
export const TYPE_STORAGE_BUCKET_PLUGIN_AWS_S3 = 'aws';
|
||||
export const TYPE_STORAGE_BUCKET_PLUGIN_MINIO = 'minio';
|
||||
export const TYPES_STORAGE_BUCKET_PLUGIN = Object.freeze([
|
||||
TYPE_STORAGE_BUCKET_PLUGIN_AWS_S3,
|
||||
TYPE_STORAGE_BUCKET_PLUGIN_MINIO,
|
||||
]);
|
||||
|
||||
export const TYPE_CREDENTIAL_STATIC = 'static';
|
||||
@@ -75,6 +77,14 @@ export default class StorageBucketModel extends GeneratedStorageBucketModel {
|
||||
return this.compositeType === TYPE_STORAGE_BUCKET_PLUGIN_AWS_S3;
|
||||
}
|
||||
|
||||
/**
|
||||
* True if the storage bucket plugin type is MinIO
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isMinIO() {
|
||||
return this.compositeType === TYPE_STORAGE_BUCKET_PLUGIN_MINIO;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the storage bucket is a plugin return `plugin.name`,
|
||||
* otherwise return the storage bucket type.
|
||||
|
||||
@@ -135,6 +135,12 @@ export default class TargetModel extends GeneratedTargetModel {
|
||||
return Boolean(pendingOrActiveSessions.length);
|
||||
}
|
||||
|
||||
get associatedAliases() {
|
||||
return this.store
|
||||
.peekAll('alias')
|
||||
.filter((alias) => alias?.destination_id === this.id);
|
||||
}
|
||||
|
||||
// =methods
|
||||
|
||||
/**
|
||||
|
||||
@@ -14,7 +14,7 @@ export default class RoleSerializer extends ApplicationSerializer {
|
||||
* include **only grants** and the version. Normally, grants are not
|
||||
* serialized.
|
||||
* If `adapterOptions.principalIDs` is set (to an array of user and
|
||||
* group IDs), then the payload is serialized via `serializewithPrincipals`.
|
||||
* group IDs), then the payload is serialized via `serializeWithPrincipals`.
|
||||
* @override
|
||||
* @param {Snapshot} snapshot
|
||||
* @return {object}
|
||||
@@ -26,7 +26,7 @@ export default class RoleSerializer extends ApplicationSerializer {
|
||||
serialized = this.serializeWithGrantStrings(snapshot, grantStrings);
|
||||
const principalIDs = snapshot?.adapterOptions?.principalIDs;
|
||||
if (principalIDs)
|
||||
serialized = this.serializewithPrincipals(snapshot, principalIDs);
|
||||
serialized = this.serializeWithPrincipals(snapshot, principalIDs);
|
||||
return serialized;
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ export default class RoleSerializer extends ApplicationSerializer {
|
||||
* @param {[string]} principalIDs
|
||||
* @return {object}
|
||||
*/
|
||||
serializewithPrincipals(snapshot, principalIDs) {
|
||||
serializeWithPrincipals(snapshot, principalIDs) {
|
||||
return {
|
||||
version: snapshot.attr('version'),
|
||||
principal_ids: principalIDs,
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
*/
|
||||
|
||||
import ApplicationSerializer from './application';
|
||||
import { TYPE_CREDENTIAL_DYNAMIC } from '../models/storage-bucket';
|
||||
import {
|
||||
TYPE_CREDENTIAL_DYNAMIC,
|
||||
TYPE_STORAGE_BUCKET_PLUGIN_AWS_S3,
|
||||
} from '../models/storage-bucket';
|
||||
|
||||
const fieldsByCredentialType = {
|
||||
static: [
|
||||
@@ -12,27 +15,55 @@ const fieldsByCredentialType = {
|
||||
'secret_access_key',
|
||||
'region',
|
||||
'disable_credential_rotation',
|
||||
'endpoint_url',
|
||||
],
|
||||
dynamic: [
|
||||
'role_arn',
|
||||
'role_external_id',
|
||||
'role_session_name',
|
||||
'role_tags',
|
||||
'disable_credential_rotation',
|
||||
'region',
|
||||
'disable_credential_rotation',
|
||||
'endpoint_url',
|
||||
],
|
||||
};
|
||||
export default class StorageBucketSerializer extends ApplicationSerializer {
|
||||
/**
|
||||
* Serializes storage-bucket. ATM just AWS needs specific serialization,
|
||||
* for the other plugin types use default.
|
||||
* @param {object} snapshot
|
||||
* @returns
|
||||
*/
|
||||
serialize(snapshot) {
|
||||
const serialized = super.serialize(...arguments);
|
||||
const { credentialType } = snapshot.record;
|
||||
const pluginType = snapshot.record.plugin?.name;
|
||||
const serialized = super.serialize(...arguments);
|
||||
|
||||
// Common for all pluginTypes
|
||||
if (!snapshot.record.isNew) {
|
||||
delete serialized.bucket_name;
|
||||
delete serialized.bucket_prefix;
|
||||
}
|
||||
|
||||
if (credentialType === TYPE_CREDENTIAL_DYNAMIC) {
|
||||
serialized.attributes.disable_credential_rotation = true;
|
||||
if (pluginType === TYPE_STORAGE_BUCKET_PLUGIN_AWS_S3) {
|
||||
return this.serializeAws(serialized, credentialType);
|
||||
}
|
||||
|
||||
return serialized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a storage bucket serialized object specific for aws
|
||||
* @param {object} serialized
|
||||
* @param {string} credentialType
|
||||
* @returns
|
||||
*/
|
||||
serializeAws(serialized, credentialType) {
|
||||
if (serialized.attributes) {
|
||||
if (credentialType === TYPE_CREDENTIAL_DYNAMIC) {
|
||||
serialized.attributes.disable_credential_rotation = true;
|
||||
}
|
||||
delete serialized.attributes.endpoint_url;
|
||||
}
|
||||
return serialized;
|
||||
}
|
||||
@@ -45,7 +76,7 @@ export default class StorageBucketSerializer extends ApplicationSerializer {
|
||||
if (options.isNestedAttribute && json.attributes) {
|
||||
// The key must be included in the fieldsByType list above
|
||||
if (!fieldsByCredentialType[credentialType].includes(key))
|
||||
//API requires these fields to be null
|
||||
// API requires these fields to be null
|
||||
json.attributes[key] = null;
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +44,14 @@ export default class TargetSerializer extends ApplicationSerializer {
|
||||
injectedApplicationCredentialSourceIDs,
|
||||
);
|
||||
|
||||
if (serialized?.with_aliases) {
|
||||
// API expects scope id along with every alias value
|
||||
serialized.with_aliases = serialized.with_aliases.map((item) => ({
|
||||
...item,
|
||||
scope_id: 'global',
|
||||
}));
|
||||
}
|
||||
|
||||
// Delete session recording related fields from non-SSH targets
|
||||
if (type !== TYPE_TARGET_SSH) {
|
||||
delete serialized?.attributes?.storage_bucket_id;
|
||||
@@ -112,4 +120,17 @@ export default class TargetSerializer extends ApplicationSerializer {
|
||||
storage_bucket_id: snapshot?.attr('storage_bucket_id'),
|
||||
};
|
||||
}
|
||||
|
||||
normalize(typeClass, hash, ...rest) {
|
||||
const normalizedHash = structuredClone(hash);
|
||||
const normalized = super.normalize(typeClass, normalizedHash, ...rest);
|
||||
// Ember data retains the previous entry in the array attr when the updated attr becomes undefined/empty.
|
||||
// So, we explicitly set the attr to an empty array if the updated attribute is undefined from the API
|
||||
|
||||
if (!normalized.data.attributes.aliases) {
|
||||
normalized.data.attributes.aliases = [];
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
import Service from '@ember/service';
|
||||
import Dexie from 'dexie';
|
||||
import { typeOf } from '@ember/utils';
|
||||
|
||||
// List of resources we support to be cached and searched in
|
||||
// indexedDb. Any field that can be searched upon should be added to
|
||||
// this index. Increment the version number of the database whenever
|
||||
// indexes change.
|
||||
export const modelIndexes = {
|
||||
token: '&id, token',
|
||||
target:
|
||||
'&id, attributes.created_time, attributes.type, attributes.name, attributes.description, attributes.address, attributes.scope.scope_id',
|
||||
session:
|
||||
'&id, attributes.created_time, attributes.type, attributes.status, attributes.endpoint, attributes.target_id, attributes.user_id, attributes.scope.scope_id',
|
||||
user: '&id, attributes.created_time, attributes.name, attributes.description, attributes.scope.scope_id',
|
||||
group:
|
||||
'&id, attributes.created_time, attributes.name, attributes.description, attributes.scope.scope_id',
|
||||
role: '&id, attributes.created_time, attributes.name, attributes.description, attributes.scope.scope_id',
|
||||
scope:
|
||||
'&id, attributes.created_time, attributes.type, attributes.name, attributes.description, attributes.scope.scope_id',
|
||||
'credential-store':
|
||||
'&id, attributes.created_time, attributes.name, attributes.description, attributes.type, attributes.scope.scope_id',
|
||||
'host-catalog':
|
||||
'&id, attributes.created_time, attributes.type, attributes.name, attributes.description, attributes.scope.scope_id, attributes.plugin.name',
|
||||
'auth-method':
|
||||
'&id, attributes.created_time, attributes.type, attributes.name, attributes.description, attributes.is_primary, attributes.scope.scope_id',
|
||||
};
|
||||
|
||||
export const formatDbName = (userId, clusterUrl) =>
|
||||
`boundary-${userId}-${clusterUrl}`;
|
||||
|
||||
/**
|
||||
* Recursively copy over the object and convert any booleans to "true" or "false" so they can be indexed.
|
||||
* Also converts any ember-like arrays back to a standard array.
|
||||
* @param result
|
||||
* @param object
|
||||
* @returns {*}
|
||||
*/
|
||||
const convertFields = (result, object) => {
|
||||
Object.entries(object).forEach(([key, value]) => {
|
||||
if (typeOf(value) === 'object') {
|
||||
result[key] = convertFields({}, value);
|
||||
} else if (typeOf(value) === 'boolean') {
|
||||
result[key] = value ? 'true' : 'false';
|
||||
} else if (typeOf(value) === 'array') {
|
||||
result[key] = [...value];
|
||||
} else {
|
||||
result[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Recursively copy over the object and convert any string "true" or "false" back to booleans.
|
||||
* Will also convert any array fields back using their model transformation.
|
||||
* @param result
|
||||
* @param object
|
||||
* @param schema
|
||||
* @param serializer
|
||||
* @returns {*}
|
||||
*/
|
||||
const unconvertFields = (result, object, schema, serializer) => {
|
||||
Object.entries(object).forEach(([key, value]) => {
|
||||
if (typeOf(value) === 'object') {
|
||||
result[key] = unconvertFields({}, value, schema, serializer);
|
||||
} else if (value === 'true' || value === 'false') {
|
||||
result[key] = value === 'true';
|
||||
} else if (typeOf(value) === 'array') {
|
||||
const attribute = schema.attributes.get(key);
|
||||
|
||||
// If attribute type is array, we transform it back to a tracked array
|
||||
if (attribute?.type === 'array') {
|
||||
const transformer = serializer.transformFor(attribute.type);
|
||||
result[key] = transformer.deserialize(value, attribute.options);
|
||||
return;
|
||||
}
|
||||
|
||||
result[key] = value;
|
||||
} else {
|
||||
result[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Service to encapsulate the IndexedDB implementation. To use this service, call
|
||||
* `setup` from the application root and
|
||||
*/
|
||||
export default class IndexedDbService extends Service {
|
||||
// =attributes
|
||||
#db;
|
||||
get db() {
|
||||
return this.#db;
|
||||
}
|
||||
|
||||
setup(dbName) {
|
||||
// Don't run setup again if we already have one or if we didn't get a name
|
||||
if (this.#db || !dbName) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#db = new Dexie(dbName);
|
||||
this.#db.version(1).stores(modelIndexes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup the data by converting all fields to plain javascript objects.
|
||||
* We will also convert booleans to "false" or "true".
|
||||
* IndexedDB doesn't support indexes on boolean values so we have to
|
||||
* convert them to be able to support using boolean values.
|
||||
*
|
||||
* Undo the operation by setting `convertFields` to false.
|
||||
*
|
||||
* @param {Object} data
|
||||
* @param {Boolean} cleanData
|
||||
* @param {Object} schema
|
||||
* @param {Object} serializer
|
||||
* @return {{id, type, attributes: {}, relationships: {}}}
|
||||
*/
|
||||
normalizeData({ data, cleanData, schema, serializer }) {
|
||||
if (!data) {
|
||||
return data;
|
||||
}
|
||||
|
||||
let { id, type, attributes = {}, relationships = {} } = data;
|
||||
|
||||
return {
|
||||
id,
|
||||
type,
|
||||
attributes: cleanData
|
||||
? convertFields({}, attributes)
|
||||
: unconvertFields({}, attributes, schema, serializer),
|
||||
relationships: cleanData
|
||||
? convertFields({}, relationships)
|
||||
: unconvertFields({}, relationships, schema, serializer),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
import Store, { CacheHandler } from '@ember-data/store';
|
||||
import RequestManager from '@ember-data/request';
|
||||
import { LegacyNetworkHandler } from '@ember-data/legacy-compat';
|
||||
import IndexedDbHandler from 'api/handlers/indexed-db-handler';
|
||||
|
||||
export default class extends Store {
|
||||
requestManager = new RequestManager();
|
||||
@@ -13,7 +14,9 @@ export default class extends Store {
|
||||
constructor(args) {
|
||||
super(args);
|
||||
|
||||
this.requestManager.use([LegacyNetworkHandler]);
|
||||
const indexedDbHandler = new IndexedDbHandler(this);
|
||||
|
||||
this.requestManager.use([indexedDbHandler, LegacyNetworkHandler]);
|
||||
this.requestManager.useCache(CacheHandler);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
import Transform from '@ember-data/serializer/transform';
|
||||
import { TrackedArray } from 'tracked-built-ins';
|
||||
|
||||
export default class ArrayTransform extends Transform {
|
||||
deserialize(serialized) {
|
||||
const arr = new TrackedArray(serialized || []);
|
||||
return arr;
|
||||
}
|
||||
|
||||
serialize(deserialized) {
|
||||
const arr = deserialized || [];
|
||||
return arr;
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
import Transform from '@ember-data/serializer/transform';
|
||||
|
||||
export default class PrincipalArrayTransform extends Transform {
|
||||
deserialize(serialized) {
|
||||
const stringValues = serialized || [];
|
||||
return stringValues.map(({ id: principal_id, scope_id, type }) => ({
|
||||
principal_id,
|
||||
scope_id,
|
||||
type,
|
||||
}));
|
||||
}
|
||||
|
||||
serialize(deserialized) {
|
||||
const strings = deserialized || [];
|
||||
return strings.map(({ principal_id: id, scope_id, type }) => ({
|
||||
id,
|
||||
scope_id,
|
||||
type,
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* Takes an object and gets the JSON stringified version of it to be converted
|
||||
* to a hash code. Taken from https://stackoverflow.com/a/8076436
|
||||
* @param obj
|
||||
*/
|
||||
export const hashCode = (obj) => {
|
||||
if (!obj) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Use a replacer function to sort the keys of the object so we get the same hashcode
|
||||
// regardless of the order of the keys.
|
||||
const jsonString = JSON.stringify(obj, Object.keys(obj).sort());
|
||||
|
||||
let hash = 0;
|
||||
for (let i = 0; i < jsonString.length; i++) {
|
||||
let code = jsonString.charCodeAt(i);
|
||||
hash = (hash << 5) - hash + code;
|
||||
hash = hash & hash; // Convert to 32bit integer
|
||||
}
|
||||
|
||||
// Make sure the hash is positive since we're using it as an ID
|
||||
return Math.abs(hash);
|
||||
};
|
||||
@@ -0,0 +1,263 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
import { get } from '@ember/object';
|
||||
import { modelIndexes } from '../services/indexed-db';
|
||||
|
||||
/**
|
||||
* Takes a POJO representing a filter query and builds a query to
|
||||
* query indexedDb with. Note that the first filter is important as that
|
||||
* will use the actual index in indexedDb while any subsequent ones will be less efficient.
|
||||
*
|
||||
* @example
|
||||
* const filter = queryIndexedDb(indexedDb, type, {
|
||||
* search: generated',
|
||||
* filters: {
|
||||
* scope_id: {
|
||||
* logicalOperator: 'and',
|
||||
* values: [{ notEquals: 'scope1' }, { notEquals: 'scope2' }],
|
||||
* },
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export async function queryIndexedDb(indexedDb, resource, query) {
|
||||
let { search, filters = {} } = query ?? {};
|
||||
let filterCollection = indexedDb[resource];
|
||||
|
||||
search = getSearchObject(search, resource);
|
||||
filters = cleanObject(filters);
|
||||
|
||||
if (Object.keys(filters).length) {
|
||||
// Remove the first item to use as the initial collection and then reduce the remaining ones
|
||||
const entries = Object.entries(filters);
|
||||
const [firstKey, firstFilterArrayOrObject] = entries.shift();
|
||||
|
||||
filterCollection = await entries.reduce(
|
||||
async (currCollection, [key, filterArrayOrObject]) => {
|
||||
// Grab the results from the set of one filters to be `AND` together with the rest of the filters
|
||||
const filterResults = await buildInitialWhereClause({
|
||||
table: indexedDb[resource],
|
||||
key,
|
||||
filterArrayOrObject,
|
||||
}).primaryKeys();
|
||||
const resultIdsSet = new Set(filterResults);
|
||||
|
||||
return (await currCollection).and((record) => {
|
||||
return resultIdsSet.has(record.id);
|
||||
});
|
||||
},
|
||||
buildInitialWhereClause({
|
||||
key: firstKey,
|
||||
table: indexedDb[resource],
|
||||
filterArrayOrObject: firstFilterArrayOrObject,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (search) {
|
||||
const { text, fields } = search;
|
||||
|
||||
filterCollection = filterCollection.filter((record) =>
|
||||
fields
|
||||
.map((key) => get(record, key))
|
||||
.some((element) => {
|
||||
if (typeof element === 'string') {
|
||||
return element?.toLowerCase().includes(text?.toLowerCase());
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// OrderBy has to be used on where clauses while if it's a
|
||||
// collection already we have to use sortBy. We'll also sort by
|
||||
// descending order based on created time to match the API as any
|
||||
// "or" clause can change the order
|
||||
if (filterCollection.orderBy) {
|
||||
return filterCollection.orderBy(getKey('created_time')).reverse().toArray();
|
||||
}
|
||||
return filterCollection.reverse().sortBy(getKey('created_time'));
|
||||
}
|
||||
|
||||
const buildInitialWhereClause = ({ filterArrayOrObject, table, key }) => {
|
||||
const filterValueArray = Array.isArray(filterArrayOrObject)
|
||||
? filterArrayOrObject
|
||||
: filterArrayOrObject.values;
|
||||
|
||||
const { logicalOperator } = filterArrayOrObject;
|
||||
|
||||
// Short circuit if all the filter types were equals or notEquals since indexedDb
|
||||
// can more efficiently query if we pass all values at once
|
||||
const checkAllFilterType = (type) =>
|
||||
filterValueArray.every((obj) => Object.keys(obj)[0] === type);
|
||||
if (checkAllFilterType('equals')) {
|
||||
return table
|
||||
.where(getKey(key))
|
||||
.anyOf(filterValueArray.map((obj) => obj['equals']));
|
||||
}
|
||||
if (checkAllFilterType('notEquals')) {
|
||||
return table
|
||||
.where(getKey(key))
|
||||
.noneOf(filterValueArray.map((obj) => obj['notEquals']));
|
||||
}
|
||||
|
||||
// Remove the first item to use as the initial where clause and then reduce the remaining ones
|
||||
const [firstOperation, firstFilterValue] = Object.entries(
|
||||
filterValueArray[0],
|
||||
)[0];
|
||||
|
||||
// Not all are filters are equals or notEquals,
|
||||
// manually build the initial where clause and resulting collections
|
||||
return filterValueArray.slice(1).reduce(
|
||||
(result, currObj) => {
|
||||
const [operation, value] = Object.entries(currObj)[0];
|
||||
|
||||
return buildIndexedDbCollection({
|
||||
key: getKey(key),
|
||||
value,
|
||||
collection: result,
|
||||
operation,
|
||||
logicalOperator,
|
||||
});
|
||||
},
|
||||
buildIndexedDbWhere({
|
||||
table,
|
||||
key: getKey(key),
|
||||
operation: firstOperation,
|
||||
value: firstFilterValue,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Builds the initial where clause for indexed DB to filter on. Note that this makes
|
||||
* the first filter important as that will do the real filtering from indexed DB while
|
||||
* any subsequent filter has to be done in memory.
|
||||
*/
|
||||
const buildIndexedDbWhere = ({ table, key, value, operation }) => {
|
||||
const baseWhereClause = table.where(key);
|
||||
switch (operation) {
|
||||
case 'contains':
|
||||
// We return an error due to not being able to support contains on first filter due to performance.
|
||||
throw new Error('Contains is not supported as the first filter.');
|
||||
case 'gt':
|
||||
return baseWhereClause.above(value);
|
||||
case 'gte':
|
||||
return baseWhereClause.aboveOrEqual(value);
|
||||
case 'lt':
|
||||
return baseWhereClause.below(value);
|
||||
case 'lte':
|
||||
return baseWhereClause.belowOrEqual(value);
|
||||
case 'notEquals':
|
||||
return baseWhereClause.notEqual(value);
|
||||
default:
|
||||
return baseWhereClause.equals(value);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Continues building the queries for indexed DB to filter on. Note that these are not true
|
||||
* indexed DB queries and in fact just standard JS filters for logical "and" queries and parallel queries
|
||||
* to indexed DB for logical "or" queries that get deduped.
|
||||
*/
|
||||
const buildIndexedDbCollection = ({
|
||||
key,
|
||||
value,
|
||||
collection,
|
||||
operation,
|
||||
logicalOperator,
|
||||
}) => {
|
||||
if (logicalOperator === 'and') {
|
||||
switch (operation) {
|
||||
case 'contains':
|
||||
return collection.and((record) => get(record, key)?.includes(value));
|
||||
case 'gt':
|
||||
return collection.and((record) => get(record, key) > value);
|
||||
case 'gte':
|
||||
return collection.and((record) => get(record, key) >= value);
|
||||
case 'lt':
|
||||
return collection.and((record) => get(record, key) < value);
|
||||
case 'lte':
|
||||
return collection.and((record) => get(record, key) <= value);
|
||||
case 'notEquals':
|
||||
return collection.and((record) => get(record, key) !== value);
|
||||
default:
|
||||
return collection.and((record) => get(record, key) === value);
|
||||
}
|
||||
}
|
||||
|
||||
switch (operation) {
|
||||
case 'contains':
|
||||
// We return an error due to not being able to support contains on this filter due to performance.
|
||||
throw new Error(
|
||||
'Contains is not supported as a filter option with "or" operator.',
|
||||
);
|
||||
case 'gt':
|
||||
return collection.or(key).above(value);
|
||||
case 'gte':
|
||||
return collection.or(key).aboveOrEqual(value);
|
||||
case 'lt':
|
||||
return collection.or(key).below(value);
|
||||
case 'lte':
|
||||
return collection.or(key).belowOrEqual(value);
|
||||
case 'notEquals':
|
||||
return collection.or(key).notEqual(value);
|
||||
default:
|
||||
return collection.or(key).equals(value);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Translates the fields passed in from the UI for resources to the keys indexed DB knows them as.
|
||||
* Usually it's the path of the actual normalized model.
|
||||
*/
|
||||
const getKey = (key) => {
|
||||
switch (key) {
|
||||
case 'scope_id':
|
||||
return `attributes.scope.${key}`;
|
||||
case 'id':
|
||||
return key;
|
||||
default:
|
||||
return `attributes.${key}`;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Cleans the object to remove any keys that have a value of an empty array. This makes
|
||||
* it easier so we don't have to worry about empty arrays getting queried in indexed db.
|
||||
*/
|
||||
const cleanObject = (obj) => {
|
||||
if (!obj) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return Object.entries(obj).reduce((result, [key, value]) => {
|
||||
if (Array.isArray(value)) {
|
||||
if (value.length > 0) {
|
||||
result[key] = value;
|
||||
}
|
||||
} else if (value.values.length > 0) {
|
||||
result[key] = value;
|
||||
}
|
||||
|
||||
return result;
|
||||
}, {});
|
||||
};
|
||||
|
||||
/**
|
||||
* Makes the search object that is expected when a user searches. Uses
|
||||
* the indexes we setup in indexed db.
|
||||
*/
|
||||
const getSearchObject = (text, resource) => {
|
||||
if (!text) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
text,
|
||||
fields: modelIndexes[resource]
|
||||
.split(', ')
|
||||
.map((index) => index.replace('&', '')),
|
||||
};
|
||||
};
|
||||
@@ -9,8 +9,8 @@
|
||||
*
|
||||
* @example
|
||||
* const filter = generateMQLFilterExpression({
|
||||
* name: [{ contains: 'search text' }],
|
||||
* scope: [{ equals: 'scope1'}, { equals: 'scope2' }],
|
||||
* name: { logicalOperator: 'and', values: [{ notEquals: 'search text' }, { notEquals: 'test' }] },
|
||||
* scope_id: [{ equals: 'scope1'}, { equals: 'scope2' }],
|
||||
* });
|
||||
*
|
||||
* // (name % "search text") and (scope_id = "scope1" or scope_id = "scope2")
|
||||
@@ -21,10 +21,10 @@ export function generateMQLFilterExpression(filterObj) {
|
||||
}
|
||||
|
||||
const expressions = Object.entries(filterObj)
|
||||
.map(([key, filterObjValue]) => {
|
||||
const filterValueArray = Array.isArray(filterObjValue)
|
||||
? filterObjValue
|
||||
: filterObjValue.values;
|
||||
.map(([key, filterArrayOrObject]) => {
|
||||
const filterValueArray = Array.isArray(filterArrayOrObject)
|
||||
? filterArrayOrObject
|
||||
: filterArrayOrObject.values;
|
||||
|
||||
let clauses;
|
||||
const filterValues = filterValueArray.map((filterObjValue) => {
|
||||
@@ -53,7 +53,7 @@ export function generateMQLFilterExpression(filterObj) {
|
||||
});
|
||||
|
||||
// Default to `or` if we don't have a logical operator
|
||||
const { logicalOperator } = filterObjValue;
|
||||
const { logicalOperator } = filterArrayOrObject;
|
||||
if (logicalOperator === 'and') {
|
||||
clauses = and(filterValues);
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* Takes an array and the current page and pagesize to calculate the correct
|
||||
* number of results to return to the caller.
|
||||
*
|
||||
* @param array
|
||||
* @param page
|
||||
* @param pageSize
|
||||
* @returns {[*]}
|
||||
*/
|
||||
export const paginateResults = (array, page, pageSize) => {
|
||||
const length = array?.length;
|
||||
if (!array || length === 0) {
|
||||
return [];
|
||||
}
|
||||
if (!page || !pageSize) {
|
||||
return array;
|
||||
}
|
||||
|
||||
const offset = (page - 1) * pageSize;
|
||||
const start = Math.min(length - 1, offset);
|
||||
const end = Math.min(length, offset + pageSize);
|
||||
|
||||
return array.slice(start, end);
|
||||
};
|
||||
@@ -3,4 +3,4 @@
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
export { default } from 'api/handlers/client-daemon-handler';
|
||||
export { default, resourceNames } from 'api/handlers/client-daemon-handler';
|
||||
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
export { default, initialize } from 'api/initializers/deprecations';
|
||||
+1
-1
@@ -3,4 +3,4 @@
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
export { default } from 'api/transforms/principal-array';
|
||||
export { default } from 'api/services/indexed-db';
|
||||
@@ -3,4 +3,4 @@
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
export { default } from 'api/utils/mql-query';
|
||||
export { default } from 'api/transforms/array';
|
||||
@@ -117,28 +117,7 @@ function routes() {
|
||||
);
|
||||
// Auth & IAM resources
|
||||
|
||||
this.get(
|
||||
'/auth-methods',
|
||||
({ authMethods }, { queryParams: { scope_id, recursive, filter } }) => {
|
||||
let resultSet;
|
||||
if (recursive && scope_id === 'global') {
|
||||
resultSet = authMethods.all();
|
||||
} else if (recursive) {
|
||||
resultSet = authMethods.where((authMethod) => {
|
||||
const authMethodModel = authMethods.find(authMethod.id);
|
||||
return (
|
||||
authMethod.scopeId === scope_id ||
|
||||
authMethodModel?.scope?.scope?.id === scope_id
|
||||
);
|
||||
});
|
||||
} else {
|
||||
resultSet = authMethods.where(
|
||||
(authMethod) => authMethod.scopeId === scope_id,
|
||||
);
|
||||
}
|
||||
return resultSet.filter(makeBooleanFilter(filter));
|
||||
},
|
||||
);
|
||||
this.get('/auth-methods');
|
||||
this.post('/auth-methods', function ({ authMethods }) {
|
||||
const attrs = this.normalizedRequestAttrs();
|
||||
if (attrs.type === 'oidc') {
|
||||
@@ -324,9 +303,7 @@ function routes() {
|
||||
);
|
||||
|
||||
// IAM: Roles
|
||||
this.get('/roles', ({ roles }, { queryParams: { scope_id: scopeId } }) => {
|
||||
return roles.where({ scopeId });
|
||||
});
|
||||
this.get('/roles');
|
||||
this.post('/roles');
|
||||
this.get('/roles/:id');
|
||||
this.patch('/roles/:id');
|
||||
@@ -405,12 +382,7 @@ function routes() {
|
||||
// Other resources
|
||||
// host-catalog
|
||||
|
||||
this.get(
|
||||
'/host-catalogs',
|
||||
({ hostCatalogs }, { queryParams: { scope_id: scopeId } }) => {
|
||||
return hostCatalogs.where({ scopeId });
|
||||
},
|
||||
);
|
||||
this.get('/host-catalogs');
|
||||
this.post(
|
||||
'/host-catalogs',
|
||||
function ({ hostCatalogs }, { queryParams: { plugin_name } }) {
|
||||
@@ -612,11 +584,7 @@ function routes() {
|
||||
|
||||
// credential-stores
|
||||
|
||||
this.get(
|
||||
'/credential-stores',
|
||||
({ credentialStores }, { queryParams: { scope_id: scopeId } }) =>
|
||||
credentialStores.where({ scopeId }),
|
||||
);
|
||||
this.get('/credential-stores');
|
||||
|
||||
this.get('/credential-stores/:id');
|
||||
this.post('/credential-stores');
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
TYPES_STORAGE_BUCKET as types,
|
||||
TYPES_STORAGE_BUCKET_PLUGIN,
|
||||
TYPE_STORAGE_BUCKET_PLUGIN_AWS_S3,
|
||||
TYPE_STORAGE_BUCKET_PLUGIN_MINIO,
|
||||
} from 'api/models/storage-bucket';
|
||||
|
||||
// Represents known plugin types, except "foobar" which models the possibility
|
||||
@@ -50,6 +51,10 @@ export default factory.extend({
|
||||
)}`,
|
||||
disable_credential_rotation: faker.datatype.boolean(),
|
||||
};
|
||||
case TYPE_STORAGE_BUCKET_PLUGIN_MINIO:
|
||||
return {
|
||||
endpoint_url: faker.internet.url(),
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -82,6 +82,14 @@ export default factory.extend({
|
||||
});
|
||||
}
|
||||
|
||||
const aliases = server.schema.aliases.all().models;
|
||||
const randomlySelectedAliases =
|
||||
aliases.length === 0 ? undefined : faker.helpers.arrayElement(aliases);
|
||||
if (randomlySelectedAliases) {
|
||||
target.update({
|
||||
aliases: randomlySelectedAliases.aliases,
|
||||
});
|
||||
}
|
||||
target.update({
|
||||
hostSets: randomlySelectedHostSets,
|
||||
brokeredCredentialSourceIds: [
|
||||
|
||||
@@ -108,9 +108,6 @@ export default function (server) {
|
||||
server.createList('policy', 3, { scope: globalScope });
|
||||
server.createList('policy', 3, { scope: orgScope });
|
||||
|
||||
// Aliases
|
||||
server.createList('alias', 3, { scope: globalScope });
|
||||
|
||||
// Other resources
|
||||
server.schema.scopes.where({ type: 'project' }).models.forEach((scope) => {
|
||||
server.createList('host-catalog', 8, { scope }, 'withChildren');
|
||||
@@ -127,6 +124,18 @@ export default function (server) {
|
||||
server.createList('role', 3, { scope });
|
||||
});
|
||||
|
||||
// Aliases
|
||||
const aliasDestinationTarget = server.schema.targets.all().models[0];
|
||||
const { id: destination_id } = aliasDestinationTarget;
|
||||
const aliases = server.createList('alias', 4, {
|
||||
scope: globalScope,
|
||||
destination_id,
|
||||
});
|
||||
|
||||
aliasDestinationTarget.update({
|
||||
aliases: aliases.map((alias) => ({ id: alias.id, value: alias.value })),
|
||||
});
|
||||
|
||||
// Workers
|
||||
server.createList('worker', 3, { scope: globalScope });
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
import ApplicationSerializer from './application';
|
||||
|
||||
export default ApplicationSerializer.extend({
|
||||
modelName: 'alias',
|
||||
});
|
||||
@@ -20,7 +20,7 @@ export default ApplicationSerializer.extend({
|
||||
this,
|
||||
arguments,
|
||||
);
|
||||
const { hostSets } = this.schema;
|
||||
const { hostSets, aliases } = this.schema;
|
||||
if (model.hostSetIds?.length) {
|
||||
json.host_sources = model.hostSetIds.map((host_set_id) => {
|
||||
const hostSet = hostSets.find(host_set_id);
|
||||
@@ -28,6 +28,23 @@ export default ApplicationSerializer.extend({
|
||||
return { id: host_set_id, host_catalog_id };
|
||||
});
|
||||
}
|
||||
|
||||
// After we clear an associated alias, we try to mimic the BE logic
|
||||
// by manually filter out the alias id from the target model
|
||||
|
||||
const associatedAliases = aliases
|
||||
.all()
|
||||
.models.reduce((arr, { destinationId, id }) => {
|
||||
// We iterate through the aliases list and
|
||||
// find the cleared alias by looking for camelCase destinationId since there is no other way to find the impacted alias
|
||||
if (destinationId !== null) arr.push(id);
|
||||
return arr;
|
||||
}, []);
|
||||
|
||||
json.aliases = model.aliases?.filter((alias) =>
|
||||
associatedAliases.includes(alias.id),
|
||||
);
|
||||
|
||||
return json;
|
||||
},
|
||||
});
|
||||
@@ -23,6 +23,7 @@
|
||||
"doc:toc": "doctoc README.md"
|
||||
},
|
||||
"dependencies": {
|
||||
"dexie": "^3.2.4",
|
||||
"ember-auto-import": "^2.6.3",
|
||||
"ember-can": "^4.2.0",
|
||||
"ember-cli-babel": "^7.26.10",
|
||||
@@ -30,7 +31,6 @@
|
||||
"ember-cli-mirage": "^2.4.0",
|
||||
"ember-data": "~4.12.0",
|
||||
"ember-fetch": "^8.1.1",
|
||||
"ember-auto-import": "^2.6.3",
|
||||
"tracked-built-ins": "^3.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -64,6 +64,7 @@
|
||||
"eslint-plugin-ember": "^11.11.1",
|
||||
"eslint-plugin-n": "^16.3.1",
|
||||
"eslint-plugin-qunit": "^8.0.1",
|
||||
"@faker-js/faker": "^8.0.2",
|
||||
"js-bexpr": "hashicorp/js-bexpr#9b4a4b54d85eba67fdfc0990133d1518d890b1e1",
|
||||
"loader.js": "^4.7.0",
|
||||
"prettier": "^3.0.0",
|
||||
|
||||
@@ -28,6 +28,10 @@ module.exports = function (environment) {
|
||||
host: '',
|
||||
namespace: 'v1',
|
||||
},
|
||||
|
||||
'ember-cli-mirage': {
|
||||
directory: './mirage',
|
||||
},
|
||||
};
|
||||
|
||||
if (environment === 'development') {
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
import { createServer } from 'miragejs';
|
||||
|
||||
export default function (config) {
|
||||
return createServer({ ...config });
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
export default function (/* server */) {
|
||||
/*
|
||||
Seed your development database using your factories.
|
||||
This data will not be loaded in your tests.
|
||||
*/
|
||||
// server.createList('post', 10);
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
import { JSONAPISerializer } from 'miragejs';
|
||||
|
||||
export default JSONAPISerializer.extend({});
|
||||
@@ -100,7 +100,7 @@ module('Unit | Adapter | application', function (hooks) {
|
||||
test('it can request records through the store from a specified scope', async function (assert) {
|
||||
assert.expect(1);
|
||||
const store = this.owner.lookup('service:store');
|
||||
this.server.get('/v1/groups', (_, { queryParams: { scope_id } }) => {
|
||||
this.server.get('/groups', (_, { queryParams: { scope_id } }) => {
|
||||
assert.strictEqual(
|
||||
scope_id,
|
||||
'p_456',
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
import Application from '@ember/application';
|
||||
|
||||
import config from 'dummy/config/environment';
|
||||
import { initialize } from 'dummy/initializers/deprecations';
|
||||
import { module, test } from 'qunit';
|
||||
import Resolver from 'ember-resolver';
|
||||
import { run } from '@ember/runloop';
|
||||
|
||||
module('Unit | Initializer | deprecations', function (hooks) {
|
||||
hooks.beforeEach(function () {
|
||||
this.TestApplication = class TestApplication extends Application {
|
||||
modulePrefix = config.modulePrefix;
|
||||
podModulePrefix = config.podModulePrefix;
|
||||
Resolver = Resolver;
|
||||
};
|
||||
|
||||
this.TestApplication.initializer({
|
||||
name: 'initializer under test',
|
||||
initialize,
|
||||
});
|
||||
|
||||
this.application = this.TestApplication.create({
|
||||
autoboot: false,
|
||||
});
|
||||
});
|
||||
|
||||
hooks.afterEach(function () {
|
||||
run(this.application, 'destroy');
|
||||
});
|
||||
|
||||
// TODO: Replace this with your real tests.
|
||||
test('it works', async function (assert) {
|
||||
await this.application.boot();
|
||||
|
||||
assert.ok(true);
|
||||
});
|
||||
});
|
||||
@@ -29,19 +29,16 @@ module('Unit | Model | auth method', function (hooks) {
|
||||
|
||||
test('it has a `changeState` method that targets a specific POST API endpoint and serialization', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.post(
|
||||
'/v1/auth-methods/123abc:change-state',
|
||||
(schema, request) => {
|
||||
const body = JSON.parse(request.requestBody);
|
||||
assert.deepEqual(body, {
|
||||
attributes: {
|
||||
state: 'foobar',
|
||||
},
|
||||
version: 1,
|
||||
});
|
||||
return { id: '123abc' };
|
||||
},
|
||||
);
|
||||
this.server.post('/auth-methods/123abc:change-state', (schema, request) => {
|
||||
const body = JSON.parse(request.requestBody);
|
||||
assert.deepEqual(body, {
|
||||
attributes: {
|
||||
state: 'foobar',
|
||||
},
|
||||
version: 1,
|
||||
});
|
||||
return { id: '123abc' };
|
||||
});
|
||||
store.push({
|
||||
data: {
|
||||
id: '123abc',
|
||||
|
||||
@@ -80,7 +80,7 @@ module('Unit | Model | base', function (hooks) {
|
||||
attributes: {},
|
||||
},
|
||||
});
|
||||
this.server.patch('/v1/users/1', () => {
|
||||
this.server.patch('users/1', () => {
|
||||
assert.ok(true, 'Correctly scoped update record URL was requested.');
|
||||
return { id: 1 };
|
||||
});
|
||||
@@ -112,17 +112,17 @@ module('Unit | Model | base', function (hooks) {
|
||||
assert.expect(3);
|
||||
const store = this.owner.lookup('service:store');
|
||||
const model = store.createRecord('user', { scopeID: 'o_123' });
|
||||
this.server.post('/v1/users', () => {
|
||||
this.server.post('users', () => {
|
||||
assert.ok(true, 'Correctly scoped create record URL was requested.');
|
||||
return { id: 'u_123' };
|
||||
});
|
||||
await model.save();
|
||||
this.server.patch('/v1/users/u_123', () => {
|
||||
this.server.patch('users/u_123', () => {
|
||||
assert.ok(true, 'Correctly scoped update record URL was requested.');
|
||||
return { id: 'u_123' };
|
||||
});
|
||||
await model.save();
|
||||
this.server.delete('/v1/users/u_123', () => {
|
||||
this.server.delete('users/u_123', () => {
|
||||
assert.ok(true, 'Correctly scoped delete record URL was requested.');
|
||||
return {};
|
||||
});
|
||||
@@ -135,17 +135,17 @@ module('Unit | Model | base', function (hooks) {
|
||||
const customScopeID = 'global';
|
||||
const scope = { scope_id: 'o_123' };
|
||||
const model = store.createRecord('user', { scope });
|
||||
this.server.post('/v1/users', () => {
|
||||
this.server.post('users', () => {
|
||||
assert.ok(true, 'Correctly scoped create record URL was requested.');
|
||||
return { id: 'u_123' };
|
||||
});
|
||||
await model.save({ adapterOptions: { scopeID: customScopeID } });
|
||||
this.server.patch('/v1/users/u_123', () => {
|
||||
this.server.patch('users/u_123', () => {
|
||||
assert.ok(true, 'Correctly scoped update record URL was requested.');
|
||||
return { id: 'u_123' };
|
||||
});
|
||||
await model.save({ adapterOptions: { scopeID: customScopeID } });
|
||||
this.server.delete('/v1/users/u_123', () => {
|
||||
this.server.delete('users/u_123', () => {
|
||||
assert.ok(true, 'Correctly scoped delete record URL was requested.');
|
||||
return {};
|
||||
});
|
||||
@@ -158,7 +158,7 @@ module('Unit | Model | base', function (hooks) {
|
||||
const method = 'my-custom-method';
|
||||
const scope = { scope_id: 'o_123' };
|
||||
const model = store.createRecord('user', { scope });
|
||||
this.server.post(`/v1/users:${method}`, () => {
|
||||
this.server.post(`users:${method}`, () => {
|
||||
assert.ok(true, 'Correctly scoped create record URL was requested.');
|
||||
return { id: 'u_123' };
|
||||
});
|
||||
|
||||
@@ -63,7 +63,7 @@ module('Unit | Model | group', function (hooks) {
|
||||
|
||||
test('it has an `addMembers` method that groups a specific POST API endpoint and serialization', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.post('/v1/groups/123abc:add-members', (schema, request) => {
|
||||
this.server.post('/groups/123abc:add-members', (schema, request) => {
|
||||
const body = JSON.parse(request.requestBody);
|
||||
assert.deepEqual(body, {
|
||||
member_ids: ['123_abc', 'foobar'],
|
||||
@@ -94,7 +94,7 @@ module('Unit | Model | group', function (hooks) {
|
||||
|
||||
test('it has a `removeMembers` method that groups a specific POST API endpoint and serialization', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.post('/v1/groups/123abc:remove-members', (schema, request) => {
|
||||
this.server.post('/groups/123abc:remove-members', (schema, request) => {
|
||||
const body = JSON.parse(request.requestBody);
|
||||
assert.deepEqual(body, {
|
||||
member_ids: ['1', '3'],
|
||||
@@ -125,7 +125,7 @@ module('Unit | Model | group', function (hooks) {
|
||||
|
||||
test('it has a `removeMember` method that deletes a single member set using `removeMembers` method', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.post('/v1/groups/123abc:remove-members', (schema, request) => {
|
||||
this.server.post('/groups/123abc:remove-members', (schema, request) => {
|
||||
const body = JSON.parse(request.requestBody);
|
||||
assert.deepEqual(body, {
|
||||
member_ids: ['3'],
|
||||
|
||||
@@ -13,7 +13,7 @@ module('Unit | Model | host set', function (hooks) {
|
||||
|
||||
test('it has an `addHosts` method that targets a specific POST API endpoint and serialization', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.post('/v1/host-sets/123abc:add-hosts', (schema, request) => {
|
||||
this.server.post('/host-sets/123abc:add-hosts', (schema, request) => {
|
||||
const body = JSON.parse(request.requestBody);
|
||||
assert.deepEqual(body, {
|
||||
host_ids: ['123_abc', 'foobar'],
|
||||
@@ -44,7 +44,7 @@ module('Unit | Model | host set', function (hooks) {
|
||||
|
||||
test('it has an `addHost` method that adds a single host using `addHosts` method', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.post('/v1/host-sets/123abc:add-hosts', (schema, request) => {
|
||||
this.server.post('/host-sets/123abc:add-hosts', (schema, request) => {
|
||||
const body = JSON.parse(request.requestBody);
|
||||
assert.deepEqual(body, {
|
||||
host_ids: ['foobar'],
|
||||
@@ -75,7 +75,7 @@ module('Unit | Model | host set', function (hooks) {
|
||||
|
||||
test('it has a `removeHosts` method that targets a specific POST API endpoint and serialization', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.post('/v1/host-sets/123abc:remove-hosts', (schema, request) => {
|
||||
this.server.post('/host-sets/123abc:remove-hosts', (schema, request) => {
|
||||
const body = JSON.parse(request.requestBody);
|
||||
assert.deepEqual(body, {
|
||||
host_ids: ['3'],
|
||||
@@ -106,7 +106,7 @@ module('Unit | Model | host set', function (hooks) {
|
||||
|
||||
test('it has a `removeHost` method that removes a single host using `removeHosts` method', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.post('/v1/host-sets/123abc:remove-hosts', (schema, request) => {
|
||||
this.server.post('/host-sets/123abc:remove-hosts', (schema, request) => {
|
||||
const body = JSON.parse(request.requestBody);
|
||||
assert.deepEqual(body, {
|
||||
host_ids: ['3'],
|
||||
|
||||
@@ -19,7 +19,7 @@ module('Unit | Model | role', function (hooks) {
|
||||
|
||||
test('it has a `saveGrantStrings` method that targets a specific POST API endpoint and serialization', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.post('/v1/roles/123abc:set-grants', (schema, request) => {
|
||||
this.server.post('/roles/123abc:set-grants', (schema, request) => {
|
||||
const body = JSON.parse(request.requestBody);
|
||||
assert.deepEqual(body, {
|
||||
grant_strings: ['foo', 'bar'],
|
||||
@@ -50,7 +50,7 @@ module('Unit | Model | role', function (hooks) {
|
||||
|
||||
test('it has an `addPrincipals` method that targets a specific POST API endpoint and serialization', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.post('/v1/roles/123abc:add-principals', (schema, request) => {
|
||||
this.server.post('/roles/123abc:add-principals', (schema, request) => {
|
||||
const body = JSON.parse(request.requestBody);
|
||||
assert.deepEqual(body, {
|
||||
principal_ids: ['123_abc', 'foobar'],
|
||||
@@ -85,17 +85,14 @@ module('Unit | Model | role', function (hooks) {
|
||||
|
||||
test('it has a `removePrincipals` method that targets a specific POST API endpoint and serialization', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.post(
|
||||
'/v1/roles/123abc:remove-principals',
|
||||
(schema, request) => {
|
||||
const body = JSON.parse(request.requestBody);
|
||||
assert.deepEqual(body, {
|
||||
principal_ids: ['1', '3', '5'],
|
||||
version: 1,
|
||||
});
|
||||
return { id: '123abc' };
|
||||
},
|
||||
);
|
||||
this.server.post('/roles/123abc:remove-principals', (schema, request) => {
|
||||
const body = JSON.parse(request.requestBody);
|
||||
assert.deepEqual(body, {
|
||||
principal_ids: ['1', '3', '5'],
|
||||
version: 1,
|
||||
});
|
||||
return { id: '123abc' };
|
||||
});
|
||||
const store = this.owner.lookup('service:store');
|
||||
store.push({
|
||||
data: {
|
||||
@@ -123,17 +120,14 @@ module('Unit | Model | role', function (hooks) {
|
||||
|
||||
test('it has a `removePrincipal` method that deletes a single principal using `removePrincipals` method', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.post(
|
||||
'/v1/roles/123abc:remove-principals',
|
||||
(schema, request) => {
|
||||
const body = JSON.parse(request.requestBody);
|
||||
assert.deepEqual(body, {
|
||||
principal_ids: ['3'],
|
||||
version: 1,
|
||||
});
|
||||
return { id: '123abc' };
|
||||
},
|
||||
);
|
||||
this.server.post('/roles/123abc:remove-principals', (schema, request) => {
|
||||
const body = JSON.parse(request.requestBody);
|
||||
assert.deepEqual(body, {
|
||||
principal_ids: ['3'],
|
||||
version: 1,
|
||||
});
|
||||
return { id: '123abc' };
|
||||
});
|
||||
const store = this.owner.lookup('service:store');
|
||||
store.push({
|
||||
data: {
|
||||
|
||||
@@ -13,7 +13,7 @@ module('Unit | Model | scope', function (hooks) {
|
||||
|
||||
test('it may have a scope fragment', async function (assert) {
|
||||
const store = this.owner.lookup('service:store');
|
||||
this.server.get('/v1/scopes', () => ({
|
||||
this.server.get('/scopes', () => ({
|
||||
items: [
|
||||
{ id: 'global', type: 'global' },
|
||||
{ id: 'o_1', type: 'org', scope: { scope_id: 'global' } },
|
||||
@@ -72,7 +72,7 @@ module('Unit | Model | scope', function (hooks) {
|
||||
test('it has an `attachStoragePolicy` method that targets a specific POST API endpoint and serialization', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.post(
|
||||
'/v1/scopes/123abc:attach-storage-policy',
|
||||
'/scopes/123abc:attach-storage-policy',
|
||||
(schema, request) => {
|
||||
const body = JSON.parse(request.requestBody);
|
||||
assert.deepEqual(body, {
|
||||
@@ -106,7 +106,7 @@ module('Unit | Model | scope', function (hooks) {
|
||||
test('it has a `detachStoragePolicy` method that targets a specific POST API endpoint and serialization', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.post(
|
||||
'/v1/scopes/123abc:detach-storage-policy',
|
||||
'/scopes/123abc:detach-storage-policy',
|
||||
(schema, request) => {
|
||||
const body = JSON.parse(request.requestBody);
|
||||
assert.deepEqual(body, {
|
||||
|
||||
@@ -72,4 +72,88 @@ module('Unit | Model | session', function (hooks) {
|
||||
assert.false(modelA.isUnknownStatus);
|
||||
assert.true(modelB.isUnknownStatus);
|
||||
});
|
||||
|
||||
test('it has target property and returns the null with no target_id set', function (assert) {
|
||||
const store = this.owner.lookup('service:store');
|
||||
store.push({
|
||||
data: {
|
||||
id: 's_123',
|
||||
type: 'session',
|
||||
attributes: { target_id: null },
|
||||
},
|
||||
});
|
||||
const session = store.peekRecord('session', 's_123');
|
||||
|
||||
assert.strictEqual(session.target, null);
|
||||
});
|
||||
|
||||
test('it has target property and returns the correct target', function (assert) {
|
||||
const store = this.owner.lookup('service:store');
|
||||
const targetId = 'ttcp_123';
|
||||
store.push({
|
||||
data: {
|
||||
id: targetId,
|
||||
type: 'target',
|
||||
attributes: {
|
||||
name: 'admin',
|
||||
scope: {
|
||||
scope_id: 'o_1',
|
||||
type: 'scope',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
store.push({
|
||||
data: {
|
||||
id: 's_123',
|
||||
type: 'session',
|
||||
attributes: { target_id: targetId },
|
||||
},
|
||||
});
|
||||
const session = store.peekRecord('session', 's_123');
|
||||
|
||||
assert.strictEqual(session.target.id, targetId);
|
||||
});
|
||||
|
||||
test('it has user property and returns the null with no user_id set', function (assert) {
|
||||
const store = this.owner.lookup('service:store');
|
||||
store.push({
|
||||
data: {
|
||||
id: 's_123',
|
||||
type: 'session',
|
||||
attributes: { user_id: null },
|
||||
},
|
||||
});
|
||||
const session = store.peekRecord('session', 's_123');
|
||||
|
||||
assert.strictEqual(session.user, null);
|
||||
});
|
||||
|
||||
test('it has user property and returns the correct user', function (assert) {
|
||||
const store = this.owner.lookup('service:store');
|
||||
const userId = 'u_123';
|
||||
store.push({
|
||||
data: {
|
||||
id: userId,
|
||||
type: 'user',
|
||||
attributes: {
|
||||
name: 'admin',
|
||||
scope: {
|
||||
scope_id: 'o_1',
|
||||
type: 'scope',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
store.push({
|
||||
data: {
|
||||
id: 's_123',
|
||||
type: 'session',
|
||||
attributes: { user_id: userId },
|
||||
},
|
||||
});
|
||||
const session = store.peekRecord('session', 's_123');
|
||||
|
||||
assert.strictEqual(session.user.id, userId);
|
||||
});
|
||||
});
|
||||
@@ -79,17 +79,14 @@ module('Unit | Model | target', function (hooks) {
|
||||
|
||||
test('it has an `addHostSources` method that targets a specific POST API endpoint and serialization', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.post(
|
||||
'/v1/targets/123abc:add-host-sources',
|
||||
(schema, request) => {
|
||||
const body = JSON.parse(request.requestBody);
|
||||
assert.deepEqual(body, {
|
||||
host_source_ids: ['123_abc', 'foobar'],
|
||||
version: 1,
|
||||
});
|
||||
return { id: '123abc' };
|
||||
},
|
||||
);
|
||||
this.server.post('/targets/123abc:add-host-sources', (schema, request) => {
|
||||
const body = JSON.parse(request.requestBody);
|
||||
assert.deepEqual(body, {
|
||||
host_source_ids: ['123_abc', 'foobar'],
|
||||
version: 1,
|
||||
});
|
||||
return { id: '123abc' };
|
||||
});
|
||||
const store = this.owner.lookup('service:store');
|
||||
store.push({
|
||||
data: {
|
||||
@@ -117,7 +114,7 @@ module('Unit | Model | target', function (hooks) {
|
||||
test('it has a `removeHostSources` method that targets a specific POST API endpoint and serialization', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.post(
|
||||
'/v1/targets/123abc:remove-host-sources',
|
||||
'/targets/123abc:remove-host-sources',
|
||||
(schema, request) => {
|
||||
const body = JSON.parse(request.requestBody);
|
||||
assert.deepEqual(body, {
|
||||
@@ -154,7 +151,7 @@ module('Unit | Model | target', function (hooks) {
|
||||
test('it has a `removeHostSource` method that deletes a single host set using `removeHostSources` method', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.post(
|
||||
'/v1/targets/123abc:remove-host-sources',
|
||||
'/targets/123abc:remove-host-sources',
|
||||
(schema, request) => {
|
||||
const body = JSON.parse(request.requestBody);
|
||||
assert.deepEqual(body, {
|
||||
@@ -283,7 +280,7 @@ module('Unit | Model | target', function (hooks) {
|
||||
test('it has an `addBrokeredCredentialSources` method that targets a specific POST API endpoint and serialization', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.post(
|
||||
'/v1/targets/123abc:add-credential-sources',
|
||||
'/targets/123abc:add-credential-sources',
|
||||
(schema, request) => {
|
||||
const body = JSON.parse(request.requestBody);
|
||||
assert.deepEqual(body, {
|
||||
@@ -317,7 +314,7 @@ module('Unit | Model | target', function (hooks) {
|
||||
test('it has a `removeBrokeredCredentialSources` method that targets a specific POST API endpoint and serialization', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.post(
|
||||
'/v1/targets/123abc:remove-credential-sources',
|
||||
'/targets/123abc:remove-credential-sources',
|
||||
(schema, request) => {
|
||||
const body = JSON.parse(request.requestBody);
|
||||
assert.deepEqual(body, {
|
||||
@@ -351,7 +348,7 @@ module('Unit | Model | target', function (hooks) {
|
||||
test('it has a `removeBrokeredCredentialSource` method that deletes a single credential library using `removeBrokeredCredentialSources` method', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.post(
|
||||
'/v1/targets/123abc:remove-credential-sources',
|
||||
'/targets/123abc:remove-credential-sources',
|
||||
(schema, request) => {
|
||||
const body = JSON.parse(request.requestBody);
|
||||
assert.deepEqual(body, {
|
||||
|
||||
@@ -13,7 +13,7 @@ module('Unit | Model | user', function (hooks) {
|
||||
|
||||
test('it has an `addAccounts` method that users a specific POST API endpoint and serialization', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.post('/v1/users/123abc:add-accounts', (schema, request) => {
|
||||
this.server.post('/users/123abc:add-accounts', (schema, request) => {
|
||||
const body = JSON.parse(request.requestBody);
|
||||
assert.deepEqual(body, {
|
||||
account_ids: ['123_abc', 'foobar'],
|
||||
@@ -44,7 +44,7 @@ module('Unit | Model | user', function (hooks) {
|
||||
|
||||
test('it has a `removeAccounts` method that users a specific POST API endpoint and serialization', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.post('/v1/users/123abc:remove-accounts', (schema, request) => {
|
||||
this.server.post('/users/123abc:remove-accounts', (schema, request) => {
|
||||
const body = JSON.parse(request.requestBody);
|
||||
assert.deepEqual(body, {
|
||||
account_ids: ['1', '3'],
|
||||
@@ -75,7 +75,7 @@ module('Unit | Model | user', function (hooks) {
|
||||
|
||||
test('it has a `removeAccount` method that deletes a single account set using `removeAccounts` method', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.post('/v1/users/123abc:remove-accounts', (schema, request) => {
|
||||
this.server.post('/users/123abc:remove-accounts', (schema, request) => {
|
||||
const body = JSON.parse(request.requestBody);
|
||||
assert.deepEqual(body, {
|
||||
account_ids: ['3'],
|
||||
@@ -122,7 +122,7 @@ module('Unit | Model | user', function (hooks) {
|
||||
|
||||
test('it defaults to null for readonly attributes when response does not have fields', async function (assert) {
|
||||
assert.expect(3);
|
||||
this.server.post('/v1/users/123abc:remove-accounts', (schema, request) => {
|
||||
this.server.post('/users/123abc:remove-accounts', (schema, request) => {
|
||||
const body = JSON.parse(request.requestBody);
|
||||
assert.deepEqual(body, {
|
||||
account_ids: ['3'],
|
||||
|
||||
@@ -15,7 +15,7 @@ module('Unit | Model | worker', function (hooks) {
|
||||
assert.expect(2);
|
||||
const scopeId = 'global';
|
||||
const workerGeneratedAuthToken = 'token';
|
||||
this.server.post('/v1/workers:create:worker-led', (schema, request) => {
|
||||
this.server.post('/workers:create:worker-led', (schema, request) => {
|
||||
const body = JSON.parse(request.requestBody);
|
||||
assert.deepEqual(body, {
|
||||
scope_id: scopeId,
|
||||
|
||||
@@ -285,6 +285,7 @@ module('Unit | Serializer | application', function (hooks) {
|
||||
type: 'target',
|
||||
attributes: {
|
||||
authorized_actions: [],
|
||||
aliases: [],
|
||||
name: 'Target 1',
|
||||
scope: { id: 'o_123', scope_id: 'o_123' },
|
||||
host_sources: [],
|
||||
|
||||
@@ -379,7 +379,7 @@ module('Unit | Serializer | auth method', function (hooks) {
|
||||
const dryRun = true;
|
||||
const issuer = 'http://www.example.net';
|
||||
const maxAge = 500;
|
||||
this.server.get('/v1/auth-methods/oidc123', () => ({
|
||||
this.server.get('/auth-methods/oidc123', () => ({
|
||||
attributes: {
|
||||
account_claim_maps: ['from=to', 'foo=bar'],
|
||||
claims_scopes: ['profile', 'email'],
|
||||
@@ -447,7 +447,7 @@ module('Unit | Serializer | auth method', function (hooks) {
|
||||
const clientCerticate = 'certificate-1234';
|
||||
const startTls = true;
|
||||
const insecureTls = false;
|
||||
this.server.get('/v1/auth-methods/ldap123', () => ({
|
||||
this.server.get('/auth-methods/ldap123', () => ({
|
||||
attributes: {
|
||||
account_attribute_maps: ['from=to', 'foo=bar'],
|
||||
urls: [ldapUrl],
|
||||
|
||||
@@ -7,6 +7,7 @@ import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
import {
|
||||
TYPE_STORAGE_BUCKET_PLUGIN_AWS_S3,
|
||||
TYPE_STORAGE_BUCKET_PLUGIN_MINIO,
|
||||
TYPE_STORAGE_BUCKET_PLUGIN,
|
||||
TYPE_CREDENTIAL_STATIC,
|
||||
TYPE_CREDENTIAL_DYNAMIC,
|
||||
@@ -15,15 +16,6 @@ import {
|
||||
module('Unit | Serializer | storage bucket', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test('it serializes records', function (assert) {
|
||||
let store = this.owner.lookup('service:store');
|
||||
let record = store.createRecord('storage-bucket', {});
|
||||
|
||||
let serializedRecord = record.serialize();
|
||||
|
||||
assert.ok(serializedRecord);
|
||||
});
|
||||
|
||||
test('it serializes a new static aws plugin as expected', async function (assert) {
|
||||
const store = this.owner.lookup('service:store');
|
||||
const record = store.createRecord('storage-bucket', {
|
||||
@@ -70,6 +62,47 @@ module('Unit | Serializer | storage bucket', function (hooks) {
|
||||
assert.deepEqual(record.serialize(), expectedResult);
|
||||
});
|
||||
|
||||
test('it serializes a new minio (static by default) plugin as expected', async function (assert) {
|
||||
const store = this.owner.lookup('service:store');
|
||||
const record = store.createRecord('storage-bucket', {
|
||||
compositeType: TYPE_STORAGE_BUCKET_PLUGIN_MINIO,
|
||||
credentialType: TYPE_CREDENTIAL_STATIC,
|
||||
name: 'minio',
|
||||
description: 'this has a minio plugin',
|
||||
bucket_name: 'bucketname',
|
||||
bucket_prefix: 'bucketprefix',
|
||||
worker_filter: 'workerfilter',
|
||||
endpoint_url: 'http://hashicorp.com',
|
||||
region: 'eu-west-1',
|
||||
access_key_id: 'foobars',
|
||||
secret_access_key: 'testing',
|
||||
disable_credential_rotation: true,
|
||||
});
|
||||
const expectedResult = {
|
||||
name: 'minio',
|
||||
description: 'this has a minio plugin',
|
||||
type: TYPE_STORAGE_BUCKET_PLUGIN,
|
||||
bucket_name: 'bucketname',
|
||||
bucket_prefix: 'bucketprefix',
|
||||
worker_filter: 'workerfilter',
|
||||
attributes: {
|
||||
region: 'eu-west-1',
|
||||
disable_credential_rotation: true,
|
||||
endpoint_url: 'http://hashicorp.com',
|
||||
role_arn: null,
|
||||
role_external_id: null,
|
||||
role_session_name: null,
|
||||
role_tags: null,
|
||||
secrets_hmac: null,
|
||||
},
|
||||
secrets: {
|
||||
access_key_id: 'foobars',
|
||||
secret_access_key: 'testing',
|
||||
},
|
||||
};
|
||||
assert.deepEqual(record.serialize(), expectedResult);
|
||||
});
|
||||
|
||||
test('it serializes a new aws plugin with dynamic credentials as expected', async function (assert) {
|
||||
const store = this.owner.lookup('service:store');
|
||||
const record = store.createRecord('storage-bucket', {
|
||||
@@ -126,6 +159,7 @@ module('Unit | Serializer | storage bucket', function (hooks) {
|
||||
bucket_name: 'bucketname',
|
||||
bucket_prefix: 'bucketprefix',
|
||||
worker_filter: 'workerfilter',
|
||||
plugin: { name: 'aws' },
|
||||
region: 'eu-west-1',
|
||||
access_key_id: 'foobars',
|
||||
secret_access_key: 'testing',
|
||||
@@ -183,6 +217,7 @@ module('Unit | Serializer | storage bucket', function (hooks) {
|
||||
bucket_name: 'bucketname',
|
||||
bucket_prefix: 'bucketprefix',
|
||||
worker_filter: 'workerfilter',
|
||||
plugin: { name: 'aws' },
|
||||
region: 'eu-west-1',
|
||||
disable_credential_rotation: true,
|
||||
access_key_id: 'test',
|
||||
@@ -222,6 +257,61 @@ module('Unit | Serializer | storage bucket', function (hooks) {
|
||||
assert.deepEqual(serializedRecord, expectedResult);
|
||||
});
|
||||
|
||||
test('it serializes when updating a MinIO (static credential by default) storage bucket correctly', async function (assert) {
|
||||
const store = this.owner.lookup('service:store');
|
||||
const serializer = store.serializerFor('storage-bucket');
|
||||
store.push({
|
||||
data: {
|
||||
id: '3',
|
||||
type: 'storage-bucket',
|
||||
attributes: {
|
||||
type: TYPE_STORAGE_BUCKET_PLUGIN,
|
||||
plugin: { name: 'minio' },
|
||||
name: 'minio',
|
||||
description: 'this has a minio plugin',
|
||||
bucket_name: 'bucketname',
|
||||
bucket_prefix: 'bucketprefix',
|
||||
worker_filter: 'workerfilter',
|
||||
endpoint_url: 'http://hashicorp.com',
|
||||
region: 'eu-west-1',
|
||||
disable_credential_rotation: true,
|
||||
access_key_id: 'test',
|
||||
secret_access_key: 'test',
|
||||
version: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
const record = store.peekRecord('storage-bucket', '3');
|
||||
record.description = 'This is new description';
|
||||
record.name = 'This is the new name';
|
||||
record.endpoint_url = 'http://developer.hashicorp.com';
|
||||
const snapshot = record._createSnapshot();
|
||||
const serializedRecord = serializer.serialize(snapshot);
|
||||
|
||||
const expectedResult = {
|
||||
name: 'This is the new name',
|
||||
description: 'This is new description',
|
||||
type: TYPE_STORAGE_BUCKET_PLUGIN,
|
||||
worker_filter: 'workerfilter',
|
||||
attributes: {
|
||||
region: 'eu-west-1',
|
||||
disable_credential_rotation: true,
|
||||
endpoint_url: 'http://developer.hashicorp.com',
|
||||
role_arn: null,
|
||||
role_external_id: null,
|
||||
role_session_name: null,
|
||||
role_tags: null,
|
||||
secrets_hmac: null,
|
||||
},
|
||||
secrets: {
|
||||
access_key_id: 'test',
|
||||
secret_access_key: 'test',
|
||||
},
|
||||
version: 1,
|
||||
};
|
||||
assert.deepEqual(serializedRecord, expectedResult);
|
||||
});
|
||||
|
||||
test('it normalizes correctly', function (assert) {
|
||||
const store = this.owner.lookup('service:store');
|
||||
const serializer = store.serializerFor('storage-bucket');
|
||||
|
||||
@@ -36,6 +36,7 @@ module('Unit | Serializer | target', function (hooks) {
|
||||
address: '0.0.0.0',
|
||||
enable_session_recording: false,
|
||||
storage_bucket_id: null,
|
||||
with_aliases: [{ value: 'a' }],
|
||||
});
|
||||
const snapshot = record._createSnapshot();
|
||||
snapshot.adapterOptions = {};
|
||||
@@ -51,6 +52,7 @@ module('Unit | Serializer | target', function (hooks) {
|
||||
worker_filter: null,
|
||||
egress_worker_filter: null,
|
||||
ingress_worker_filter: null,
|
||||
with_aliases: [{ value: 'a', scope_id: 'global' }],
|
||||
attributes: {
|
||||
default_port: 1234,
|
||||
default_client_port: 4321,
|
||||
@@ -85,6 +87,7 @@ module('Unit | Serializer | target', function (hooks) {
|
||||
enable_session_recording: false,
|
||||
storage_bucket_id: null,
|
||||
address: '0.0.0.0',
|
||||
with_aliases: [{ value: 'www.test.com' }],
|
||||
});
|
||||
const snapshot = record._createSnapshot();
|
||||
snapshot.adapterOptions = {};
|
||||
@@ -100,6 +103,7 @@ module('Unit | Serializer | target', function (hooks) {
|
||||
worker_filter: null,
|
||||
egress_worker_filter: null,
|
||||
ingress_worker_filter: null,
|
||||
with_aliases: [{ value: 'www.test.com', scope_id: 'global' }],
|
||||
attributes: {
|
||||
default_port: 1234,
|
||||
default_client_port: 4321,
|
||||
@@ -189,6 +193,7 @@ module('Unit | Serializer | target', function (hooks) {
|
||||
enable_session_recording: true,
|
||||
storage_bucket_id: 'bucketID',
|
||||
default_port: 1234,
|
||||
with_aliases: [],
|
||||
scope: {
|
||||
scope_id: 'org_1',
|
||||
},
|
||||
@@ -206,6 +211,7 @@ module('Unit | Serializer | target', function (hooks) {
|
||||
worker_filter: null,
|
||||
egress_worker_filter: null,
|
||||
ingress_worker_filter: null,
|
||||
with_aliases: [],
|
||||
attributes: {
|
||||
default_port: 1234,
|
||||
default_client_port: null,
|
||||
@@ -226,6 +232,7 @@ module('Unit | Serializer | target', function (hooks) {
|
||||
type: TYPE_TARGET_SSH,
|
||||
worker_filter: 'worker',
|
||||
default_port: 1234,
|
||||
with_aliases: [],
|
||||
scope: {
|
||||
scope_id: 'org_1',
|
||||
},
|
||||
@@ -243,6 +250,7 @@ module('Unit | Serializer | target', function (hooks) {
|
||||
worker_filter: 'worker',
|
||||
egress_worker_filter: null,
|
||||
ingress_worker_filter: null,
|
||||
with_aliases: [],
|
||||
attributes: {
|
||||
default_port: 1234,
|
||||
default_client_port: null,
|
||||
@@ -263,6 +271,7 @@ module('Unit | Serializer | target', function (hooks) {
|
||||
type: TYPE_TARGET_SSH,
|
||||
egress_worker_filter: 'egress worker',
|
||||
default_port: 1234,
|
||||
with_aliases: [],
|
||||
scope: {
|
||||
scope_id: 'org_1',
|
||||
},
|
||||
@@ -280,6 +289,7 @@ module('Unit | Serializer | target', function (hooks) {
|
||||
worker_filter: null,
|
||||
egress_worker_filter: 'egress worker',
|
||||
ingress_worker_filter: null,
|
||||
with_aliases: [],
|
||||
attributes: {
|
||||
default_port: 1234,
|
||||
default_client_port: null,
|
||||
@@ -300,6 +310,7 @@ module('Unit | Serializer | target', function (hooks) {
|
||||
type: TYPE_TARGET_SSH,
|
||||
ingress_worker_filter: 'ingress worker',
|
||||
default_port: 1234,
|
||||
with_aliases: [],
|
||||
scope: {
|
||||
scope_id: 'org_1',
|
||||
},
|
||||
@@ -317,6 +328,7 @@ module('Unit | Serializer | target', function (hooks) {
|
||||
worker_filter: null,
|
||||
egress_worker_filter: null,
|
||||
ingress_worker_filter: 'ingress worker',
|
||||
with_aliases: [],
|
||||
attributes: {
|
||||
default_port: 1234,
|
||||
default_client_port: null,
|
||||
@@ -352,6 +364,7 @@ module('Unit | Serializer | target', function (hooks) {
|
||||
id: '1',
|
||||
type: 'target',
|
||||
attributes: {
|
||||
aliases: [],
|
||||
authorized_actions: [],
|
||||
name: 'Target 1',
|
||||
host_sources: [
|
||||
@@ -388,6 +401,7 @@ module('Unit | Serializer | target', function (hooks) {
|
||||
id: '1',
|
||||
type: 'target',
|
||||
attributes: {
|
||||
aliases: [],
|
||||
brokered_credential_source_ids: [],
|
||||
injected_application_credential_source_ids: [],
|
||||
authorized_actions: [],
|
||||
@@ -420,6 +434,7 @@ module('Unit | Serializer | target', function (hooks) {
|
||||
id: '1',
|
||||
type: 'target',
|
||||
attributes: {
|
||||
aliases: [],
|
||||
authorized_actions: [],
|
||||
name: 'Target 1',
|
||||
scope: { id: 'o_123', scope_id: 'o_123' },
|
||||
@@ -452,6 +467,7 @@ module('Unit | Serializer | target', function (hooks) {
|
||||
id: '1',
|
||||
type: 'target',
|
||||
attributes: {
|
||||
aliases: [],
|
||||
authorized_actions: [],
|
||||
name: 'Target 1',
|
||||
host_sources: [],
|
||||
@@ -484,6 +500,7 @@ module('Unit | Serializer | target', function (hooks) {
|
||||
id: '1',
|
||||
type: 'target',
|
||||
attributes: {
|
||||
aliases: [],
|
||||
authorized_actions: [],
|
||||
name: 'Target 1',
|
||||
host_sources: [],
|
||||
@@ -516,6 +533,7 @@ module('Unit | Serializer | target', function (hooks) {
|
||||
id: '1',
|
||||
type: 'target',
|
||||
attributes: {
|
||||
aliases: [],
|
||||
authorized_actions: [],
|
||||
name: 'Target 1',
|
||||
host_sources: [],
|
||||
@@ -549,6 +567,7 @@ module('Unit | Serializer | target', function (hooks) {
|
||||
id: '1',
|
||||
type: 'target',
|
||||
attributes: {
|
||||
aliases: [],
|
||||
authorized_actions: [],
|
||||
name: 'Target 1',
|
||||
host_sources: [],
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'dummy/tests/helpers';
|
||||
import Service from '@ember/service';
|
||||
|
||||
class SessionService extends Service {}
|
||||
|
||||
module('Unit | Service | indexed-db', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// TODO: Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
this.owner.register('service:session', SessionService);
|
||||
|
||||
let service = this.owner.lookup('service:indexed-db');
|
||||
assert.ok(service);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
|
||||
module('Unit | Transform | array', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test('it serializes a principals array', function (assert) {
|
||||
const transform = this.owner.lookup('transform:array');
|
||||
const principal = [
|
||||
{ id: 2, type: 'user' },
|
||||
{ id: 3, type: 'group' },
|
||||
{ id: 3, type: 'managed-group' },
|
||||
];
|
||||
const expectedArr = [
|
||||
{ id: 2, type: 'user' },
|
||||
{ id: 3, type: 'group' },
|
||||
{ id: 3, type: 'managed-group' },
|
||||
];
|
||||
const emptyArr = [];
|
||||
assert.deepEqual(transform.serialize(principal), expectedArr);
|
||||
assert.deepEqual(transform.serialize(emptyArr), []);
|
||||
});
|
||||
|
||||
test('it deserializes a principals array', function (assert) {
|
||||
const transform = this.owner.lookup('transform:array');
|
||||
const principal = [
|
||||
{ id: 2, type: 'user' },
|
||||
{ id: 3, type: 'group' },
|
||||
{ id: 3, type: 'managed-group' },
|
||||
];
|
||||
const expectedArr = [
|
||||
{ id: 2, type: 'user' },
|
||||
{ id: 3, type: 'group' },
|
||||
{ id: 3, type: 'managed-group' },
|
||||
];
|
||||
const emptyArr = [];
|
||||
assert.deepEqual(transform.deserialize(principal), expectedArr);
|
||||
assert.deepEqual(transform.deserialize(emptyArr), []);
|
||||
});
|
||||
});
|
||||
@@ -1,17 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
|
||||
module('Unit | Transform | principal array', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let transform = this.owner.lookup('transform:principal-array');
|
||||
assert.ok(transform);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
import { hashCode } from 'api/utils/hash-code';
|
||||
import { module, test } from 'qunit';
|
||||
|
||||
module('Unit | Utility | hash-code', function () {
|
||||
test('it hashes', function (assert) {
|
||||
const result = hashCode({ a: 1, b: 2 });
|
||||
assert.ok(result);
|
||||
});
|
||||
|
||||
test('it hashes to same result regardless of object order', function (assert) {
|
||||
const first = hashCode({ a: 1, b: 2, c: 3 });
|
||||
const second = hashCode({ b: 2, c: 3, a: 1 });
|
||||
assert.strictEqual(first, second);
|
||||
});
|
||||
|
||||
test('it hashes to different result if objects are different', function (assert) {
|
||||
const first = hashCode({ a: 1, b: 2, c: 3 });
|
||||
const second = hashCode({ a: '1', b: '2', c: '3' });
|
||||
assert.notStrictEqual(first, second);
|
||||
});
|
||||
|
||||
test('it hashes an empty object', function (assert) {
|
||||
const result = hashCode({});
|
||||
assert.ok(result);
|
||||
});
|
||||
|
||||
test('it hashes null to 0', function (assert) {
|
||||
const result = hashCode(null);
|
||||
assert.strictEqual(result, 0);
|
||||
});
|
||||
|
||||
test('it hashes undefined to 0', function (assert) {
|
||||
const result = hashCode(undefined);
|
||||
assert.strictEqual(result, 0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,532 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
import { queryIndexedDb } from 'api/utils/indexed-db-query';
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
|
||||
import { setupIndexedDb } from 'api/test-support/helpers/indexed-db';
|
||||
import { pluralize } from 'ember-inflector';
|
||||
import { camelize } from '@ember/string';
|
||||
import { TYPE_TARGET_TCP, TYPE_TARGET_SSH } from 'api/models/target';
|
||||
|
||||
const seedIndexDb = async (resource, store, indexedDb, server) => {
|
||||
const resourceData =
|
||||
server.schema[pluralize(camelize(resource))].all().models;
|
||||
const serializedData = resourceData.map((resource) =>
|
||||
JSON.parse(JSON.stringify(server.serializerOrRegistry.serialize(resource))),
|
||||
);
|
||||
const serializer = store.serializerFor(resource);
|
||||
const schema = store.modelFor(resource);
|
||||
const normalizedPayload = serializer.normalizeResponse(
|
||||
store,
|
||||
schema,
|
||||
{ items: serializedData },
|
||||
null,
|
||||
'query',
|
||||
);
|
||||
|
||||
await indexedDb.db[resource].bulkPut(
|
||||
normalizedPayload.data.map((datum) =>
|
||||
indexedDb.normalizeData({
|
||||
data: datum,
|
||||
cleanData: true,
|
||||
schema,
|
||||
serializer,
|
||||
}),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
module('Unit | Utility | indexed-db-query', function (hooks) {
|
||||
setupTest(hooks);
|
||||
setupMirage(hooks);
|
||||
setupIndexedDb(hooks);
|
||||
|
||||
let indexedDb;
|
||||
let store;
|
||||
|
||||
let scope1;
|
||||
let scope2;
|
||||
const resource = 'target';
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
indexedDb = this.owner.lookup('service:indexed-db');
|
||||
store = this.owner.lookup('service:store');
|
||||
scope1 = this.server.create('scope', {
|
||||
id: 'p_123',
|
||||
type: 'project',
|
||||
});
|
||||
scope2 = this.server.create('scope', {
|
||||
id: 'p_456',
|
||||
type: 'project',
|
||||
});
|
||||
this.server.createList(resource, 5, {
|
||||
type: TYPE_TARGET_SSH,
|
||||
scopeId: scope1.id,
|
||||
});
|
||||
this.server.createList(resource, 5, {
|
||||
type: TYPE_TARGET_TCP,
|
||||
scopeId: scope2.id,
|
||||
});
|
||||
});
|
||||
|
||||
test('it filters by single field', async function (assert) {
|
||||
await seedIndexDb(resource, store, indexedDb, this.server);
|
||||
const query = {
|
||||
filters: {
|
||||
type: [{ equals: TYPE_TARGET_SSH }],
|
||||
},
|
||||
};
|
||||
|
||||
const result = await queryIndexedDb(indexedDb.db, resource, query);
|
||||
|
||||
assert.strictEqual(result.length, 5);
|
||||
});
|
||||
|
||||
test('it filters by single field with "and" logicalOperator', async function (assert) {
|
||||
await seedIndexDb(resource, store, indexedDb, this.server);
|
||||
const query = {
|
||||
filters: {
|
||||
type: {
|
||||
logicalOperator: 'and',
|
||||
values: [{ equals: TYPE_TARGET_SSH }],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = await queryIndexedDb(indexedDb.db, resource, query);
|
||||
|
||||
assert.strictEqual(result.length, 5);
|
||||
});
|
||||
|
||||
test('it filters by single field with "or" logicalOperator', async function (assert) {
|
||||
await seedIndexDb(resource, store, indexedDb, this.server);
|
||||
const query = {
|
||||
filters: {
|
||||
type: {
|
||||
logicalOperator: 'or',
|
||||
values: [{ equals: TYPE_TARGET_SSH }, { equals: TYPE_TARGET_TCP }],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = await queryIndexedDb(indexedDb.db, resource, query);
|
||||
|
||||
assert.strictEqual(result.length, 10);
|
||||
});
|
||||
|
||||
test('it filters by multiple fields', async function (assert) {
|
||||
await seedIndexDb(resource, store, indexedDb, this.server);
|
||||
const query = {
|
||||
filters: {
|
||||
type: [{ equals: TYPE_TARGET_SSH }],
|
||||
scope_id: [{ equals: scope1.id }],
|
||||
},
|
||||
};
|
||||
|
||||
const result = await queryIndexedDb(indexedDb.db, resource, query);
|
||||
|
||||
assert.strictEqual(result.length, 5);
|
||||
});
|
||||
|
||||
test('it filters by multiple fields with "or" logicalOperator', async function (assert) {
|
||||
this.server.create(resource, { type: TYPE_TARGET_TCP, scopeId: scope1.id });
|
||||
await seedIndexDb(resource, store, indexedDb, this.server);
|
||||
const query = {
|
||||
filters: {
|
||||
type: {
|
||||
logicalOperator: 'or',
|
||||
values: [{ equals: TYPE_TARGET_SSH }, { equals: TYPE_TARGET_TCP }],
|
||||
},
|
||||
scope_id: [{ equals: scope1.id }],
|
||||
},
|
||||
};
|
||||
|
||||
const result = await queryIndexedDb(indexedDb.db, resource, query);
|
||||
assert.strictEqual(result.length, 6);
|
||||
});
|
||||
|
||||
test('it filters by multiple fields with "and" logicalOperator', async function (assert) {
|
||||
this.server.create(resource, { type: TYPE_TARGET_TCP, scopeId: scope1.id });
|
||||
await seedIndexDb(resource, store, indexedDb, this.server);
|
||||
const query = {
|
||||
filters: {
|
||||
type: {
|
||||
logicalOperator: 'and',
|
||||
values: [{ equals: TYPE_TARGET_SSH }, { notEquals: TYPE_TARGET_TCP }],
|
||||
},
|
||||
scope_id: [{ equals: scope1.id }],
|
||||
},
|
||||
};
|
||||
|
||||
const result = await queryIndexedDb(indexedDb.db, resource, query);
|
||||
assert.strictEqual(result.length, 5);
|
||||
});
|
||||
|
||||
test('it filters by multiple fields and uses two "or" logicalOperators', async function (assert) {
|
||||
await seedIndexDb(resource, store, indexedDb, this.server);
|
||||
const query = {
|
||||
filters: {
|
||||
type: {
|
||||
logicalOperator: 'or',
|
||||
values: [{ equals: TYPE_TARGET_SSH }, { equals: TYPE_TARGET_TCP }],
|
||||
},
|
||||
scope_id: {
|
||||
logicalOperator: 'or',
|
||||
values: [{ equals: scope1.id }, { equals: scope2.id }],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = await queryIndexedDb(indexedDb.db, resource, query);
|
||||
assert.strictEqual(result.length, 10);
|
||||
});
|
||||
|
||||
test('it filters by multiple fields and uses two "and" logicalOperators', async function (assert) {
|
||||
await seedIndexDb(resource, store, indexedDb, this.server);
|
||||
const query = {
|
||||
filters: {
|
||||
type: {
|
||||
logicalOperator: 'and',
|
||||
values: [{ notEquals: TYPE_TARGET_SSH }, { equals: TYPE_TARGET_TCP }],
|
||||
},
|
||||
scope_id: {
|
||||
logicalOperator: 'and',
|
||||
values: [{ equals: scope2.id }, { notEquals: scope1.id }],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = await queryIndexedDb(indexedDb.db, resource, query);
|
||||
assert.strictEqual(result.length, 5);
|
||||
});
|
||||
|
||||
test('it cannot filter with "contains" as initial operation', async function (assert) {
|
||||
assert.expect(1);
|
||||
await seedIndexDb(resource, store, indexedDb, this.server);
|
||||
const query = {
|
||||
filters: {
|
||||
scope_id: [{ contains: scope1.id }],
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
await queryIndexedDb(indexedDb.db, resource, query);
|
||||
} catch (e) {
|
||||
assert.strictEqual(
|
||||
e.message,
|
||||
'Contains is not supported as the first filter.',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('it cannot filter with "contains" as subsequent operation', async function (assert) {
|
||||
assert.expect(2);
|
||||
await seedIndexDb(resource, store, indexedDb, this.server);
|
||||
const query = {
|
||||
filters: {
|
||||
scope_id: {
|
||||
logicalOperator: 'and',
|
||||
values: [{ equals: scope1.id }, { contains: scope2.id.slice(0, 2) }],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = await queryIndexedDb(indexedDb.db, resource, query);
|
||||
|
||||
assert.strictEqual(result.length, 5);
|
||||
|
||||
query.filters.scope_id.logicalOperator = 'or';
|
||||
|
||||
try {
|
||||
await queryIndexedDb(indexedDb.db, resource, query);
|
||||
} catch (e) {
|
||||
assert.strictEqual(
|
||||
e.message,
|
||||
'Contains is not supported as a filter option with "or" operator.',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: Modify test cases that use comparison opertators to compare fields that
|
||||
// are of type number. Additionally, add test case for searching on boolean fields.
|
||||
test('it filters by single field with "gt" as initial operation', async function (assert) {
|
||||
await seedIndexDb(resource, store, indexedDb, this.server);
|
||||
const query = {
|
||||
filters: {
|
||||
scope_id: [{ gt: scope1.id }],
|
||||
},
|
||||
};
|
||||
|
||||
const result = await queryIndexedDb(indexedDb.db, resource, query);
|
||||
|
||||
assert.strictEqual(result.length, 5);
|
||||
});
|
||||
|
||||
test('it filters by single field with "gt" as subsequent operation', async function (assert) {
|
||||
await seedIndexDb(resource, store, indexedDb, this.server);
|
||||
const query = {
|
||||
filters: {
|
||||
scope_id: {
|
||||
logicalOperator: 'and',
|
||||
values: [{ equals: scope2.id }, { gt: scope1.id }],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let result = await queryIndexedDb(indexedDb.db, resource, query);
|
||||
|
||||
assert.strictEqual(result.length, 5);
|
||||
|
||||
query.filters.scope_id.logicalOperator = 'or';
|
||||
|
||||
result = await queryIndexedDb(indexedDb.db, resource, query);
|
||||
|
||||
assert.strictEqual(result.length, 5);
|
||||
});
|
||||
|
||||
test('it filters by single field with "gte" as initial operation', async function (assert) {
|
||||
await seedIndexDb(resource, store, indexedDb, this.server);
|
||||
const query = {
|
||||
filters: {
|
||||
scope_id: [{ gte: scope1.id }],
|
||||
},
|
||||
};
|
||||
|
||||
const result = await queryIndexedDb(indexedDb.db, resource, query);
|
||||
|
||||
assert.strictEqual(result.length, 10);
|
||||
});
|
||||
|
||||
test('it filters by single field with "gte" as subsequent operation', async function (assert) {
|
||||
await seedIndexDb(resource, store, indexedDb, this.server);
|
||||
const query = {
|
||||
filters: {
|
||||
scope_id: {
|
||||
logicalOperator: 'and',
|
||||
values: [{ equals: scope2.id }, { gte: scope1.id }],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let result = await queryIndexedDb(indexedDb.db, resource, query);
|
||||
|
||||
assert.strictEqual(result.length, 5);
|
||||
|
||||
query.filters.scope_id.logicalOperator = 'or';
|
||||
|
||||
result = await queryIndexedDb(indexedDb.db, resource, query);
|
||||
|
||||
assert.strictEqual(result.length, 10);
|
||||
});
|
||||
|
||||
test('it filters by single field with "lt" as initial operation', async function (assert) {
|
||||
await seedIndexDb(resource, store, indexedDb, this.server);
|
||||
const query = {
|
||||
filters: {
|
||||
scope_id: [{ lt: scope2.id }],
|
||||
},
|
||||
};
|
||||
|
||||
const result = await queryIndexedDb(indexedDb.db, resource, query);
|
||||
|
||||
assert.strictEqual(result.length, 5);
|
||||
});
|
||||
|
||||
test('it filters by single field with "lt" as subsequent operation', async function (assert) {
|
||||
await seedIndexDb(resource, store, indexedDb, this.server);
|
||||
const query = {
|
||||
filters: {
|
||||
scope_id: {
|
||||
logicalOperator: 'and',
|
||||
values: [{ equals: scope1.id }, { lt: scope2.id }],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let result = await queryIndexedDb(indexedDb.db, resource, query);
|
||||
|
||||
assert.strictEqual(result.length, 5);
|
||||
|
||||
query.filters.scope_id.logicalOperator = 'or';
|
||||
|
||||
result = await queryIndexedDb(indexedDb.db, resource, query);
|
||||
|
||||
assert.strictEqual(result.length, 5);
|
||||
});
|
||||
|
||||
test('it filters by single field with "lte" as initial operation', async function (assert) {
|
||||
await seedIndexDb(resource, store, indexedDb, this.server);
|
||||
const query = {
|
||||
filters: {
|
||||
scope_id: [{ lte: scope2.id }],
|
||||
},
|
||||
};
|
||||
|
||||
const result = await queryIndexedDb(indexedDb.db, resource, query);
|
||||
|
||||
assert.strictEqual(result.length, 10);
|
||||
});
|
||||
|
||||
test('it filters by single field with "lte" as subsequent operation', async function (assert) {
|
||||
await seedIndexDb(resource, store, indexedDb, this.server);
|
||||
const query = {
|
||||
filters: {
|
||||
scope_id: {
|
||||
logicalOperator: 'and',
|
||||
values: [{ equals: scope1.id }, { lte: scope2.id }],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let result = await queryIndexedDb(indexedDb.db, resource, query);
|
||||
|
||||
assert.strictEqual(result.length, 5);
|
||||
|
||||
query.filters.scope_id.logicalOperator = 'or';
|
||||
|
||||
result = await queryIndexedDb(indexedDb.db, resource, query);
|
||||
|
||||
assert.strictEqual(result.length, 10);
|
||||
});
|
||||
|
||||
test('it filters by single field with "notEquals" as initial operation', async function (assert) {
|
||||
await seedIndexDb(resource, store, indexedDb, this.server);
|
||||
const query = {
|
||||
filters: {
|
||||
scope_id: [{ notEquals: scope1.id }],
|
||||
},
|
||||
};
|
||||
|
||||
const result = await queryIndexedDb(indexedDb.db, resource, query);
|
||||
|
||||
assert.strictEqual(result.length, 5);
|
||||
});
|
||||
|
||||
test('it filters by single field with "notEquals" as subsequent operation', async function (assert) {
|
||||
await seedIndexDb(resource, store, indexedDb, this.server);
|
||||
const query = {
|
||||
filters: {
|
||||
scope_id: {
|
||||
logicalOperator: 'and',
|
||||
values: [{ equals: scope2.id }, { notEquals: scope1.id }],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let result = await queryIndexedDb(indexedDb.db, resource, query);
|
||||
|
||||
assert.strictEqual(result.length, 5);
|
||||
|
||||
query.filters.scope_id.logicalOperator = 'or';
|
||||
|
||||
result = await queryIndexedDb(indexedDb.db, resource, query);
|
||||
|
||||
assert.strictEqual(result.length, 5);
|
||||
});
|
||||
|
||||
test('it filters and returns zero results', async function (assert) {
|
||||
await seedIndexDb(resource, store, indexedDb, this.server);
|
||||
const query = {
|
||||
filters: {
|
||||
type: [{ equals: 'unknown' }],
|
||||
},
|
||||
};
|
||||
|
||||
const result = await queryIndexedDb(indexedDb.db, resource, query);
|
||||
|
||||
assert.strictEqual(result.length, 0);
|
||||
});
|
||||
|
||||
test('it searches with a query that matches a field exactly', async function (assert) {
|
||||
await seedIndexDb(resource, store, indexedDb, this.server);
|
||||
const query = { search: scope1.id };
|
||||
|
||||
const result = await queryIndexedDb(indexedDb.db, resource, query);
|
||||
|
||||
assert.strictEqual(result.length, 5);
|
||||
});
|
||||
|
||||
test('it searches with a query that matches a field partially', async function (assert) {
|
||||
await seedIndexDb(resource, store, indexedDb, this.server);
|
||||
const query = { search: scope2.id.slice(0, 2) };
|
||||
|
||||
const result = await queryIndexedDb(indexedDb.db, resource, query);
|
||||
|
||||
assert.strictEqual(result.length, 10);
|
||||
});
|
||||
|
||||
test('it searches and returns no results with a query that matches no fields', async function (assert) {
|
||||
await seedIndexDb(resource, store, indexedDb, this.server);
|
||||
const query = { search: `${scope1.id}456789` };
|
||||
|
||||
const result = await queryIndexedDb(indexedDb.db, resource, query);
|
||||
|
||||
assert.strictEqual(result.length, 0);
|
||||
});
|
||||
|
||||
test('it filters by a single field and searches', async function (assert) {
|
||||
await seedIndexDb(resource, store, indexedDb, this.server);
|
||||
const query = {
|
||||
search: scope1.id,
|
||||
filters: {
|
||||
type: [{ equals: TYPE_TARGET_SSH }],
|
||||
},
|
||||
};
|
||||
|
||||
const result = await queryIndexedDb(indexedDb.db, resource, query);
|
||||
|
||||
assert.strictEqual(result.length, 5);
|
||||
});
|
||||
|
||||
test('it filters by multiple fields and searches', async function (assert) {
|
||||
const target = this.server.create(resource, {
|
||||
name: 'Generated_ssh_target',
|
||||
type: TYPE_TARGET_SSH,
|
||||
scopeId: scope1.id,
|
||||
});
|
||||
await seedIndexDb(resource, store, indexedDb, this.server);
|
||||
const query = {
|
||||
search: target.name,
|
||||
filters: {
|
||||
type: [{ equals: TYPE_TARGET_SSH }],
|
||||
scope_id: [{ equals: scope1.id }],
|
||||
},
|
||||
};
|
||||
|
||||
const result = await queryIndexedDb(indexedDb.db, resource, query);
|
||||
|
||||
assert.strictEqual(result.length, 1);
|
||||
});
|
||||
|
||||
test('it filters on boolean type fields with truthy value', async function (assert) {
|
||||
const authMethod = this.server.create('auth-method', {
|
||||
name: 'Generated_auth_method',
|
||||
scopeId: scope1.id,
|
||||
});
|
||||
scope1.update({ primaryAuthMethodId: authMethod.id });
|
||||
await seedIndexDb('auth-method', store, indexedDb, this.server);
|
||||
const query = { filters: { is_primary: [{ equals: 'true' }] } };
|
||||
|
||||
const result = await queryIndexedDb(indexedDb.db, 'auth-method', query);
|
||||
|
||||
assert.strictEqual(result.length, 1);
|
||||
});
|
||||
|
||||
test('it filters on boolean type fields with falsy value', async function (assert) {
|
||||
const authMethod = this.server.create('auth-method', {
|
||||
name: 'Generated_auth_method',
|
||||
scopeId: scope1.id,
|
||||
});
|
||||
scope1.update({ primaryAuthMethodId: authMethod.id });
|
||||
await seedIndexDb('auth-method', store, indexedDb, this.server);
|
||||
const query = { filters: { is_primary: [{ equals: 'false' }] } };
|
||||
|
||||
const result = await queryIndexedDb(indexedDb.db, 'auth-method', query);
|
||||
|
||||
assert.strictEqual(result.length, 0);
|
||||
});
|
||||
});
|
||||
@@ -4,6 +4,7 @@
|
||||
}}
|
||||
|
||||
<Hds::Dropdown
|
||||
...attributes
|
||||
@height='314px'
|
||||
@onClose={{this.close}}
|
||||
@listPosition={{@listPosition}}
|
||||
@@ -11,7 +12,7 @@
|
||||
>
|
||||
<DD.ToggleButton
|
||||
@color='secondary'
|
||||
@text={{@name}}
|
||||
@text={{@toggleText}}
|
||||
@count={{@checkedItems.length}}
|
||||
{{on 'click' this.open}}
|
||||
/>
|
||||
|
||||
@@ -23,17 +23,19 @@ module('Integration | Component | dropdown/index', function (hooks) {
|
||||
name: 'Fake Project',
|
||||
displayName: 'Fake Project',
|
||||
};
|
||||
const name = 'Click Me';
|
||||
const name = 'test';
|
||||
const toggleText = 'Click Me';
|
||||
|
||||
const FIRST_ITEM_SELECTOR = `[data-test-checkbox="${firstItem.id}"]`;
|
||||
const SECOND_ITEM_SELECTOR = `[data-test-checkbox="${secondItem.id}"]`;
|
||||
const TOGGLE_DROPDOWN_SELECTOR = '.hds-dropdown-toggle-button';
|
||||
const TOGGLE_DROPDOWN_SELECTOR = `[name=${name}] button`;
|
||||
const SEARCH_INPUT_SELECTOR = '[data-test-dropdown-search]';
|
||||
const COUNT_SELECTOR = '.hds-dropdown-toggle-button__count';
|
||||
const ITEM_SELECTOR = '.hds-dropdown-list-item';
|
||||
const COUNT_SELECTOR = `[name=${name}] button div:nth-child(2)`;
|
||||
const ITEM_SELECTOR = 'li';
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
this.set('name', name);
|
||||
this.set('toggleText', toggleText);
|
||||
this.set('array', [firstItem, secondItem]);
|
||||
this.set('checkedItems', [firstItem.id]);
|
||||
this.set('applyFilter', () => {});
|
||||
@@ -41,14 +43,15 @@ module('Integration | Component | dropdown/index', function (hooks) {
|
||||
|
||||
test('it renders', async function (assert) {
|
||||
await render(hbs`<Dropdown
|
||||
@name={{this.name}}
|
||||
name={{this.name}}
|
||||
@toggleText={{this.toggleText}}
|
||||
@itemOptions={{this.array}}
|
||||
@checkedItems={{this.checkedItems}}
|
||||
@applyFilter={{this.applyFilter}}
|
||||
/>`);
|
||||
await click(TOGGLE_DROPDOWN_SELECTOR);
|
||||
|
||||
assert.dom('.hds-dropdown-toggle-button__text').hasText(name);
|
||||
assert.dom(TOGGLE_DROPDOWN_SELECTOR).includesText(toggleText);
|
||||
assert.dom(FIRST_ITEM_SELECTOR).isVisible();
|
||||
assert.dom(FIRST_ITEM_SELECTOR).isChecked();
|
||||
assert.dom(SECOND_ITEM_SELECTOR).isVisible();
|
||||
@@ -58,7 +61,8 @@ module('Integration | Component | dropdown/index', function (hooks) {
|
||||
|
||||
test('it renders a search input when isSearchable is true', async function (assert) {
|
||||
await render(hbs`<Dropdown
|
||||
@name={{this.name}}
|
||||
name={{this.name}}
|
||||
@toggleText={{this.toggleText}}
|
||||
@itemOptions={{this.array}}
|
||||
@checkedItems={{this.checkedItems}}
|
||||
@applyFilter={{this.applyFilter}}
|
||||
@@ -71,7 +75,8 @@ module('Integration | Component | dropdown/index', function (hooks) {
|
||||
|
||||
test('it renders the correct count of checked items', async function (assert) {
|
||||
await render(hbs`<Dropdown
|
||||
@name={{this.name}}
|
||||
name={{this.name}}
|
||||
@toggleText={{this.toggleText}}
|
||||
@itemOptions={{this.array}}
|
||||
@checkedItems={{this.checkedItems}}
|
||||
@applyFilter={{this.applyFilter}}
|
||||
@@ -87,7 +92,8 @@ module('Integration | Component | dropdown/index', function (hooks) {
|
||||
});
|
||||
|
||||
await render(hbs`<Dropdown
|
||||
@name={{this.name}}
|
||||
name={{this.name}}
|
||||
@toggleText={{this.toggleText}}
|
||||
@itemOptions={{this.array}}
|
||||
@checkedItems={{this.checkedItems}}
|
||||
@applyFilter={{this.applyFilter}}
|
||||
@@ -102,7 +108,8 @@ module('Integration | Component | dropdown/index', function (hooks) {
|
||||
this.set('checkedItems', []);
|
||||
|
||||
await render(hbs`<Dropdown
|
||||
@name={{this.name}}
|
||||
name={{this.name}}
|
||||
@toggleText={{this.toggleText}}
|
||||
@itemOptions={{this.array}}
|
||||
@checkedItems={{this.checkedItems}}
|
||||
@applyFilter={{this.applyFilter}}
|
||||
@@ -128,7 +135,8 @@ module('Integration | Component | dropdown/index', function (hooks) {
|
||||
);
|
||||
|
||||
await render(hbs`<Dropdown
|
||||
@name={{this.name}}
|
||||
name={{this.name}}
|
||||
@toggleText={{this.toggleText}}
|
||||
@itemOptions={{this.itemOptions}}
|
||||
@checkedItems={{this.checkedItems}}
|
||||
@applyFilter={{this.applyFilter}}
|
||||
|
||||
@@ -44,6 +44,7 @@ toggle-secret: Toggle secret visibility
|
||||
minimize: Minimize
|
||||
fullscreen: Fullscreen
|
||||
clear-all-filters: Clear all filters
|
||||
clear: Clear
|
||||
clear-filters: Clear filters
|
||||
learn-more: Learn more
|
||||
read-more: Read more
|
||||
|
||||
@@ -41,6 +41,7 @@ titles:
|
||||
no-results-found: No results found
|
||||
filters-applied: 'Filters Applied:'
|
||||
unit: Unit
|
||||
not-set: Not set
|
||||
year: '{numberOfYears, plural, =1 {# year} other {# years}}'
|
||||
days: '{days, plural, =1 {# day} other {# days}}'
|
||||
descriptions:
|
||||
@@ -59,6 +60,7 @@ questions:
|
||||
remove-confirm: Are you sure you want to remove this association?
|
||||
abandon-confirm: Are you sure you want to navigate away? Your unsaved changes will be lost.
|
||||
retry-confirm: Would you like to retry?
|
||||
clear-confirm: Are you sure you want to clear the resource id?
|
||||
themes:
|
||||
system: System Default
|
||||
light: Light
|
||||
|
||||
@@ -168,7 +168,7 @@ ip_addresses:
|
||||
help: Read-only public and private IP addresses given by the plugin host catalog provider
|
||||
sync-interval:
|
||||
label: Sync Interval
|
||||
help: Optional. Number of seconds between the time Boundary syncs hosts in this set.
|
||||
help: Number of seconds between the time Boundary syncs hosts in this set.
|
||||
tags:
|
||||
label: Tags
|
||||
secret-editor:
|
||||
|
||||
@@ -7,3 +7,4 @@ delete-success: Deleted successfully.
|
||||
add-success: Added successfully.
|
||||
remove-success: Removed successfully.
|
||||
canceled-success: Canceled successfully.
|
||||
clear-success: Cleared successfully
|
||||
@@ -62,6 +62,7 @@ auth-method:
|
||||
searching: Searching
|
||||
finding: Finding
|
||||
always: Always
|
||||
primary: Primary
|
||||
types:
|
||||
password: Password
|
||||
oidc: OIDC
|
||||
@@ -83,7 +84,7 @@ auth-method:
|
||||
form:
|
||||
urls:
|
||||
label: Server address (URL and port)
|
||||
help: 'The domain name or IP address of the LDAP server. (Example: ldap.example.com:389)'
|
||||
help: 'The domain name or IP address of the LDAP server. (Example: ldaps://ldap.example.com:636)'
|
||||
certificates:
|
||||
label: Certificates
|
||||
help: PEM encoded x509 certificates in ASN.1 DER form that can be used as trust anchors when connecting to an LDAP provider.
|
||||
@@ -418,11 +419,12 @@ storage-bucket:
|
||||
messages:
|
||||
none:
|
||||
title: No storage buckets yet
|
||||
description: To start using session recording, you need to have an Amazon S3 bucket.
|
||||
description: To start using session recording, you need to have a storage bucket.
|
||||
link: Add a new storage bucket
|
||||
titles:
|
||||
new: New Storage Bucket
|
||||
amazon_s3: Amazon S3
|
||||
minio: MinIO
|
||||
actions:
|
||||
delete: Delete Storage Bucket
|
||||
types:
|
||||
@@ -434,6 +436,9 @@ storage-bucket:
|
||||
dynamic:
|
||||
title: Dynamic
|
||||
description: Specify an Assume Role used to generate credentials to use with this AWS bucket
|
||||
plugin-types:
|
||||
aws: Amazon S3
|
||||
minio: MinIO
|
||||
form:
|
||||
scope:
|
||||
label: Scope
|
||||
@@ -443,33 +448,36 @@ storage-bucket:
|
||||
provider:
|
||||
label: Provider
|
||||
help: Boundary currently only supports Amazon S3 storage.
|
||||
endpoint_url:
|
||||
label: Endpoint URL
|
||||
help: Cannot be changed later.
|
||||
bucket_name:
|
||||
label: Bucket name
|
||||
help: The name of this bucket in AWS
|
||||
help: The name of this storage bucket.
|
||||
bucket_prefix:
|
||||
label: Bucket prefix
|
||||
help: The base path where session recordings will be stored.
|
||||
role_arn:
|
||||
label: Role ARN
|
||||
help: The ARN (Assume Role Name) used by this instance that the worker runs on
|
||||
help: The ARN (Assume Role Name) used by this instance that the worker runs on.
|
||||
role_external_id:
|
||||
label: Role external ID
|
||||
help: Used to provide access to this account if you have this configured
|
||||
help: Used to provide access to this account if you have this configured.
|
||||
role_tags:
|
||||
label: Role tags
|
||||
help: Attributes to pass to AWS
|
||||
help: Attributes to pass to AWS.
|
||||
role_session_name:
|
||||
label: Role session name
|
||||
help: Unique identifier for the AWS session to help identify it in your logs
|
||||
help: Unique identifier for the AWS session to help identify it in your logs.
|
||||
region:
|
||||
label: Region
|
||||
help: The AWS region to use
|
||||
help: The region that this storage bucket is in.
|
||||
access_key_id:
|
||||
label: Access key ID
|
||||
help: The Access key ID generated by AWS for the IAM User to use with this storage bucket. We will not show this data after it is saved, but you can replace it.
|
||||
help: The Access key ID generated by your storage provider for the IAM User to use with this storage bucket. We will not show this data after it is saved, but you can replace it.
|
||||
secret_access_key:
|
||||
label: Secret access key
|
||||
help: The secret access key generated by AWS for the IAM user to use with this storage bucket. We will not show this data after it is saved, but you can replace it.
|
||||
help: The secret access key generated by your storage provider for the IAM user to use with this storage bucket. We will not show this data after it is saved, but you can replace it.
|
||||
worker_filter:
|
||||
label: Worker filter
|
||||
help: Filter for Boundary workers that have network access to your storage. Boolean expression.
|
||||
@@ -576,6 +584,7 @@ target:
|
||||
new: New Target
|
||||
active-sessions: Active sessions
|
||||
sessions-flyout: Active sessions for {targetDisplayName}
|
||||
aliases-flyout: Aliases for {targetDisplayName}
|
||||
actions:
|
||||
create: New Target
|
||||
delete: Delete Target
|
||||
@@ -586,6 +595,8 @@ target:
|
||||
enable-session-recording: Enable Session Recording
|
||||
enable-recording: Enable recording
|
||||
view-more-sessions: View more sessions
|
||||
add-an-alias: Add an alias
|
||||
view-more-aliases: View more aliases
|
||||
types:
|
||||
tcp: Generic TCP
|
||||
ssh: SSH
|
||||
@@ -646,8 +657,8 @@ target:
|
||||
filter:
|
||||
label: Record sessions for this target
|
||||
select:
|
||||
label: AWS storage buckets
|
||||
description: Sessions will be recorded and stored in this location. Must use Amazon S3.
|
||||
label: Storage buckets
|
||||
description: Sessions will be recorded and stored in this location.
|
||||
brokered-credential-source:
|
||||
title: Brokered Credential
|
||||
title_plural: Brokered Credentials
|
||||
@@ -1002,5 +1013,35 @@ policy:
|
||||
alias:
|
||||
title: Alias
|
||||
title_plural: Aliases
|
||||
description: An Alias is a DNS-like string associated with a destination resource.
|
||||
details: Alias Details
|
||||
titles:
|
||||
new: New Alias
|
||||
resource_id: Resource ID
|
||||
points_to: Points to
|
||||
messages:
|
||||
none:
|
||||
title: Welcome to Aliases
|
||||
description: No Aliases yet
|
||||
link: Create a new alias
|
||||
delete: Deleting this alias may break any workflows using it. You can also clear the alias of the resource it points to without deleting it and then edit it to point it to a new resource.
|
||||
form:
|
||||
options:
|
||||
label: Alias options
|
||||
type:
|
||||
label: Target
|
||||
help: Servers, API Endpoints, and network devices
|
||||
destination_id:
|
||||
label: Target ID
|
||||
help: Specify any target across your deployment to associate this alias with
|
||||
value:
|
||||
label: Alias Value
|
||||
help: The alias you would like to use for this resource
|
||||
authorize_session_arguments:
|
||||
label: Host ID
|
||||
help: Always use this host when connecting using this alias
|
||||
sidebar:
|
||||
help: Use a domain name as a shortcut for anyone with access to this target.
|
||||
alias:
|
||||
label: Aliases
|
||||
help: You can create aliases for this target that will make it easier to reach. Users must have access to this project to use aliases.
|
||||
@@ -17,3 +17,4 @@ failed: Failed
|
||||
loading:
|
||||
title: Loading all items...
|
||||
description: All of your {resource} are still loading. Please wait while we finish loading everything on screen.
|
||||
generic-description: All of your resources are still loading. Please wait while we finish loading everything on screen.
|
||||
@@ -1,37 +0,0 @@
|
||||
{{!
|
||||
Copyright (c) HashiCorp, Inc.
|
||||
SPDX-License-Identifier: MPL-2.0
|
||||
}}
|
||||
|
||||
<article ...attributes class='rose-card-link'>
|
||||
<LinkTo
|
||||
title={{@title}}
|
||||
@route={{if @route @route 'index'}}
|
||||
@model={{@model}}
|
||||
>
|
||||
<div class='rose-card-link-header'>
|
||||
{{#if @title}}
|
||||
<h2 class='rose-card-link-title'>
|
||||
{{#if @icon}}
|
||||
<Rose::Icon @name={{@icon}} @size='medium' />
|
||||
{{/if}}
|
||||
<span class='rose-card-link-title-text'>{{@title}}</span>
|
||||
</h2>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<div class='rose-card-link-body'>
|
||||
{{#if @description}}
|
||||
<p class='rose-card-link-description'>{{@description}}</p>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<div class='rose-card-link-footer'>
|
||||
{{#if @id}}
|
||||
<p class='rose-card-link-footer-id'>{{@id}}</p>
|
||||
{{/if}}
|
||||
</div>
|
||||
</LinkTo>
|
||||
|
||||
{{yield}}
|
||||
</article>
|
||||
@@ -1,8 +0,0 @@
|
||||
{{!
|
||||
Copyright (c) HashiCorp, Inc.
|
||||
SPDX-License-Identifier: MPL-2.0
|
||||
}}
|
||||
|
||||
<div ...attributes class='rose-cards'>
|
||||
{{yield (hash link=(component 'rose/card/link'))}}
|
||||
</div>
|
||||
@@ -31,6 +31,13 @@
|
||||
color: var(--token-form-indicator-optional-color);
|
||||
}
|
||||
|
||||
// Override for sortable HDS table columns that ignore selected theme in app
|
||||
// and use the system default theme causing color mismatch if selected theme
|
||||
// is not the system default theme
|
||||
.hds-table__th-sort button {
|
||||
color: var(--token-color-foreground-strong);
|
||||
}
|
||||
|
||||
// TODO: This is a temporary hack to fix an issue causing weird whitespace
|
||||
// and scrollbars from an accessible class in HDS. We have to use !important
|
||||
// as they already forced it with !important and we can't override it otherwise
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { render, find } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
|
||||
module('Integration | Component | rose/card/link', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
test('it renders', async function (assert) {
|
||||
await render(hbs`<Rose::Card::Link />`);
|
||||
assert.ok(find('article'));
|
||||
assert.ok(find('.rose-card-link'));
|
||||
assert.ok(find('.rose-card-link-header'));
|
||||
assert.ok(find('.rose-card-link-body'));
|
||||
assert.ok(find('.rose-card-link-footer'));
|
||||
});
|
||||
|
||||
test('it renders with @title', async function (assert) {
|
||||
await render(hbs`<Rose::Card::Link @title="card title" />`);
|
||||
assert.strictEqual(find('a').title, 'card title');
|
||||
assert.strictEqual(
|
||||
find('.rose-card-link-title').textContent.trim(),
|
||||
'card title',
|
||||
);
|
||||
});
|
||||
|
||||
test('it renders with @description', async function (assert) {
|
||||
await render(hbs`<Rose::Card::Link @description="card description" />`);
|
||||
assert.strictEqual(
|
||||
find('.rose-card-link-body').textContent.trim(),
|
||||
'card description',
|
||||
);
|
||||
});
|
||||
|
||||
test('it renders with @id', async function (assert) {
|
||||
await render(hbs`<Rose::Card::Link @id="card_id" />`);
|
||||
assert.strictEqual(
|
||||
find('.rose-card-link-footer-id').textContent.trim(),
|
||||
'card_id',
|
||||
);
|
||||
});
|
||||
|
||||
test('it renders with @icon', async function (assert) {
|
||||
await render(
|
||||
hbs`<Rose::Card::Link @title="foo" @icon="flight-icons/svg/alert-circle-16" />`,
|
||||
);
|
||||
assert.ok(find('.rose-icon'));
|
||||
});
|
||||
|
||||
test('it renders with attributes', async function (assert) {
|
||||
await render(hbs`<Rose::Card::Link id="card" />`);
|
||||
assert.ok(find('#card'));
|
||||
});
|
||||
|
||||
test('it renders with content', async function (assert) {
|
||||
await render(hbs`<Rose::Card::Link>
|
||||
<button type="button" />
|
||||
</Rose::Card::Link>`);
|
||||
assert.ok(find('button'));
|
||||
});
|
||||
});
|
||||
@@ -1,26 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { render, find } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
|
||||
module('Integration | Component | rose/cards', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
test('it renders', async function (assert) {
|
||||
await render(hbs`
|
||||
<Rose::Cards as |cards|>
|
||||
<cards.link
|
||||
@title="Card Title"
|
||||
@description="Description"
|
||||
/>
|
||||
</Rose::Cards>
|
||||
`);
|
||||
assert.ok(find('.rose-cards'));
|
||||
assert.ok(find('.rose-card-link'));
|
||||
});
|
||||
});
|
||||
@@ -23,7 +23,7 @@
|
||||
<form.radioGroup
|
||||
@name='theme'
|
||||
@selectedValue={{@model.state}}
|
||||
@changed={{route-action 'changeState'}}
|
||||
@changed={{fn @changeState @model}}
|
||||
as |radioGroup|
|
||||
>
|
||||
{{#each @model.options.state as |state|}}
|
||||
@@ -48,14 +48,14 @@
|
||||
{{#if @model.isPrimary}}
|
||||
<dropdown.button
|
||||
@disabled={{@model.canSave}}
|
||||
{{on 'click' (route-action 'removeAsPrimary' @model)}}
|
||||
{{on 'click' (fn @removeAsPrimary @model)}}
|
||||
>
|
||||
{{t 'resources.auth-method.actions.remove-as-primary'}}
|
||||
</dropdown.button>
|
||||
{{else}}
|
||||
<dropdown.button
|
||||
@disabled={{@model.canSave}}
|
||||
{{on 'click' (route-action 'makePrimary' @model)}}
|
||||
{{on 'click' (fn @makePrimary @model)}}
|
||||
>
|
||||
{{t 'resources.auth-method.actions.make-primary'}}
|
||||
</dropdown.button>
|
||||
@@ -86,7 +86,7 @@
|
||||
<dropdown.button
|
||||
@style='danger'
|
||||
@disabled={{@model.canSave}}
|
||||
{{on 'click' (route-action 'delete' @model)}}
|
||||
{{on 'click' (fn @delete @model)}}
|
||||
>
|
||||
{{t 'resources.auth-method.actions.delete'}}
|
||||
</dropdown.button>
|
||||
|
||||
@@ -76,11 +76,11 @@
|
||||
<Hds::Form::TextInput::Field
|
||||
name='password'
|
||||
@type='password'
|
||||
@value={{@model.password}}
|
||||
@value={{this.password}}
|
||||
@isInvalid={{@model.errors.password}}
|
||||
disabled={{form.disabled}}
|
||||
autocomplete='new-password'
|
||||
{{on 'input' (set-from-event @model 'password')}}
|
||||
{{on 'input' (set-from-event this 'password')}}
|
||||
as |F|
|
||||
>
|
||||
<F.Label>{{t 'form.password.label'}}</F.Label>
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
{{!
|
||||
Copyright (c) HashiCorp, Inc.
|
||||
SPDX-License-Identifier: MPL-2.0
|
||||
}}
|
||||
|
||||
<Rose::Form
|
||||
@onSubmit={{@submit}}
|
||||
@cancel={{@cancel}}
|
||||
@disabled={{@model.isSaving}}
|
||||
@showEditToggle={{if @model.isNew false true}}
|
||||
as |form|
|
||||
>
|
||||
<Hds::Form::TextInput::Field
|
||||
name='name'
|
||||
@isOptional={{true}}
|
||||
@value={{@model.name}}
|
||||
@isInvalid={{@model.errors.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}}
|
||||
<F.Error as |E|>
|
||||
{{#each @model.errors.name as |error|}}
|
||||
<E.Message>{{error.message}}</E.Message>
|
||||
{{/each}}
|
||||
</F.Error>
|
||||
{{/if}}
|
||||
</Hds::Form::TextInput::Field>
|
||||
<Hds::Form::Textarea::Field
|
||||
name='description'
|
||||
@isOptional={{true}}
|
||||
@value={{@model.description}}
|
||||
@isInvalid={{@model.errors.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}}
|
||||
<F.Error as |E|>
|
||||
{{#each @model.errors.description as |error|}}
|
||||
<E.Message>{{error.message}}</E.Message>
|
||||
{{/each}}
|
||||
</F.Error>
|
||||
{{/if}}
|
||||
</Hds::Form::Textarea::Field>
|
||||
|
||||
<Hds::Form::Fieldset as |F|>
|
||||
<F.Legend>
|
||||
{{t 'resources.alias.form.options.label'}}
|
||||
</F.Legend>
|
||||
<F.HelperText>
|
||||
{{t 'resources.alias.description'}}
|
||||
</F.HelperText>
|
||||
<F.Control>
|
||||
<Hds::Form::RadioCard::Group @name='type' @alignment='center' as |G|>
|
||||
<G.Legend>{{t 'resources.target.form.type.label'}}</G.Legend>
|
||||
<G.RadioCard @maxWidth='15rem' @checked='true' @disabled='true' as |R|>
|
||||
<R.Label>{{t 'resources.alias.form.type.label'}}</R.Label>
|
||||
<R.Description>{{t 'resources.alias.form.type.help'}}</R.Description>
|
||||
<R.Icon @name='target' />
|
||||
</G.RadioCard>
|
||||
</Hds::Form::RadioCard::Group>
|
||||
</F.Control>
|
||||
</Hds::Form::Fieldset>
|
||||
|
||||
<Hds::Form::TextInput::Field
|
||||
name='value'
|
||||
@isRequired={{true}}
|
||||
@value={{@model.value}}
|
||||
@isInvalid={{@model.errors.value}}
|
||||
disabled={{form.disabled}}
|
||||
{{on 'input' (set-from-event @model 'value')}}
|
||||
as |F|
|
||||
>
|
||||
<F.Label>{{t 'resources.alias.form.value.label'}}</F.Label>
|
||||
<F.HelperText>{{t 'resources.alias.form.value.help'}}</F.HelperText>
|
||||
{{#if @model.errors.value}}
|
||||
<F.Error as |E|>
|
||||
{{#each @model.errors.value as |error|}}
|
||||
<E.Message>{{error.message}}</E.Message>
|
||||
{{/each}}
|
||||
</F.Error>
|
||||
{{/if}}
|
||||
</Hds::Form::TextInput::Field>
|
||||
|
||||
<Hds::Form::TextInput::Field
|
||||
name='destination_id'
|
||||
@isOptional={{true}}
|
||||
@value={{@model.destination_id}}
|
||||
@isInvalid={{@model.errors.destination_id}}
|
||||
readonly={{@hideTargetId}}
|
||||
disabled={{form.disabled}}
|
||||
{{on 'input' (set-from-event @model 'destination_id')}}
|
||||
as |F|
|
||||
>
|
||||
<F.Label>{{t 'resources.alias.form.destination_id.label'}}</F.Label>
|
||||
<F.HelperText>{{t
|
||||
'resources.alias.form.destination_id.help'
|
||||
}}</F.HelperText>
|
||||
{{#if @model.errors.destination_id}}
|
||||
<F.Error as |E|>
|
||||
{{#each @model.errors.destination_id as |error|}}
|
||||
<E.Message>{{error.message}}</E.Message>
|
||||
{{/each}}
|
||||
</F.Error>
|
||||
{{/if}}
|
||||
</Hds::Form::TextInput::Field>
|
||||
|
||||
<Hds::Form::TextInput::Field
|
||||
name='authorize_session_arguments'
|
||||
@isOptional={{true}}
|
||||
@value={{@model.authorize_session_arguments.host_id}}
|
||||
@isInvalid={{@model.errors.authorize_session_arguments.host_id}}
|
||||
disabled={{form.disabled}}
|
||||
{{on 'input' this.handleHostIdChange}}
|
||||
as |F|
|
||||
>
|
||||
<F.Label>{{t
|
||||
'resources.alias.form.authorize_session_arguments.label'
|
||||
}}</F.Label>
|
||||
<F.HelperText>{{t
|
||||
'resources.alias.form.authorize_session_arguments.help'
|
||||
}}</F.HelperText>
|
||||
{{#if @model.errors.authorize_session_arguments.host_id}}
|
||||
<F.Error as |E|>
|
||||
{{#each @model.errors.authorize_session_arguments.host_id as |error|}}
|
||||
<E.Message>{{error.message}}</E.Message>
|
||||
{{/each}}
|
||||
</F.Error>
|
||||
{{/if}}
|
||||
</Hds::Form::TextInput::Field>
|
||||
|
||||
<InfoField
|
||||
@value={{@model.scopeModel.displayName}}
|
||||
@icon='globe'
|
||||
disabled={{form.disabled}}
|
||||
as |F|
|
||||
>
|
||||
<F.Label>{{t 'resources.scope.title'}}</F.Label>
|
||||
</InfoField>
|
||||
|
||||
{{#if (can 'save model' @model)}}
|
||||
<form.actions
|
||||
@enableEditText={{t 'actions.edit-form'}}
|
||||
@submitText={{t 'actions.save'}}
|
||||
@cancelText={{t 'actions.cancel'}}
|
||||
/>
|
||||
{{/if}}
|
||||
</Rose::Form>
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
import { action } from '@ember/object';
|
||||
|
||||
export default class FormAliasComponent extends Component {
|
||||
//actions
|
||||
/**
|
||||
* Handles input changes
|
||||
*/
|
||||
@action
|
||||
handleHostIdChange({ target: { value } }) {
|
||||
this.args.model.authorize_session_arguments = {
|
||||
host_id: value,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -97,8 +97,8 @@
|
||||
<Hds::Form::TextInput::Field
|
||||
name='client_id'
|
||||
@value={{@model.client_id}}
|
||||
@isRequired={{true}}
|
||||
@isInvalid={{@model.errors.client_id}}
|
||||
@isOptional={{true}}
|
||||
disabled={{form.disabled}}
|
||||
{{on 'input' (set-from-event @model 'client_id')}}
|
||||
as |F|
|
||||
@@ -117,8 +117,8 @@
|
||||
<Hds::Form::TextInput::Field
|
||||
name='client_secret'
|
||||
@value={{@model.client_secret}}
|
||||
@isRequired={{true}}
|
||||
@isInvalid={{@model.errors.client_secret}}
|
||||
@isOptional={{true}}
|
||||
disabled={{form.disabled}}
|
||||
@hasVisibilityToggle={{false}}
|
||||
@type='password'
|
||||
@@ -147,7 +147,11 @@
|
||||
{{/unless}}
|
||||
|
||||
{{! Signing Algorithms }}
|
||||
<Form::Field::ListWrapper @layout='horizontal' @disabled={{form.disabled}}>
|
||||
<Form::Field::ListWrapper
|
||||
@layout='horizontal'
|
||||
@isOptional={{true}}
|
||||
@disabled={{form.disabled}}
|
||||
>
|
||||
<:fieldset as |F|>
|
||||
<F.Legend>
|
||||
{{t 'form.signing_algorithms.label'}}
|
||||
@@ -412,6 +416,7 @@
|
||||
<Hds::Form::TextInput::Field
|
||||
name='max_age'
|
||||
@value={{@model.max_age}}
|
||||
@isOptional={{true}}
|
||||
@isInvalid={{@model.errors.max_age}}
|
||||
disabled={{form.disabled}}
|
||||
{{on 'input' (set-from-event @model 'max_age')}}
|
||||
@@ -431,6 +436,7 @@
|
||||
<Hds::Form::TextInput::Field
|
||||
name='api_url_prefix'
|
||||
@value={{@model.api_url_prefix}}
|
||||
@isRequired={{true}}
|
||||
@isInvalid={{@model.errors.api_url_prefix}}
|
||||
disabled={{form.disabled}}
|
||||
{{on 'input' (set-from-event @model 'api_url_prefix')}}
|
||||
|
||||
@@ -11,11 +11,10 @@
|
||||
as |form|
|
||||
>
|
||||
<Hds::Form::TextInput::Field
|
||||
@isOptional={{true}}
|
||||
name='name'
|
||||
@value={{@model.name}}
|
||||
@isInvalid={{@model.errors.name}}
|
||||
@type='text'
|
||||
name='name'
|
||||
@isOptional={{true}}
|
||||
disabled={{form.disabled}}
|
||||
{{on 'input' (set-from-event @model 'name')}}
|
||||
as |F|
|
||||
@@ -25,17 +24,19 @@
|
||||
{{#if @model.errors.name}}
|
||||
<F.Error as |E|>
|
||||
{{#each @model.errors.name as |error|}}
|
||||
<E.Message>{{error.message}}</E.Message>
|
||||
<E.Message>
|
||||
{{error.message}}
|
||||
</E.Message>
|
||||
{{/each}}
|
||||
</F.Error>
|
||||
{{/if}}
|
||||
</Hds::Form::TextInput::Field>
|
||||
|
||||
<Hds::Form::Textarea::Field
|
||||
@isOptional={{true}}
|
||||
name='description'
|
||||
@value={{@model.description}}
|
||||
@isInvalid={{@model.errors.description}}
|
||||
name='description'
|
||||
@isOptional={{true}}
|
||||
disabled={{form.disabled}}
|
||||
as |F|
|
||||
>
|
||||
|
||||
@@ -11,12 +11,12 @@
|
||||
@showEditToggle={{if @model.isNew false true}}
|
||||
as |form|
|
||||
>
|
||||
|
||||
<Hds::Form::TextInput::Field
|
||||
@isOptional={{true}}
|
||||
name='name'
|
||||
@value={{@model.name}}
|
||||
@isInvalid={{@model.errors.name}}
|
||||
@type='text'
|
||||
name='name'
|
||||
@isOptional={{true}}
|
||||
disabled={{form.disabled}}
|
||||
{{on 'input' (set-from-event @model 'name')}}
|
||||
as |F|
|
||||
@@ -26,17 +26,19 @@
|
||||
{{#if @model.errors.name}}
|
||||
<F.Error as |E|>
|
||||
{{#each @model.errors.name as |error|}}
|
||||
<E.Message>{{error.message}}</E.Message>
|
||||
<E.Message>
|
||||
{{error.message}}
|
||||
</E.Message>
|
||||
{{/each}}
|
||||
</F.Error>
|
||||
{{/if}}
|
||||
</Hds::Form::TextInput::Field>
|
||||
|
||||
<Hds::Form::Textarea::Field
|
||||
@isOptional={{true}}
|
||||
name='description'
|
||||
@value={{@model.description}}
|
||||
@isInvalid={{@model.errors.description}}
|
||||
name='description'
|
||||
@isOptional={{true}}
|
||||
disabled={{form.disabled}}
|
||||
as |F|
|
||||
>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
import { types } from 'api/models/credential-store';
|
||||
import { TYPES_CREDENTIAL_STORE } from 'api/models/credential-store';
|
||||
|
||||
//Note: this is a temporary solution till we have resource type helper in place
|
||||
const icons = ['keychain', 'vault'];
|
||||
@@ -16,6 +16,9 @@ export default class FormStaticCredentialStoreIndexComponent extends Component {
|
||||
* @type {object}
|
||||
*/
|
||||
get mapResourceTypeWithIcon() {
|
||||
return types.reduce((obj, type, i) => ({ ...obj, [type]: icons[i] }), {});
|
||||
return TYPES_CREDENTIAL_STORE.reduce(
|
||||
(obj, type, i) => ({ ...obj, [type]: icons[i] }),
|
||||
{},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -116,6 +116,7 @@
|
||||
<Hds::Form::TextInput::Field
|
||||
name='worker_filter'
|
||||
@value={{@model.worker_filter}}
|
||||
@isOptional={{true}}
|
||||
@isInvalid={{@model.errors.worker_filter}}
|
||||
disabled={{form.disabled}}
|
||||
{{on 'input' (set-from-event @model 'worker_filter')}}
|
||||
@@ -178,6 +179,7 @@
|
||||
<Hds::Form::TextInput::Field
|
||||
name='namespace'
|
||||
@value={{@model.namespace}}
|
||||
@isOptional={{true}}
|
||||
@isInvalid={{@model.errors.namespace}}
|
||||
disabled={{form.disabled}}
|
||||
{{on 'input' (set-from-event @model 'namespace')}}
|
||||
@@ -199,6 +201,7 @@
|
||||
<Hds::Form::TextInput::Field
|
||||
name='tls_server_name'
|
||||
@value={{@model.tls_server_name}}
|
||||
@isOptional={{true}}
|
||||
@isInvalid={{@model.errors.tls_server_name}}
|
||||
disabled={{form.disabled}}
|
||||
{{on 'input' (set-from-event @model 'tls_server_name')}}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
import { types } from 'api/models/credential-store';
|
||||
import { TYPES_CREDENTIAL_STORE } from 'api/models/credential-store';
|
||||
|
||||
//Note: this is a temporary solution till we have resource type helper in place
|
||||
const icons = ['keychain', 'vault'];
|
||||
@@ -16,6 +16,9 @@ export default class FormVaultCredentialStoreIndexComponent extends Component {
|
||||
* @type {object}
|
||||
*/
|
||||
get mapResourceTypeWithIcon() {
|
||||
return types.reduce((obj, type, i) => ({ ...obj, [type]: icons[i] }), {});
|
||||
return TYPES_CREDENTIAL_STORE.reduce(
|
||||
(obj, type, i) => ({ ...obj, [type]: icons[i] }),
|
||||
{},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -51,6 +51,7 @@
|
||||
</F.Error>
|
||||
{{/if}}
|
||||
</Hds::Form::Textarea::Field>
|
||||
|
||||
{{#if @model.isNew}}
|
||||
<Hds::Form::RadioCard::Group @name='type' as |G|>
|
||||
<G.Legend>{{t 'form.type.label'}}</G.Legend>
|
||||
|
||||
@@ -107,6 +107,7 @@
|
||||
<Hds::Form::TextInput::Field
|
||||
name='region'
|
||||
@value={{@model.region}}
|
||||
@isRequired={{true}}
|
||||
disabled={{form.disabled}}
|
||||
{{on 'input' (set-from-event @model 'region')}}
|
||||
as |F|
|
||||
@@ -123,6 +124,7 @@
|
||||
<Hds::Form::TextInput::Field
|
||||
name='access_key_id'
|
||||
@value={{@model.access_key_id}}
|
||||
@isRequired={{true}}
|
||||
disabled={{form.disabled}}
|
||||
{{on 'input' (set-from-event @model 'access_key_id')}}
|
||||
as |F|
|
||||
@@ -139,6 +141,7 @@
|
||||
<Hds::Form::TextInput::Field
|
||||
name='secret_access_key'
|
||||
@value={{@model.secret_access_key}}
|
||||
@isRequired={{true}}
|
||||
disabled={{form.disabled}}
|
||||
{{on 'input' (set-from-event @model 'secret_access_key')}}
|
||||
as |F|
|
||||
|
||||
@@ -5,20 +5,23 @@
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
import { action } from '@ember/object';
|
||||
import { types, pluginTypes } from 'api/models/host-catalog';
|
||||
import {
|
||||
TYPES_HOST_CATALOG,
|
||||
TYPES_HOST_CATALOG_PLUGIN,
|
||||
} 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;
|
||||
hostCatalogTypes = TYPES_HOST_CATALOG;
|
||||
/**
|
||||
* maps resource type with icon
|
||||
* @type {object}
|
||||
*/
|
||||
get mapResourceTypeWithIcon() {
|
||||
return pluginTypes.reduce(
|
||||
return TYPES_HOST_CATALOG_PLUGIN.reduce(
|
||||
(obj, plugin, i) => ({ ...obj, [plugin]: icons[i] }),
|
||||
{},
|
||||
);
|
||||
|
||||
@@ -107,6 +107,7 @@
|
||||
<Hds::Form::TextInput::Field
|
||||
name='tenant_id'
|
||||
@value={{@model.tenant_id}}
|
||||
@isRequired={{true}}
|
||||
disabled={{form.disabled}}
|
||||
{{on 'input' (set-from-event @model 'tenant_id')}}
|
||||
as |F|
|
||||
@@ -123,6 +124,7 @@
|
||||
<Hds::Form::TextInput::Field
|
||||
name='subscription_id'
|
||||
@value={{@model.subscription_id}}
|
||||
@isRequired={{true}}
|
||||
disabled={{form.disabled}}
|
||||
{{on 'input' (set-from-event @model 'subscription_id')}}
|
||||
as |F|
|
||||
@@ -139,6 +141,7 @@
|
||||
<Hds::Form::TextInput::Field
|
||||
name='client_id'
|
||||
@value={{@model.client_id}}
|
||||
@isRequired={{true}}
|
||||
disabled={{form.disabled}}
|
||||
{{on 'input' (set-from-event @model 'client_id')}}
|
||||
as |F|
|
||||
@@ -155,6 +158,7 @@
|
||||
<Hds::Form::TextInput::Field
|
||||
name='secret_value'
|
||||
@value={{@model.secret_value}}
|
||||
@isRequired={{true}}
|
||||
disabled={{form.disabled}}
|
||||
{{on 'input' (set-from-event @model 'secret_value')}}
|
||||
as |F|
|
||||
|
||||
@@ -4,20 +4,23 @@
|
||||
*/
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
import { types, pluginTypes } from 'api/models/host-catalog';
|
||||
import {
|
||||
TYPES_HOST_CATALOG,
|
||||
TYPES_HOST_CATALOG_PLUGIN,
|
||||
} 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;
|
||||
hostCatalogTypes = TYPES_HOST_CATALOG;
|
||||
/**
|
||||
* maps resource type with icon
|
||||
* @type {object}
|
||||
*/
|
||||
get mapResourceTypeWithIcon() {
|
||||
return pluginTypes.reduce(
|
||||
return TYPES_HOST_CATALOG_PLUGIN.reduce(
|
||||
(obj, plugin, i) => ({ ...obj, [plugin]: icons[i] }),
|
||||
{},
|
||||
);
|
||||
|
||||
@@ -4,20 +4,23 @@
|
||||
*/
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
import { types, pluginTypes } from 'api/models/host-catalog';
|
||||
import {
|
||||
TYPES_HOST_CATALOG,
|
||||
TYPES_HOST_CATALOG_PLUGIN,
|
||||
} 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;
|
||||
hostCatalogTypes = TYPES_HOST_CATALOG;
|
||||
/**
|
||||
* maps resource type with icon
|
||||
* @type {object}
|
||||
*/
|
||||
get mapResourceTypeWithIcon() {
|
||||
return pluginTypes.reduce(
|
||||
return TYPES_HOST_CATALOG_PLUGIN.reduce(
|
||||
(obj, plugin, i) => ({ ...obj, [plugin]: icons[i] }),
|
||||
{},
|
||||
);
|
||||
|
||||
@@ -97,7 +97,11 @@
|
||||
</:field>
|
||||
</Form::Field::ListWrapper>
|
||||
|
||||
<Form::Field::ListWrapper @layout='horizontal' @disabled={{form.disabled}}>
|
||||
<Form::Field::ListWrapper
|
||||
@layout='horizontal'
|
||||
@isOptional={{true}}
|
||||
@disabled={{form.disabled}}
|
||||
>
|
||||
<:fieldset as |F|>
|
||||
<F.Legend>
|
||||
{{t 'resources.host-set.form.filter.label'}}
|
||||
@@ -127,13 +131,14 @@
|
||||
name='sync_interval_seconds'
|
||||
@value={{@model.sync_interval_seconds}}
|
||||
@isInvalid={{@model.errors.sync_interval_seconds}}
|
||||
@isOptional={{true}}
|
||||
disabled={{form.disabled}}
|
||||
{{on 'input' (set-from-event @model 'sync_interval_seconds')}}
|
||||
as |F|
|
||||
>
|
||||
<F.Label>{{t 'form.name.label'}}</F.Label>
|
||||
<F.Label>{{t 'form.sync-interval.label'}}</F.Label>
|
||||
<F.HelperText>
|
||||
{{t 'form.name.help'}}
|
||||
{{t 'form.sync-interval.help'}}
|
||||
<Hds::Link::Inline @href={{doc-url 'host-set.sync-interval-seconds'}}>
|
||||
{{t 'actions.learn-more'}}
|
||||
</Hds::Link::Inline>
|
||||
|
||||
@@ -123,13 +123,14 @@
|
||||
name='sync_interval_seconds'
|
||||
@value={{@model.sync_interval_seconds}}
|
||||
@isInvalid={{@model.errors.sync_interval_seconds}}
|
||||
@isOptional={{true}}
|
||||
disabled={{form.disabled}}
|
||||
{{on 'input' (set-from-event @model 'sync_interval_seconds')}}
|
||||
as |F|
|
||||
>
|
||||
<F.Label>{{t 'form.name.label'}}</F.Label>
|
||||
<F.Label>{{t 'form.sync-interval.label'}}</F.Label>
|
||||
<F.HelperText>
|
||||
{{t 'form.name.help'}}
|
||||
{{t 'form.sync-interval.help'}}
|
||||
<Hds::Link::Inline @href={{doc-url 'host-set.sync-interval-seconds'}}>
|
||||
{{t 'actions.learn-more'}}
|
||||
</Hds::Link::Inline>
|
||||
|
||||
@@ -35,7 +35,7 @@ export default class FormRoleAddPrincipalsIndexComponent extends Component {
|
||||
get filteredPrincipals() {
|
||||
// Retrieve principal IDs assigned to current role
|
||||
const currentPrincipalIDs = this.args.model.principals.map(
|
||||
(principal) => principal.principal_id,
|
||||
(principal) => principal.id,
|
||||
);
|
||||
|
||||
// Retrieve principal IDs not assigned to current role
|
||||
|
||||
Loaded 100 of 356 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user