Merge pull request #313 from hashicorp/session-recording
Merge session recording feature branch
This commit is contained in:
210 files changed
+8541
-161
No files matched your search
@@ -3,6 +3,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- session-recording
|
||||
|
||||
jobs:
|
||||
dependencies:
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
import ModelAbility from './model';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
/**
|
||||
* Provides abilities for channel recordings.
|
||||
*/
|
||||
export default class ChannelRecordingAbility extends ModelAbility {
|
||||
// =services
|
||||
@service abilities;
|
||||
|
||||
// =permissions
|
||||
get canGetAsciicast() {
|
||||
const sessionRecording = this.model.connection_recording.session_recording;
|
||||
const sessionRecordingAbility = this.abilities.abilityFor(
|
||||
'session-recording',
|
||||
sessionRecording
|
||||
);
|
||||
|
||||
return this.model.isAsciicast && sessionRecordingAbility.canDownload;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
import ModelAbility from './model';
|
||||
|
||||
/**
|
||||
* Provides abilities for session recordings.
|
||||
*/
|
||||
export default class SessionRecordingAbility extends ModelAbility {
|
||||
// =services
|
||||
|
||||
// =permissions
|
||||
|
||||
/**
|
||||
* Only "known" session recording types may be read.
|
||||
* @type {boolean}
|
||||
*/
|
||||
get canRead() {
|
||||
return !this.model.isUnknown && super.canRead;
|
||||
}
|
||||
|
||||
get canDownload() {
|
||||
return this.hasAuthorizedAction('download');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
import ModelAbility from './model';
|
||||
|
||||
/**
|
||||
* Provides abilities for storage buckets.
|
||||
*/
|
||||
export default class StorageBucketAbility extends ModelAbility {
|
||||
// =services
|
||||
|
||||
// =permissions
|
||||
|
||||
/**
|
||||
* Only "known" storage bucket types may be read.
|
||||
* @type {boolean}
|
||||
*/
|
||||
get canRead() {
|
||||
return !this.model.isUnknown && super.canRead;
|
||||
}
|
||||
|
||||
/**
|
||||
* Only "known" storage bucket types may be updated.
|
||||
* @type {boolean}
|
||||
*/
|
||||
get canUpdate() {
|
||||
return !this.model.isUnknown && super.canUpdate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Only "known" storage bucket types may be deleted.
|
||||
* @type {boolean}
|
||||
*/
|
||||
get canDelete() {
|
||||
return !this.model.isUnknown && super.canDelete;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import ApplicationAdapter from './application';
|
||||
import { MIME_TYPE_ASCIICAST } from 'api/models/channel-recording';
|
||||
|
||||
export default class SessionRecordingAdapter extends ApplicationAdapter {
|
||||
/**
|
||||
* Overrides the default `buildURL` such that for custom download methods,
|
||||
* URLs will get the query parameters. All other request types behave as normal.
|
||||
*
|
||||
* @param modelName {string}
|
||||
* @param id {string}
|
||||
* @param snapshot {object}
|
||||
* @param requestType {string}
|
||||
* @param query {object}
|
||||
* @returns {string}
|
||||
*/
|
||||
buildURL(modelName, id, snapshot, requestType, query) {
|
||||
let url = super.buildURL(...arguments);
|
||||
const method = snapshot?.adapterOptions?.method;
|
||||
if (method === 'download') {
|
||||
const queryParams = new URLSearchParams(query).toString();
|
||||
if (queryParams) url = `${url}?${queryParams}`;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the download recording for recordings. Takes in a mime type for the
|
||||
* type or recording to return.
|
||||
* The ID can be the session ID, session recording ID,
|
||||
* connection recording ID, or the channel recording ID.
|
||||
* @param id {string}
|
||||
* @param mimeType {string}
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
async getRecordingDownload({ id, mimeType }) {
|
||||
const options = { adapterOptions: { method: 'download' } };
|
||||
const url = this.buildURL('session-recording', id, options, 'findRecord', {
|
||||
mime_type: mimeType,
|
||||
});
|
||||
|
||||
const response = await fetch(url, { headers: this.headers });
|
||||
return response.text();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an ascii cast recording.
|
||||
* @param id
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
getAsciicast(id) {
|
||||
return this.getRecordingDownload({
|
||||
id,
|
||||
mimeType: MIME_TYPE_ASCIICAST,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import ApplicationAdapter from './application';
|
||||
|
||||
export default class StorageBucketAdapter extends ApplicationAdapter {
|
||||
/**
|
||||
* Overrides the default `buildURL` such that on _create_, URLs for records of
|
||||
* type `plugin` get an extra query parameter `?plugin_name`. All other
|
||||
* request types behave as normal.
|
||||
*
|
||||
* @override
|
||||
* @param {string} modelName
|
||||
* @param {string} id
|
||||
* @return {string} url
|
||||
* @return {string} requestType
|
||||
*/
|
||||
buildURL(modelName, id, snapshot, requestType) {
|
||||
let url = super.buildURL(...arguments);
|
||||
const pluginName = snapshot?.attr('plugin')?.name;
|
||||
if (requestType === 'createRecord' && pluginName) {
|
||||
url = `${url}?plugin_name=${pluginName}`;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
import BaseModel from '../../models/base';
|
||||
import { attr } from '@ember-data/model';
|
||||
|
||||
/**
|
||||
* A channel contains data for a single channel within a connection.
|
||||
*/
|
||||
export default class GeneratedChannelRecordingModel extends BaseModel {
|
||||
// =attributes
|
||||
|
||||
@attr('string', {
|
||||
description:
|
||||
'The total number of bytes uploaded from the client in this Channel.',
|
||||
readOnly: true,
|
||||
})
|
||||
bytes_up;
|
||||
|
||||
@attr('string', {
|
||||
description:
|
||||
'The total number of bytes downloaded to the client in this Channel.',
|
||||
readOnly: true,
|
||||
})
|
||||
bytes_down;
|
||||
|
||||
@attr('number', {
|
||||
description:
|
||||
'The total number of errors that occurred during the use of this Channel.',
|
||||
readOnly: true,
|
||||
})
|
||||
errors;
|
||||
|
||||
@attr('date', {
|
||||
description: 'The time the Channel started.',
|
||||
readOnly: true,
|
||||
})
|
||||
start_time;
|
||||
|
||||
@attr('date', {
|
||||
description: 'The time the Channel finished.',
|
||||
readOnly: true,
|
||||
})
|
||||
end_time;
|
||||
|
||||
@attr({
|
||||
description: 'The array of mime types that this recording will support.',
|
||||
readOnly: true,
|
||||
})
|
||||
mime_types;
|
||||
|
||||
@attr('duration', {
|
||||
description: 'The duration of this channel recording in ms.',
|
||||
readOnly: true,
|
||||
})
|
||||
duration;
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
import BaseModel from '../../models/base';
|
||||
import { attr } from '@ember-data/model';
|
||||
|
||||
/**
|
||||
* A connection contains data for a boundary connection within a session.
|
||||
*/
|
||||
export default class GeneratedConnectionRecordingModel extends BaseModel {
|
||||
// =attributes
|
||||
|
||||
@attr('string', {
|
||||
description:
|
||||
'The ID of the Connection which this Connection Recording recorded.',
|
||||
readOnly: true,
|
||||
})
|
||||
connection_id;
|
||||
|
||||
@attr('string', {
|
||||
description:
|
||||
'The total number of bytes uploaded from the client in this Connection.',
|
||||
readOnly: true,
|
||||
})
|
||||
bytes_up;
|
||||
|
||||
@attr('string', {
|
||||
description:
|
||||
'The total number of bytes downloaded to the client in this Connection.',
|
||||
readOnly: true,
|
||||
})
|
||||
bytes_down;
|
||||
|
||||
@attr('number', {
|
||||
description:
|
||||
'The total number of errors that occurred during the use of this Connection.',
|
||||
readOnly: true,
|
||||
})
|
||||
errors;
|
||||
|
||||
@attr('date', {
|
||||
description: 'The time the Connection started.',
|
||||
readOnly: true,
|
||||
})
|
||||
start_time;
|
||||
|
||||
@attr('date', {
|
||||
description: 'The time the Connection finished.',
|
||||
readOnly: true,
|
||||
})
|
||||
end_time;
|
||||
|
||||
@attr({
|
||||
description: 'The array of mime types that this recording will support.',
|
||||
readOnly: true,
|
||||
})
|
||||
mime_types;
|
||||
|
||||
@attr('duration', {
|
||||
description: 'The duration of this connection recording in ms.',
|
||||
readOnly: true,
|
||||
})
|
||||
duration;
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
import BaseModel from '../../models/base';
|
||||
import { attr } from '@ember-data/model';
|
||||
|
||||
/**
|
||||
* A session recording contains all data related to session recordings
|
||||
*/
|
||||
export default class GeneratedSessionRecordingModel extends BaseModel {
|
||||
// =attributes
|
||||
|
||||
@attr('string', {
|
||||
description: 'The type of the resource, to help differentiate schemas.',
|
||||
readOnly: true,
|
||||
})
|
||||
type;
|
||||
|
||||
@attr('date', {
|
||||
description:
|
||||
'The time the Session Recording was created in the controller.',
|
||||
readOnly: true,
|
||||
})
|
||||
created_time;
|
||||
|
||||
@attr('date', {
|
||||
description: 'The time of the most recent update to the Session Recording.',
|
||||
readOnly: true,
|
||||
})
|
||||
updated_time;
|
||||
|
||||
@attr({
|
||||
description:
|
||||
'Contains the values of related fields at the time this Session Recording was created',
|
||||
readOnly: true,
|
||||
})
|
||||
create_time_values;
|
||||
|
||||
@attr('string', {
|
||||
description: 'The ID of the Session which this Session Recording recorded.',
|
||||
readOnly: true,
|
||||
})
|
||||
session_id;
|
||||
|
||||
@attr('string', {
|
||||
description:
|
||||
'The ID of the Storage Bucket for the Target of this Session Recording',
|
||||
readOnly: true,
|
||||
})
|
||||
storage_bucket_id;
|
||||
|
||||
@attr('string', {
|
||||
description:
|
||||
'The total number of bytes uploaded from the client in the Session.',
|
||||
readOnly: true,
|
||||
})
|
||||
bytes_up;
|
||||
|
||||
@attr('string', {
|
||||
description:
|
||||
'The total number of bytes downloaded to the client in the Session.',
|
||||
readOnly: true,
|
||||
})
|
||||
bytes_down;
|
||||
|
||||
@attr('number', {
|
||||
description:
|
||||
'The total number of errors that occurred during the use of the Session.',
|
||||
readOnly: true,
|
||||
})
|
||||
errors;
|
||||
|
||||
@attr('date', {
|
||||
description: 'The time the Session started.',
|
||||
readOnly: true,
|
||||
})
|
||||
start_time;
|
||||
|
||||
@attr('date', {
|
||||
description: 'The time the Session finished.',
|
||||
readOnly: true,
|
||||
})
|
||||
end_time;
|
||||
|
||||
@attr({
|
||||
description: 'The array of mime types that this recording will support.',
|
||||
readOnly: true,
|
||||
})
|
||||
mime_types;
|
||||
|
||||
@attr('duration', {
|
||||
description: 'The duration of this session recording in ms.',
|
||||
readOnly: true,
|
||||
})
|
||||
duration;
|
||||
|
||||
@attr('string', {
|
||||
description:
|
||||
'The current state of the session recording. One of "started", "available" and "unknown".',
|
||||
readOnly: true,
|
||||
})
|
||||
state;
|
||||
|
||||
@attr('string', {
|
||||
description:
|
||||
'Any error seen during the closing of the session recording. Currently only set if state is "unknown".',
|
||||
readOnly: true,
|
||||
})
|
||||
error_details;
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import BaseModel from '../../models/base';
|
||||
import { attr } from '@ember-data/model';
|
||||
|
||||
/**
|
||||
* A storage bucket is a resource that that represents a bucket in an external object store.
|
||||
*/
|
||||
export default class GeneratedStorageBucketModel extends BaseModel {
|
||||
// =attributes
|
||||
|
||||
@attr('string', {
|
||||
description: 'The type of the resource, to help differentiate schemas',
|
||||
})
|
||||
type;
|
||||
|
||||
@attr({
|
||||
description: 'Plugin information for this resource.',
|
||||
readOnly: true,
|
||||
})
|
||||
plugin;
|
||||
|
||||
@attr('string', {
|
||||
description: 'Optional name for identification purposes.',
|
||||
})
|
||||
name;
|
||||
|
||||
@attr('string', {
|
||||
description: 'Optional user-set description for identification purposes.',
|
||||
})
|
||||
description;
|
||||
|
||||
@attr('date', {
|
||||
description: 'The time this resource was created\nOutput only.',
|
||||
readOnly: true,
|
||||
})
|
||||
created_time;
|
||||
|
||||
@attr('date', {
|
||||
description: 'The time this resource was last updated\nOutput only.',
|
||||
readOnly: true,
|
||||
})
|
||||
updated_time;
|
||||
|
||||
@attr('number', {
|
||||
description: 'Current version number of this resource.',
|
||||
})
|
||||
version;
|
||||
|
||||
@attr('string', {
|
||||
description: 'The name of the bucket in the external object store.',
|
||||
})
|
||||
bucket_name;
|
||||
|
||||
@attr('string', {
|
||||
description:
|
||||
'The base path where session recordings will be stored in the external object store.',
|
||||
})
|
||||
bucket_prefix;
|
||||
|
||||
@attr('string', {
|
||||
description:
|
||||
'An expression used to filter the workers that have network access to a service that is hosting the external object store.',
|
||||
})
|
||||
worker_filter;
|
||||
|
||||
@attr('string', {
|
||||
isNestedAttribute: true,
|
||||
description: 'The AWS region to use.',
|
||||
})
|
||||
region;
|
||||
|
||||
@attr('string', {
|
||||
readOnly: true,
|
||||
isNestedAttribute: true,
|
||||
})
|
||||
secrets_hmac;
|
||||
|
||||
@attr('string', {
|
||||
isNestedSecret: true,
|
||||
description: 'The AWS access key ID to use.',
|
||||
})
|
||||
access_key_id;
|
||||
|
||||
@attr('string', {
|
||||
isNestedSecret: true,
|
||||
description: 'The secret access key to use.',
|
||||
})
|
||||
secret_access_key;
|
||||
|
||||
@attr('boolean', {
|
||||
isNestedAttribute: true,
|
||||
description: '',
|
||||
})
|
||||
disable_credential_rotation;
|
||||
}
|
||||
@@ -96,4 +96,16 @@ export default class GeneratedTargetModel extends BaseModel {
|
||||
"The default port that will be listened on by the client's local proxy.",
|
||||
})
|
||||
default_client_port;
|
||||
|
||||
@attr('string', {
|
||||
description: 'The storage bucket associated with this target.',
|
||||
isNestedAttribute: true,
|
||||
})
|
||||
storage_bucket_id;
|
||||
|
||||
@attr('boolean', {
|
||||
description: 'Whether session recording is enabled on the target.',
|
||||
isNestedAttribute: true,
|
||||
})
|
||||
enable_session_recording;
|
||||
}
|
||||
@@ -57,7 +57,7 @@ export default class BaseModel extends Model {
|
||||
|
||||
get scopeModel() {
|
||||
const id = this.scopeID;
|
||||
return this.store.peekRecord('scope', id);
|
||||
return id ? this.store.peekRecord('scope', id) : null;
|
||||
}
|
||||
|
||||
// Ember seems to have a bug with its internals and can't correctly set
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import GeneratedChannelRecordingModel from '../generated/models/channel-recording';
|
||||
import { belongsTo } from '@ember-data/model';
|
||||
|
||||
export const MIME_TYPE_ASCIICAST = 'application/x-asciicast';
|
||||
|
||||
export default class ChannelRecordingModel extends GeneratedChannelRecordingModel {
|
||||
@belongsTo('connection-recording', { async: false }) connection_recording;
|
||||
|
||||
/**
|
||||
* True if the channel supports asciicast.
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isAsciicast() {
|
||||
return this.mime_types?.includes(MIME_TYPE_ASCIICAST) ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Download the ascii cast recording for this channel.
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
getAsciicast() {
|
||||
const adapter = this.store.adapterFor('session-recording');
|
||||
return adapter.getAsciicast(this.id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import GeneratedConnectionRecordingModel from '../generated/models/connection-recording';
|
||||
import { belongsTo, hasMany } from '@ember-data/model';
|
||||
|
||||
export default class ConnectionRecordingModel extends GeneratedConnectionRecordingModel {
|
||||
@belongsTo('session-recording', { async: false }) session_recording;
|
||||
@hasMany('channel-recording', { async: false }) channel_recordings;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import GeneratedSessionRecordingModel from '../generated/models/session-recording';
|
||||
import { hasMany } from '@ember-data/model';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export const TYPE_SESSION_RECORDING_SSH = 'ssh';
|
||||
export const TYPES_SESSION_RECORDING = Object.freeze([
|
||||
TYPE_SESSION_RECORDING_SSH,
|
||||
]);
|
||||
|
||||
export const STATE_SESSION_RECORDING_STARTED = 'started';
|
||||
export const STATE_SESSION_RECORDING_AVAILABLE = 'available';
|
||||
export const STATE_SESSION_RECORDING_UNKNOWN = 'unknown';
|
||||
|
||||
export default class SessionRecordingModel extends GeneratedSessionRecordingModel {
|
||||
@hasMany('connection-recording', { async: false }) connection_recordings;
|
||||
|
||||
@service store;
|
||||
|
||||
/**
|
||||
* True if the session recording type is `ssh`.
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isSSH() {
|
||||
return this.type === TYPE_SESSION_RECORDING_SSH;
|
||||
}
|
||||
|
||||
/**
|
||||
* True if the session recording is an unknown type.
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isUnknown() {
|
||||
return !TYPES_SESSION_RECORDING.includes(this.type);
|
||||
}
|
||||
|
||||
/**
|
||||
* True if the session recording is in an available state.
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isAvailable() {
|
||||
return this.state === STATE_SESSION_RECORDING_AVAILABLE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns scope name or scope id from the target that belongs to the session recording.
|
||||
* @type {string}
|
||||
*/
|
||||
get targetScopeDisplayName() {
|
||||
return (
|
||||
this.create_time_values?.target?.scope?.name ||
|
||||
this.create_time_values?.target?.scope?.id
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the user name or user id that belongs to the session recording.
|
||||
* @type {string}
|
||||
*/
|
||||
get userDisplayName() {
|
||||
return (
|
||||
this.create_time_values?.user?.name || this.create_time_values?.user?.id
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import GeneratedStorageBucketModel from '../generated/models/storage-bucket';
|
||||
|
||||
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 TYPES_STORAGE_BUCKET_PLUGIN = Object.freeze([
|
||||
TYPE_STORAGE_BUCKET_PLUGIN_AWS_S3,
|
||||
]);
|
||||
|
||||
export default class StorageBucketModel extends GeneratedStorageBucketModel {
|
||||
/**
|
||||
* True if the storage bucket is a plugin.
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isPlugin() {
|
||||
return this.type === TYPE_STORAGE_BUCKET_PLUGIN;
|
||||
}
|
||||
|
||||
/**
|
||||
* True if the storage bucket is an unknown type.
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isUnknown() {
|
||||
return (
|
||||
!TYPES_STORAGE_BUCKET.includes(this.type) ||
|
||||
(this.isPlugin &&
|
||||
!TYPES_STORAGE_BUCKET_PLUGIN.includes(this.plugin?.name))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* True if the storage bucket plugin type is AWS.
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isAWS() {
|
||||
return this.compositeType === TYPE_STORAGE_BUCKET_PLUGIN_AWS_S3;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the storage bucket is a plugin return `plugin.name`,
|
||||
* otherwise return the storage bucket type.
|
||||
* @type {string}
|
||||
*/
|
||||
get compositeType() {
|
||||
if (this.isUnknown) return 'unknown';
|
||||
return this.plugin.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets `type` to `plugin` and `plugin.name` to the specified type.
|
||||
*/
|
||||
set compositeType(type) {
|
||||
this.type = TYPE_STORAGE_BUCKET_PLUGIN;
|
||||
this.plugin = { name: type };
|
||||
}
|
||||
}
|
||||
@@ -317,6 +317,7 @@ export default class TargetModel extends GeneratedTargetModel {
|
||||
options
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* True if the target type is tcp.
|
||||
* @type {boolean}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import ApplicationSerializer from './application';
|
||||
import { EmbeddedRecordsMixin } from '@ember-data/serializer/rest';
|
||||
|
||||
export default class ConnectionSerializer extends ApplicationSerializer.extend(
|
||||
EmbeddedRecordsMixin
|
||||
) {
|
||||
attrs = {
|
||||
channel_recordings: { embedded: 'always' },
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import ApplicationSerializer from './application';
|
||||
import { EmbeddedRecordsMixin } from '@ember-data/serializer/rest';
|
||||
export default class RecordingSerializer extends ApplicationSerializer.extend(
|
||||
EmbeddedRecordsMixin
|
||||
) {
|
||||
attrs = {
|
||||
connection_recordings: { embedded: 'always' },
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import ApplicationSerializer from './application';
|
||||
|
||||
export default class StorageBucketSerializer extends ApplicationSerializer {
|
||||
serialize(snapshot) {
|
||||
const serialized = super.serialize(...arguments);
|
||||
if (!snapshot.record.isNew) {
|
||||
delete serialized.bucket_name;
|
||||
delete serialized.bucket_prefix;
|
||||
}
|
||||
return serialized;
|
||||
}
|
||||
|
||||
normalize(typeClass, hash, ...rest) {
|
||||
const normalizedHash = structuredClone(hash);
|
||||
const normalized = super.normalize(typeClass, normalizedHash, ...rest);
|
||||
|
||||
// Remove secret fields so we don't track them after creating/updating
|
||||
normalized.data.attributes.access_key_id = null;
|
||||
normalized.data.attributes.secret_access_key = null;
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,16 @@ export default class TargetSerializer extends ApplicationSerializer {
|
||||
snapshot,
|
||||
injectedApplicationCredentialSourceIDs
|
||||
);
|
||||
|
||||
if (snapshot?.adapterOptions?.method === 'set-storage-bucket') {
|
||||
serialized = this.serializeWithStorageBucket(snapshot);
|
||||
}
|
||||
if (snapshot?.adapterOptions?.method === 'remove-storage-bucket') {
|
||||
return {
|
||||
version: snapshot.attr('version'),
|
||||
};
|
||||
}
|
||||
|
||||
return serialized;
|
||||
}
|
||||
|
||||
@@ -91,4 +101,17 @@ export default class TargetSerializer extends ApplicationSerializer {
|
||||
injected_application_credential_source_ids,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a payload containing only version and storage bucket id,
|
||||
* rather than existing instances on the model.
|
||||
* @param {Snapshot} snapshot
|
||||
* @returns {object}
|
||||
*/
|
||||
serializeWithStorageBucket(snapshot) {
|
||||
return {
|
||||
version: snapshot.attr('version'),
|
||||
storage_bucket_id: snapshot?.attr('storage_bucket_id'),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
import Transform from '@ember-data/serializer/transform';
|
||||
|
||||
export default class DurationTransform extends Transform {
|
||||
/**
|
||||
* Normalize a duration string in the format "3.0001s" to milliseconds.
|
||||
* The backend returns a precision of nanoseconds but we round to whole
|
||||
* milliseconds to avoid any issues with floating point math.
|
||||
* @param serialized
|
||||
* @returns {number}
|
||||
*/
|
||||
deserialize(serialized) {
|
||||
if (!serialized) return 0;
|
||||
const duration = serialized.split('s')[0];
|
||||
return Math.round(parseFloat(duration) * 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize a duration in ms to a string in seconds.
|
||||
* @param deserialized
|
||||
* @returns {string}
|
||||
*/
|
||||
serialize(deserialized) {
|
||||
if (!deserialized) return '0s';
|
||||
const durationInSeconds = deserialized / 1000;
|
||||
|
||||
// We use `toLocaleString` to handle scenarios with very large numbers or
|
||||
// very small numbers which will be represented as exponents.
|
||||
// We force the max number of fraction digits to match the precision of the
|
||||
// backend which is in nanoseconds.
|
||||
return `${durationInSeconds.toLocaleString('fullwide', {
|
||||
useGrouping: false,
|
||||
maximumFractionDigits: 9,
|
||||
})}s`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
export { default } from 'api/abilities/channel-recording';
|
||||
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
export { default } from 'api/abilities/session-recording';
|
||||
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
export { default } from 'api/abilities/storage-bucket';
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'api/adapters/session-recording';
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'api/adapters/storage-bucket';
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'api/models/channel-recording';
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'api/models/connection-recording';
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'api/models/session-recording';
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'api/models/storage-bucket';
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'api/serializers/connection-recording';
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'api/serializers/session-recording';
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'api/serializers/storage-bucket';
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'api/transforms/duration';
|
||||
@@ -14,6 +14,8 @@ import { targetHandler } from './route-handlers/target';
|
||||
import { pickRandomStatusString } from './factories/session';
|
||||
import initializeMockIPC from './scenarios/ipc';
|
||||
import makeBooleanFilter from './helpers/bexpr-filter';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { asciicasts } from './data/asciicasts';
|
||||
|
||||
const isTesting = environmentConfig.environment === 'test';
|
||||
|
||||
@@ -33,6 +35,7 @@ export default function (mirageConfig) {
|
||||
function routes() {
|
||||
initializeMockIPC(this, environmentConfig);
|
||||
|
||||
this.passthrough();
|
||||
// make this `http://localhost:8080`, for example, if your API is on a different server
|
||||
// this.urlPrefix = '';
|
||||
|
||||
@@ -649,8 +652,7 @@ function routes() {
|
||||
this.patch('/managed-groups/:id');
|
||||
this.del('/managed-groups/:id');
|
||||
|
||||
// worker
|
||||
|
||||
// workers
|
||||
this.get(
|
||||
'/workers',
|
||||
({ workers }, { queryParams: { scope_id: scopeId } }) => {
|
||||
@@ -671,6 +673,65 @@ function routes() {
|
||||
return workers.create(newWorker.attrs);
|
||||
});
|
||||
|
||||
// storage-buckets
|
||||
this.get(
|
||||
'/storage-buckets',
|
||||
({ storageBuckets }, { queryParams: { scope_id: scopeId, recursive } }) => {
|
||||
if (recursive && scopeId === 'global') {
|
||||
return storageBuckets.all();
|
||||
}
|
||||
return storageBuckets.where({ scopeId });
|
||||
}
|
||||
);
|
||||
this.get('/storage-buckets/:id');
|
||||
this.del('/storage-buckets/:id');
|
||||
this.patch('/storage-buckets/:id');
|
||||
this.post(
|
||||
'/storage-buckets',
|
||||
function ({ storageBuckets }, { queryParams: { plugin_name } }) {
|
||||
const attrs = this.normalizedRequestAttrs();
|
||||
if (plugin_name) {
|
||||
attrs.type = 'plugin';
|
||||
attrs.plugin = {
|
||||
name: plugin_name,
|
||||
};
|
||||
}
|
||||
|
||||
// Remove the secrets and add the hmac in the response
|
||||
delete attrs.secrets;
|
||||
attrs.secrets_hmac = faker.git.commitSha();
|
||||
|
||||
return storageBuckets.create(attrs);
|
||||
}
|
||||
);
|
||||
|
||||
// session recordings
|
||||
this.get(
|
||||
'/session-recordings',
|
||||
(
|
||||
{ sessionRecordings },
|
||||
{ queryParams: { scope_id: scopeId, recursive } }
|
||||
) => {
|
||||
if (recursive && scopeId === 'global') {
|
||||
return sessionRecordings.all();
|
||||
}
|
||||
return sessionRecordings.where({ scopeId });
|
||||
}
|
||||
);
|
||||
this.get(
|
||||
'/session-recordings/:idMethod',
|
||||
async ({ sessionRecordings }, { params: { idMethod } }) => {
|
||||
const id = idMethod.split(':')[0];
|
||||
const method = idMethod.split(':')[1];
|
||||
|
||||
if (method === 'download') {
|
||||
return faker.helpers.arrayElement(asciicasts);
|
||||
} else {
|
||||
return sessionRecordings.find(id);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/* Uncomment the following line and the Response import above
|
||||
* Then change the response code to simulate error responses.
|
||||
* this.get('/scopes', () => new Response(505));
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
export const asciicast1 = `{"version": 2, "width": 124, "height": 36, "timestamp": 1670352594, "env": {"SHELL": "/bin/zsh", "TERM": "xterm-256color"}}
|
||||
[2.138652, "o", "\\u001b[1m\\u001b[7m%\\u001b[27m\\u001b[1m\\u001b[0m \\r \\r\\u001b]2;ember-asciinema-player\\u0007\\u001b]1;..iinema-player\\u0007"]
|
||||
[2.220877, "o", "\\r\\u001b[0m\\u001b[27m\\u001b[24m\\u001b[J\\u001b[33member-asciinema-player\\u001b[00m \\u001b[32mgit\\u001b[00m:\\u001b[36m(main*)\\u001b[00m $ \\u001b[K"]
|
||||
[2.221019, "o", "\\u001b[?1h\\u001b="]
|
||||
[2.221076, "o", "\\u001b[?2004h"]
|
||||
[2.676444, "o", "y"]
|
||||
[2.788653, "o", "\\bya"]
|
||||
[2.868652, "o", "r"]
|
||||
[3.001764, "o", "n"]
|
||||
[3.167636, "o", " "]
|
||||
[3.313668, "o", "t"]
|
||||
[3.460527, "o", "e"]
|
||||
[3.561983, "o", "s"]
|
||||
[3.63678, "o", "t"]
|
||||
[3.853437, "o", "\\u001b[?1l\\u001b>"]
|
||||
[3.853473, "o", "\\u001b[?2004l\\r\\r\\n"]
|
||||
[3.854604, "o", "\\u001b]2;yarn test\\u0007\\u001b]1;yarn\\u0007"]
|
||||
[4.489178, "o", "\\u001b[2K"]
|
||||
[4.48982, "o", "\\u001b[1G\\u001b[1myarn run v1.22.19\\u001b[22m\\r\\n"]
|
||||
[4.555618, "o", "\\u001b[2K"]
|
||||
[4.555649, "o", "\\u001b[1G\\u001b[2m$ npm-run-all lint\\u001b[22m\\r\\n"]
|
||||
[5.345253, "o", "\\u001b[2K"]
|
||||
[5.345509, "o", "\\u001b[1G\\u001b[2m$ npm-run-all --print-name --aggregate-output --continue-on-error --parallel \\"lint:!(fix)\\"\\u001b[22m\\r\\n"]
|
||||
[7.225926, "o", "\\r\\n> ember-asciinema-player@0.0.0 lint:js ember-asciinema-player/package.json\\r\\n> eslint . --cache \\r\\n\\r\\n$ eslint . --cache\\r\\n"]
|
||||
[8.398641, "o", "\\r\\n> ember-asciinema-player@0.0.0 lint:hbs ember-asciinema-player/package.json\\r\\n> ember-template-lint . \\r\\n\\r\\n$ ember-template-lint .\\r\\n\\r\\n"]
|
||||
[8.418537, "o", "\\u001b[2K\\u001b[1G\\u001b[2m$ ember test\\u001b[22m\\r\\n"]
|
||||
[12.256156, "o", "\\u001b[?25l"]
|
||||
[12.256464, "o", "\\u001b[32m⠋\\u001b[39m \\u001b[32mBuilding\\u001b[39m"]
|
||||
[12.2573, "o", "\\u001b[2K"]
|
||||
[12.257386, "o", "\\u001b[1G\\u001b[?25h"]
|
||||
[12.257689, "o", "Environment: test\\r\\n"]
|
||||
[12.257857, "o", "\\u001b[?25l\\u001b[32m⠋\\u001b[39m \\u001b[32mBuilding\\u001b[39m"]
|
||||
[12.539035, "o", "\\u001b[2K"]
|
||||
[12.539144, "o", "\\u001b[1G"]
|
||||
[12.539219, "o", "\\u001b[32m⠙\\u001b[39m \\u001b[32mbuilding... \\u001b[39m"]
|
||||
[12.745202, "o", "\\u001b[2K\\u001b[1G"]
|
||||
[12.745343, "o", "\\u001b[32m⠹\\u001b[39m \\u001b[32mbuilding... \\u001b[39m[Babel: @ember/test-helpers > applyPatches]"]
|
||||
[12.825004, "o", "\\u001b[2K"]
|
||||
[12.825031, "o", "\\u001b[1G"]
|
||||
[12.825122, "o", "\\u001b[32m⠸\\u001b[39m \\u001b[32mbuilding... \\u001b[39m[Babel: ember-qunit > applyPatches]"]
|
||||
[12.933588, "o", "\\u001b[2K\\u001b[1G"]
|
||||
[12.933624, "o", "\\u001b[32m⠼\\u001b[39m \\u001b[32mbuilding... \\u001b[39m[Babel: ember-load-initializers > applyPatches]"]
|
||||
[13.194738, "o", "\\u001b[2K\\u001b[1G"]
|
||||
[13.195009, "o", "\\u001b[32m⠴\\u001b[39m \\u001b[32mbuilding... \\u001b[39m[Babel: ember-asciinema-player > applyPatches]"]
|
||||
[13.307878, "o", "\\u001b[2K\\u001b[1G\\u001b[32m⠦\\u001b[39m \\u001b[32mbuilding... \\u001b[39m[Package /assets/vendor.js]"]
|
||||
[13.750717, "o", "\\u001b[2K\\u001b[1G"]
|
||||
[13.750755, "o", "\\u001b[32m⠧\\u001b[39m \\u001b[32mbuilding... \\u001b[39m[Concat: Test Support JS]"]
|
||||
[13.869411, "o", "\\u001b[2K\\u001b[1G"]
|
||||
[13.869438, "o", "\\u001b[32m⠇\\u001b[39m \\u001b[32mbuilding... \\u001b[39m[ember-auto-import-webpack]"]
|
||||
[14.655374, "o", "\\u001b[2K\\u001b[1G"]
|
||||
[14.655415, "o", "\\u001b[32m⠏\\u001b[39m \\u001b[32mbuilding... \\u001b[39m[ember-auto-import-webpack]"]
|
||||
[14.73686, "o", "\\u001b[2K\\u001b[1G\\u001b[?25h"]
|
||||
[14.738533, "o", "\\u001b[?25h"]
|
||||
[14.738818, "o", "\\u001b[?25l\\u001b[32m⠋\\u001b[39m cleaning up"]
|
||||
[14.739061, "o", "\\u001b[2K\\u001b[1G\\u001b[?25hcleaning up...\\r\\n"]
|
||||
[14.739197, "o", "\\u001b[?25l\\u001b[32m⠋\\u001b[39m cleaning up"]
|
||||
[14.907985, "o", "\\u001b[2K"]
|
||||
[14.90808, "o", "\\u001b[1G\\u001b[?25h"]
|
||||
[14.908128, "o", "\\u001b[32mBuilt project successfully.\\u001b[39m\\r\\n"]
|
||||
[16.224632, "o", "ok 1 Chrome 108.0 - [2 ms] - ember-qunit: Ember.onerror validation: Ember.onerror is functioning properly\\r\\n"]
|
||||
[16.346568, "o", "\\r\\n1..1\\r\\n# tests 1\\r\\n# pass 1\\r\\n# skip 0\\r\\n# todo 0\\r\\n# fail 0\\r\\n\\r\\n# ok\\r\\n"]
|
||||
[16.403167, "o", "\\u001b[2K"]
|
||||
[16.403388, "o", "\\u001b[1G✨ Done in 11.92s.\\r\\n"]
|
||||
[16.413967, "o", "\\u001b[1m\\u001b[7m%\\u001b[27m\\u001b[1m\\u001b[0m \\r \\r"]
|
||||
[16.414147, "o", "\\u001b]2;ember-asciinema-player\\u0007\\u001b]1;..iinema-player\\u0007"]
|
||||
[16.528341, "o", "\\r\\u001b[0m\\u001b[27m\\u001b[24m\\u001b[J\\u001b[33member-asciinema-player\\u001b[00m \\u001b[32mgit\\u001b[00m:\\u001b[36m(main*)\\u001b[00m $ \\u001b[K"]
|
||||
[16.528501, "o", "\\u001b[?1h\\u001b="]
|
||||
[16.52863, "o", "\\u001b[?2004h"]
|
||||
[19.078794, "o", "\\u001b[?2004l\\r\\r\\n"]`;
|
||||
|
||||
export const asciicast2 = `{"version": 2, "width": 112, "height": 30, "timestamp": 1672793720, "env": {"SHELL": "/bin/bash", "TERM": "xterm-256color"}}
|
||||
[0.036928, "o", "\\u001b[?2004h\\u001b[1;32m[\\u001b]0;johan@nixos: ~/src/hashicorp/boundary-enterprise\\u0007johan@nixos:~/src/hashicorp/boundary-enterprise]$\\u001b[0m "]
|
||||
[2.827983, "o", "b"]
|
||||
[2.9386, "o", "o"]
|
||||
[2.97036, "o", "u"]
|
||||
[3.032542, "o", "n"]
|
||||
[3.108687, "o", "d"]
|
||||
[3.246045, "o", "a"]
|
||||
[3.340117, "o", "r"]
|
||||
[3.841216, "o", "y"]
|
||||
[4.314958, "o", " "]
|
||||
[6.052102, "o", "u"]
|
||||
[6.146486, "o", "s"]
|
||||
[6.207341, "o", "e"]
|
||||
[6.273087, "o", "r"]
|
||||
[6.378873, "o", "s"]
|
||||
[6.799687, "o", " "]
|
||||
[7.08347, "o", "l"]
|
||||
[7.159196, "o", "i"]
|
||||
[7.249656, "o", "s"]
|
||||
[7.352251, "o", "t"]
|
||||
[7.450038, "o", " "]
|
||||
[7.688195, "o", "-"]
|
||||
[7.931818, "o", "r"]
|
||||
[8.001542, "o", "e"]
|
||||
[8.387771, "o", "c"]
|
||||
[8.526339, "o", "u"]
|
||||
[8.631438, "o", "r"]
|
||||
[8.81899, "o", "s"]
|
||||
[8.958184, "o", "i"]
|
||||
[9.085688, "o", "v"]
|
||||
[9.134885, "o", "e"]
|
||||
[9.786868, "o", "\\r\\n"]
|
||||
[9.788828, "o", "\\u001b[?2004l\\r"]
|
||||
[10.086691, "o", "\\u001b[0m\\r\\nUser information:\\r\\n ID: u_anon\\r\\n Scope ID: global\\r\\n Version: 1\\r\\n Name: anonymous\\r\\n Description: The anonymous user matches any request, whether authenticated or not\\r\\n Authorized Actions:\\r\\n no-op\\r\\n read\\r\\n update\\r\\n delete\\r\\n add-accounts\\r\\n set-accounts\\r\\n remove-accounts\\r\\n\\r\\n ID: u_auth\\r\\n Scope ID: global\\r\\n Version: 1\\r\\n Name: authenticated\\r\\n Description: The authenticated user matches any user that has a valid token\\r\\n Authorized Actions:\\r\\n no-op\\r\\n read\\r\\n update\\r\\n delete\\r\\n add-accounts\\r\\n set-accounts\\r\\n remove-accounts\\r\\n\\r\\n ID: u_recovery\\r\\n Scope ID: global\\r\\n Version: 1\\r\\n Name: recovery\\r\\n Description: The recovery user is used for any request that was performed with the recovery KMS\\r\\n workflow\\r\\n Autho"]
|
||||
[10.086736, "o", "rized Actions:\\r\\n no-op\\r\\n read\\r\\n update\\r\\n delete\\r\\n add-accounts\\r\\n set-accounts\\r\\n remove-accounts\\r\\n\\r\\n ID: u_1234567890\\r\\n Scope ID: global\\r\\n Version: 3\\r\\n Name: admin\\r\\n Description: Initial admin user within the \\"global\\" scope\\r\\n Primary Account ID: acctpw_1234567890\\r\\n Login Name: admin\\r\\n Authorized Actions:\\r\\n no-op\\r\\n read\\r\\n update\\r\\n delete\\r\\n add-accounts\\r\\n set-accounts\\r\\n remove-accounts\\r\\n\\r\\n ID: u_0987654321\\r\\n Scope ID: global\\r\\n Version: 3\\r\\n Name: user\\r\\n Description: Initial unprivileged user\\r\\n Primary Account ID: acctpw_0987654321\\r\\n Login Name: user\\r\\n Authorized Actions:\\r\\n no-op\\r\\n read\\r\\n update\\r\\n delete\\r\\n add-accounts\\r\\n set-accounts\\r\\n remove-accounts\\u001b[0m\\r\\n"]
|
||||
[10.106453, "o", "\\u001b[?2004h\\u001b[1;32m[\\u001b]0;johan@nixos: ~/src/hashicorp/boundary-enterprise\\u0007johan@nixos:~/src/hashicorp/boundary-enterprise]$\\u001b[0m "]
|
||||
[12.501496, "o", "b"]
|
||||
[12.703134, "o", "o"]
|
||||
[12.752198, "o", "u"]
|
||||
[12.775329, "o", "n"]
|
||||
[12.905261, "o", "d"]
|
||||
[13.002373, "o", "a"]
|
||||
[13.100111, "o", "r"]
|
||||
[13.354779, "o", "y"]
|
||||
[13.699827, "o", " "]
|
||||
[13.998849, "o", "t"]
|
||||
[14.139192, "o", "a"]
|
||||
[14.19663, "o", "r"]
|
||||
[14.392075, "o", "g"]
|
||||
[14.489938, "o", "e"]
|
||||
[14.579533, "o", "t"]
|
||||
[14.674628, "o", "s"]
|
||||
[14.830951, "o", " "]
|
||||
[15.015311, "o", "l"]
|
||||
[15.077147, "o", "i"]
|
||||
[15.262221, "o", "s"]
|
||||
[15.355571, "o", "t"]
|
||||
[15.456597, "o", " "]
|
||||
[15.91612, "o", "-"]
|
||||
[16.264491, "o", "r"]
|
||||
[16.351595, "o", "e"]
|
||||
[16.798456, "o", "c"]
|
||||
[16.891689, "o", "u"]
|
||||
[16.997975, "o", "r"]
|
||||
[17.176378, "o", "s"]
|
||||
[17.290104, "o", "i"]
|
||||
[17.434691, "o", "v"]
|
||||
[17.482502, "o", "e"]
|
||||
[18.393541, "o", "\\r\\n"]
|
||||
[18.39558, "o", "\\u001b[?2004l\\r"]
|
||||
[18.673452, "o", "\\u001b[0m\\r\\nTarget information:\\r\\n ID: ttcp_1234567890\\r\\n Scope ID: p_1234567890\\r\\n Version: 2\\r\\n Type: tcp\\r\\n Name: Generated target\\r\\n Description: Provides an initial target in Boundary\\r\\n Authorized Actions:\\r\\n no-op\\r\\n read\\r\\n update\\r\\n delete\\r\\n add-host-sources\\r\\n set-host-sources\\r\\n remove-host-sources\\r\\n add-credential-sources\\r\\n set-credential-sources\\r\\n remove-credential-sources\\r\\n authorize-session\\u001b[0m\\r\\n"]
|
||||
[18.692528, "o", "\\u001b[?2004h\\u001b[1;32m[\\u001b]0;johan@nixos: ~/src/hashicorp/boundary-enterprise\\u0007johan@nixos:~/src/hashicorp/boundary-enterprise]$\\u001b[0m "]
|
||||
[20.083966, "o", "e"]
|
||||
[20.37301, "o", "x"]
|
||||
[20.51445, "o", "i"]
|
||||
[20.612885, "o", "t"]
|
||||
[21.303896, "o", "\\r\\n"]
|
||||
[21.307902, "o", "\\u001b[?2004l\\rexit\\r\\n"]`;
|
||||
|
||||
export const asciicasts = [asciicast1, asciicast2];
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
import factory from '../generated/factories/channel-recording';
|
||||
import generateId from '../helpers/id';
|
||||
import { faker } from '@faker-js/faker';
|
||||
|
||||
export default factory.extend({
|
||||
id: () => generateId('chr_'),
|
||||
start_time(i) {
|
||||
return i % 3 === 2 ? null : faker.date.recent(3, this.end_time);
|
||||
},
|
||||
end_time: (i) => (i % 3 === 2 ? null : faker.date.recent()),
|
||||
duration: (i) => (i % 3 === 2 ? null : `${faker.datatype.number()}s`),
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
import factory from '../generated/factories/connection-recording';
|
||||
import generateId from '../helpers/id';
|
||||
import { faker } from '@faker-js/faker';
|
||||
|
||||
export default factory.extend({
|
||||
id: () => generateId('cr_'),
|
||||
connection_id: () => generateId('sc_'),
|
||||
start_time(i) {
|
||||
return i % 3 === 2 ? null : faker.date.recent(3, this.end_time);
|
||||
},
|
||||
end_time: (i) => (i % 3 === 2 ? null : faker.date.recent()),
|
||||
duration: (i) => (i % 3 === 2 ? null : `${faker.datatype.number()}s`),
|
||||
});
|
||||
@@ -37,6 +37,12 @@ export default factory.extend({
|
||||
collectionActions.workers = ['create:worker-led', 'list'];
|
||||
}
|
||||
|
||||
// Session recording resources are only available on global or org scope
|
||||
if (this.type === 'global' || this.type === 'org') {
|
||||
collectionActions['storage-buckets'] = ['create', 'list'];
|
||||
collectionActions['session-recordings'] = ['list'];
|
||||
}
|
||||
|
||||
return collectionActions;
|
||||
},
|
||||
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
import factory from '../generated/factories/session-recording';
|
||||
import permissions from '../helpers/permissions';
|
||||
import generateId from '../helpers/id';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { trait } from 'miragejs';
|
||||
import {
|
||||
TYPES_SESSION_RECORDING as types,
|
||||
STATE_SESSION_RECORDING_STARTED,
|
||||
STATE_SESSION_RECORDING_AVAILABLE,
|
||||
STATE_SESSION_RECORDING_UNKNOWN,
|
||||
} from 'api/models/session-recording';
|
||||
|
||||
const states = [
|
||||
STATE_SESSION_RECORDING_AVAILABLE,
|
||||
STATE_SESSION_RECORDING_STARTED,
|
||||
STATE_SESSION_RECORDING_UNKNOWN,
|
||||
];
|
||||
const pickRandomElement = (arr) => faker.helpers.arrayElement(arr);
|
||||
export default factory.extend({
|
||||
id: () => generateId('sr_'),
|
||||
|
||||
// Cycle through available types
|
||||
type: (i) => types[i % types.length],
|
||||
state: (i) => states[i % states.length],
|
||||
start_time() {
|
||||
return this.state === STATE_SESSION_RECORDING_AVAILABLE
|
||||
? faker.date.recent(3, this.end_time)
|
||||
: null;
|
||||
},
|
||||
end_time() {
|
||||
return this.state === STATE_SESSION_RECORDING_AVAILABLE
|
||||
? faker.date.recent(1, this.updated_time)
|
||||
: null;
|
||||
},
|
||||
duration() {
|
||||
return this.state === STATE_SESSION_RECORDING_AVAILABLE
|
||||
? `${faker.datatype.number()}s`
|
||||
: null;
|
||||
},
|
||||
|
||||
authorized_actions: () =>
|
||||
permissions.authorizedActionsFor('session-recording') || [
|
||||
'no-op',
|
||||
'read',
|
||||
'download',
|
||||
],
|
||||
|
||||
withNonExistingUserAndTarget: trait({
|
||||
afterCreate(sessionRecording, server) {
|
||||
const user = pickRandomElement(server.schema.users.all().models);
|
||||
user.id = 'deleted_user';
|
||||
const session = pickRandomElement(server.schema.sessions.all().models);
|
||||
session.id = 'deleted_session';
|
||||
const target = pickRandomElement(
|
||||
server.schema.targets.where((target) => target.storage_bucket_id).models
|
||||
);
|
||||
target.id = 'deleted_target';
|
||||
|
||||
sessionRecording.update({
|
||||
sessionId: session.id,
|
||||
storage_bucket_id: target.storage_bucket_id,
|
||||
create_time_values: {
|
||||
user: user.attrs,
|
||||
target: target.attrs,
|
||||
},
|
||||
});
|
||||
},
|
||||
}),
|
||||
|
||||
withExistingUserAndTarget: trait({
|
||||
afterCreate(sessionRecording, server) {
|
||||
const user = pickRandomElement(server.schema.users.all().models);
|
||||
const session = pickRandomElement(server.schema.sessions.all().models);
|
||||
const target = pickRandomElement(
|
||||
server.schema.targets.where((target) => target.storage_bucket_id).models
|
||||
);
|
||||
|
||||
sessionRecording.update({
|
||||
sessionId: session.id,
|
||||
storage_bucket_id: target.storage_bucket_id,
|
||||
create_time_values: {
|
||||
user: user.attrs,
|
||||
target: target.attrs,
|
||||
},
|
||||
});
|
||||
},
|
||||
}),
|
||||
|
||||
withConnectionAndChannels: trait({
|
||||
afterCreate(session_recording, server) {
|
||||
server
|
||||
.createList('connection-recording', 3, { session_recording })
|
||||
.forEach((connection_recording) => {
|
||||
server.createList('channel-recording', 3, { connection_recording });
|
||||
});
|
||||
},
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
import factory from '../generated/factories/storage-bucket';
|
||||
import permissions from '../helpers/permissions';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import generateId from '../helpers/id';
|
||||
import {
|
||||
TYPES_STORAGE_BUCKET as types,
|
||||
TYPES_STORAGE_BUCKET_PLUGIN,
|
||||
TYPE_STORAGE_BUCKET_PLUGIN_AWS_S3,
|
||||
} from 'api/models/storage-bucket';
|
||||
|
||||
// Represents known plugin types, except "foobar" which models the possibility
|
||||
// of receiving an _unknown_ type, which the UI must handle gracefully.
|
||||
const pluginTypes = [...TYPES_STORAGE_BUCKET_PLUGIN, 'foobar'];
|
||||
|
||||
export default factory.extend({
|
||||
id: () => generateId('sb_'),
|
||||
|
||||
// Cycle through available types
|
||||
type: (i) => types[i % types.length],
|
||||
|
||||
authorized_actions: () =>
|
||||
permissions.authorizedActionsFor('storage-bucket') || [
|
||||
'no-op',
|
||||
'read',
|
||||
'update',
|
||||
'delete',
|
||||
],
|
||||
|
||||
plugin: function (i) {
|
||||
if (this.type === 'plugin') {
|
||||
return {
|
||||
id: `plugin-id-${i}`,
|
||||
name: pluginTypes[i % pluginTypes.length],
|
||||
description: faker.random.words(),
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
attributes() {
|
||||
switch (this.plugin?.name) {
|
||||
case TYPE_STORAGE_BUCKET_PLUGIN_AWS_S3:
|
||||
return {
|
||||
region: `us-${faker.address.cardinalDirection()}-${faker.datatype.number(
|
||||
{
|
||||
max: 9,
|
||||
}
|
||||
)}`,
|
||||
disable_credential_rotation: faker.datatype.boolean(),
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -8,7 +8,7 @@ import { trait } from 'miragejs';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import permissions from '../helpers/permissions';
|
||||
import generateId from '../helpers/id';
|
||||
import { TYPES_TARGET } from 'api/models/target';
|
||||
import { TYPES_TARGET, TYPE_TARGET_SSH } from 'api/models/target';
|
||||
|
||||
const randomBoolean = (chance = 0.5) => Math.random() < chance;
|
||||
const hostSetChance = 0.3;
|
||||
@@ -26,6 +26,8 @@ export default factory.extend({
|
||||
'remove-host-sources',
|
||||
'add-credential-sources',
|
||||
'remove-credential-sources',
|
||||
'set-storage-bucket',
|
||||
'remove-storage-bucket',
|
||||
],
|
||||
id: () => generateId('t_'),
|
||||
|
||||
@@ -66,6 +68,18 @@ export default factory.extend({
|
||||
.where((credential) => credential.scopeId === target.scope.id)
|
||||
.models.filter(() => randomBoolean())
|
||||
.map((cred) => cred.id);
|
||||
|
||||
const randomlySelectedStorageBucket = faker.helpers.arrayElement(
|
||||
server.schema.storageBuckets.all().models
|
||||
);
|
||||
|
||||
if (randomlySelectedStorageBucket && target.type === TYPE_TARGET_SSH) {
|
||||
target.update({
|
||||
storage_bucket_id: randomlySelectedStorageBucket.id,
|
||||
enable_session_recording: true,
|
||||
});
|
||||
}
|
||||
|
||||
target.update({
|
||||
hostSets: randomlySelectedHostSets,
|
||||
brokeredCredentialSourceIds: [
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
import { Factory } from 'miragejs';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { MIME_TYPE_ASCIICAST } from 'api/models/channel-recording';
|
||||
|
||||
/**
|
||||
* GeneratedChannelRecordingModelFactory
|
||||
*/
|
||||
export default Factory.extend({
|
||||
bytes_up: () =>
|
||||
faker.random.numeric(faker.datatype.number({ min: 1, max: 12 })),
|
||||
bytes_down: () =>
|
||||
faker.random.numeric(faker.datatype.number({ min: 1, max: 12 })),
|
||||
errors: () => faker.datatype.number({ max: 10 }),
|
||||
mime_types: () => [MIME_TYPE_ASCIICAST],
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
import { Factory } from 'miragejs';
|
||||
import { faker } from '@faker-js/faker';
|
||||
|
||||
/**
|
||||
* GeneratedConnectionRecordingModelFactory
|
||||
*/
|
||||
export default Factory.extend({
|
||||
bytes_up: () =>
|
||||
faker.random.numeric(faker.datatype.number({ min: 1, max: 12 })),
|
||||
bytes_down: () =>
|
||||
faker.random.numeric(faker.datatype.number({ min: 1, max: 12 })),
|
||||
errors: () => faker.datatype.number({ max: 10 }),
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
import { Factory } from 'miragejs';
|
||||
import { faker } from '@faker-js/faker';
|
||||
|
||||
/**
|
||||
* GeneratedSessionRecordingModelFactory
|
||||
*/
|
||||
export default Factory.extend({
|
||||
bytes_up: () =>
|
||||
faker.random.numeric(faker.datatype.number({ min: 1, max: 12 })),
|
||||
bytes_down: () =>
|
||||
faker.random.numeric(faker.datatype.number({ min: 1, max: 12 })),
|
||||
errors: () => faker.datatype.number({ max: 10 }),
|
||||
created_time: () => faker.date.recent(),
|
||||
updated_time: () => faker.date.recent(),
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
import { Factory } from 'miragejs';
|
||||
import { faker } from '@faker-js/faker';
|
||||
|
||||
/**
|
||||
* GeneratedStorageBucketModelFactory
|
||||
*/
|
||||
export default Factory.extend({
|
||||
name: () => faker.random.words(),
|
||||
description: () => faker.random.words(),
|
||||
created_time: () => faker.date.recent(),
|
||||
updated_time: () => faker.date.recent(),
|
||||
version: () => faker.datatype.number(),
|
||||
bucket_name: () => faker.internet.domainWord(),
|
||||
bucket_prefix: () => faker.system.directoryPath(),
|
||||
worker_filter: () => faker.random.words(),
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
import { belongsTo, Model } from 'miragejs';
|
||||
|
||||
export default Model.extend({
|
||||
connection_recording: belongsTo(),
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
import { belongsTo, hasMany, Model } from 'miragejs';
|
||||
|
||||
export default Model.extend({
|
||||
session_recording: belongsTo(),
|
||||
channel_recordings: hasMany(),
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
import { belongsTo, hasMany, Model } from 'miragejs';
|
||||
|
||||
export default Model.extend({
|
||||
scope: belongsTo(),
|
||||
connection_recordings: hasMany(),
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
import { belongsTo } from 'miragejs';
|
||||
import Model from './base';
|
||||
export default Model.extend({
|
||||
scope: belongsTo(),
|
||||
});
|
||||
@@ -100,5 +100,6 @@ export function targetHandler({ targets }, { params: { idMethod } }) {
|
||||
removedCredentialSources;
|
||||
}
|
||||
}
|
||||
|
||||
return target.update(updatedAttrs);
|
||||
}
|
||||
@@ -94,6 +94,10 @@ export default function (server) {
|
||||
role.update({ managedGroups });
|
||||
});
|
||||
|
||||
// Storage Buckets
|
||||
server.createList('storage-bucket', 3, { scope: globalScope });
|
||||
server.createList('storage-bucket', 2, { scope: orgScope });
|
||||
|
||||
// Other resources
|
||||
server.schema.scopes.where({ type: 'project' }).models.forEach((scope) => {
|
||||
server.createList('host-catalog', 8, { scope }, 'withChildren');
|
||||
@@ -109,4 +113,26 @@ export default function (server) {
|
||||
|
||||
// Workers
|
||||
server.createList('worker', 3, { scope: globalScope });
|
||||
|
||||
// Session Recordings
|
||||
server.createList(
|
||||
'session-recording',
|
||||
3,
|
||||
{ scope: globalScope },
|
||||
'withConnectionAndChannels',
|
||||
'withExistingUserAndTarget'
|
||||
);
|
||||
server.create(
|
||||
'session-recording',
|
||||
{ scope: globalScope },
|
||||
'withConnectionAndChannels',
|
||||
'withNonExistingUserAndTarget'
|
||||
);
|
||||
server.createList(
|
||||
'session-recording',
|
||||
2,
|
||||
{ scope: orgScope },
|
||||
'withConnectionAndChannels',
|
||||
'withExistingUserAndTarget'
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
import ApplicationSerializer from './application';
|
||||
|
||||
export default ApplicationSerializer.extend({
|
||||
modelName: 'channel-recording',
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
import ApplicationSerializer from './application';
|
||||
import ChannelSerializer from './channel-recording';
|
||||
|
||||
export default ApplicationSerializer.extend({
|
||||
modelName: 'connection-recording',
|
||||
_hashForModel(model) {
|
||||
const json = ApplicationSerializer.prototype._hashForModel.apply(
|
||||
this,
|
||||
arguments
|
||||
);
|
||||
const { channelRecordings } = this.schema;
|
||||
if (model.channelRecordingIds) {
|
||||
json.channel_recordings = model.channelRecordingIds.map((id) => {
|
||||
const channel = channelRecordings.find(id);
|
||||
return ChannelSerializer.prototype._hashForModel.apply(this, [channel]);
|
||||
});
|
||||
}
|
||||
return json;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
import ApplicationSerializer from './application';
|
||||
import ConnectionRecordingSerializer from './connection-recording';
|
||||
import ScopeSerializer from './scope';
|
||||
|
||||
export default ApplicationSerializer.extend({
|
||||
modelName: 'session-recording',
|
||||
_hashForModel(model) {
|
||||
const json = ApplicationSerializer.prototype._hashForModel.apply(
|
||||
this,
|
||||
arguments
|
||||
);
|
||||
const { connectionRecordings, scopes } = this.schema;
|
||||
|
||||
// Manually map our models to the actual embedded json representation
|
||||
if (model.connectionRecordingIds) {
|
||||
json.connection_recordings = model.connectionRecordingIds.map((id) => {
|
||||
const connection = connectionRecordings.find(id);
|
||||
return ConnectionRecordingSerializer.prototype._hashForModel.apply(
|
||||
this,
|
||||
[connection]
|
||||
);
|
||||
});
|
||||
}
|
||||
const userScopeId = model.create_time_values?.user?.scopeId;
|
||||
if (userScopeId) {
|
||||
const scope = scopes.find(userScopeId);
|
||||
json.create_time_values.user.scope =
|
||||
ScopeSerializer.prototype._hashForModel.apply(this, [scope]);
|
||||
}
|
||||
const targetScopeId = model.create_time_values?.target?.scopeId;
|
||||
if (targetScopeId) {
|
||||
const scope = scopes.find(targetScopeId);
|
||||
json.create_time_values.target.scope =
|
||||
ScopeSerializer.prototype._hashForModel.apply(this, [scope]);
|
||||
}
|
||||
delete json.create_time_values?.target?.storage_bucket_id;
|
||||
|
||||
json.session_id = model.sessionId;
|
||||
return json;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
import ApplicationSerializer from './application';
|
||||
export default ApplicationSerializer.extend({
|
||||
modelName: 'storage-bucket',
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
import { MIME_TYPE_ASCIICAST } from 'api/models/channel-recording';
|
||||
import { TYPE_SESSION_RECORDING_SSH } from 'api/models/session-recording';
|
||||
|
||||
module('Unit | Ability | channel-recording', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
let canService;
|
||||
let store;
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
canService = this.owner.lookup('service:can');
|
||||
store = this.owner.lookup('service:store');
|
||||
});
|
||||
|
||||
test('it exists', function (assert) {
|
||||
const ability = this.owner.lookup('ability:channel-recording');
|
||||
assert.ok(ability);
|
||||
});
|
||||
|
||||
test('it can download asciicast', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const sessionRecordingModel = store.createRecord('session-recording', {
|
||||
authorized_actions: ['download'],
|
||||
type: TYPE_SESSION_RECORDING_SSH,
|
||||
});
|
||||
const connectionRecordingModel = store.createRecord(
|
||||
'connection-recording',
|
||||
{
|
||||
session_recording: sessionRecordingModel,
|
||||
}
|
||||
);
|
||||
const channelRecordingModel = store.createRecord('channel-recording', {
|
||||
mime_types: [MIME_TYPE_ASCIICAST],
|
||||
connection_recording: connectionRecordingModel,
|
||||
});
|
||||
|
||||
assert.true(
|
||||
canService.can('getAsciicast channel-recording', channelRecordingModel)
|
||||
);
|
||||
});
|
||||
|
||||
test('it cannot download asciicast without the correct mime type', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const sessionRecordingModel = store.createRecord('session-recording', {
|
||||
authorized_actions: ['download'],
|
||||
type: TYPE_SESSION_RECORDING_SSH,
|
||||
});
|
||||
const connectionRecordingModel = store.createRecord(
|
||||
'connection-recording',
|
||||
{
|
||||
session_recording: sessionRecordingModel,
|
||||
}
|
||||
);
|
||||
const channelRecordingModel = store.createRecord('channel-recording', {
|
||||
mime_types: ['unknown'],
|
||||
connection_recording: connectionRecordingModel,
|
||||
});
|
||||
|
||||
assert.false(
|
||||
canService.can('getAsciicast channel-recording', channelRecordingModel)
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
|
||||
module('Unit | Ability | session-recording', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
let canService;
|
||||
let store;
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
canService = this.owner.lookup('service:can');
|
||||
store = this.owner.lookup('service:store');
|
||||
});
|
||||
|
||||
test('it exists', function (assert) {
|
||||
const ability = this.owner.lookup('ability:session-recording');
|
||||
assert.ok(ability);
|
||||
});
|
||||
|
||||
test('can download when a recording has download authorization', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
const recordingWithAuthorizedAction = store.createRecord(
|
||||
'session-recording',
|
||||
{
|
||||
authorized_actions: ['download'],
|
||||
}
|
||||
);
|
||||
const recordingWithoutAuthorizedAction = store.createRecord(
|
||||
'session-recording',
|
||||
{
|
||||
authorized_actions: [],
|
||||
}
|
||||
);
|
||||
|
||||
assert.true(
|
||||
canService.can(
|
||||
'download session-recording',
|
||||
recordingWithAuthorizedAction
|
||||
)
|
||||
);
|
||||
assert.false(
|
||||
canService.can(
|
||||
'download session-recording',
|
||||
recordingWithoutAuthorizedAction
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -9,74 +9,90 @@ import { setupTest } from 'ember-qunit';
|
||||
module('Unit | Abilities | Target', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
let canService;
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
canService = this.owner.lookup('service:can');
|
||||
});
|
||||
|
||||
test('it reflects when a given target may connect based on authorized_actions', function (assert) {
|
||||
assert.expect(2);
|
||||
const service = this.owner.lookup('service:can');
|
||||
const model = {
|
||||
authorized_actions: ['authorize-session'],
|
||||
};
|
||||
assert.ok(service.can('connect target', model));
|
||||
assert.true(canService.can('connect target', model));
|
||||
model.authorized_actions = [];
|
||||
assert.notOk(service.can('connect target', model));
|
||||
assert.false(canService.can('connect target', model));
|
||||
});
|
||||
|
||||
test('it reflects when a given target may add host sources', function (assert) {
|
||||
assert.expect(2);
|
||||
const service = this.owner.lookup('service:can');
|
||||
const model = {
|
||||
authorized_actions: ['add-host-sources'],
|
||||
};
|
||||
assert.ok(service.can('addHostSources target', model));
|
||||
assert.true(canService.can('addHostSources target', model));
|
||||
model.authorized_actions = [];
|
||||
assert.notOk(service.can('addHostSources target', model));
|
||||
assert.false(canService.can('addHostSources target', model));
|
||||
});
|
||||
|
||||
test('it reflects when a given target may remove host sources', function (assert) {
|
||||
assert.expect(2);
|
||||
const service = this.owner.lookup('service:can');
|
||||
const model = {
|
||||
authorized_actions: ['remove-host-sources'],
|
||||
};
|
||||
assert.ok(service.can('removeHostSources target', model));
|
||||
assert.true(canService.can('removeHostSources target', model));
|
||||
model.authorized_actions = [];
|
||||
assert.notOk(service.can('removeHostSources target', model));
|
||||
assert.false(canService.can('removeHostSources target', model));
|
||||
});
|
||||
|
||||
test('it reflects when a given target may add brokered credential sources', function (assert) {
|
||||
assert.expect(2);
|
||||
const service = this.owner.lookup('service:can');
|
||||
const model = {
|
||||
authorized_actions: ['add-credential-sources'],
|
||||
};
|
||||
assert.ok(service.can('addBrokeredCredentialSources target', model));
|
||||
assert.true(canService.can('addBrokeredCredentialSources target', model));
|
||||
model.authorized_actions = [];
|
||||
assert.notOk(service.can('addBrokeredCredentialSources target', model));
|
||||
assert.false(canService.can('addBrokeredCredentialSources target', model));
|
||||
});
|
||||
|
||||
test('it reflects when a given target may remove credential sources', function (assert) {
|
||||
assert.expect(2);
|
||||
const service = this.owner.lookup('service:can');
|
||||
const model = {
|
||||
authorized_actions: ['remove-credential-sources'],
|
||||
};
|
||||
assert.ok(service.can('removeCredentialSources target', model));
|
||||
assert.true(canService.can('removeCredentialSources target', model));
|
||||
model.authorized_actions = [];
|
||||
assert.notOk(service.can('removeCredentialSources target', model));
|
||||
assert.false(canService.can('removeCredentialSources target', model));
|
||||
});
|
||||
|
||||
test('it reflects when a given ssh target may add injected application credential sources', function (assert) {
|
||||
assert.expect(2);
|
||||
const service = this.owner.lookup('service:can');
|
||||
assert.expect(3);
|
||||
const model = {
|
||||
authorized_actions: ['add-credential-sources'],
|
||||
isSSH: true,
|
||||
};
|
||||
assert.ok(
|
||||
service.can('addInjectedApplicationCredentialSources target', model)
|
||||
const modelWithoutAuthorizedActions = {
|
||||
authorized_actions: [],
|
||||
isSSH: true,
|
||||
};
|
||||
const modelWithoutSSHType = {
|
||||
authorized_actions: ['add-credential-sources'],
|
||||
};
|
||||
|
||||
assert.true(
|
||||
canService.can('addInjectedApplicationCredentialSources target', model)
|
||||
);
|
||||
model.authorized_actions = [];
|
||||
assert.notOk(
|
||||
service.can('addInjectedApplicationCredentialSources target', model)
|
||||
assert.false(
|
||||
canService.can(
|
||||
'addInjectedApplicationCredentialSources target',
|
||||
modelWithoutAuthorizedActions
|
||||
)
|
||||
);
|
||||
assert.false(
|
||||
canService.can(
|
||||
'addInjectedApplicationCredentialSources target',
|
||||
modelWithoutSSHType
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
|
||||
module('Unit | Adapter | session recording', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test('it generates correct URLs for downloading asciicast', function (assert) {
|
||||
assert.expect(1);
|
||||
const mockSnapshot = {
|
||||
adapterOptions: {
|
||||
method: 'download',
|
||||
},
|
||||
};
|
||||
const queryObject = {
|
||||
mime_type: 'mimeType',
|
||||
};
|
||||
const adapter = this.owner.lookup('adapter:session-recording');
|
||||
const createRecordURL = adapter.buildURL(
|
||||
'session-recording',
|
||||
'sr_id',
|
||||
mockSnapshot,
|
||||
'findRecord',
|
||||
queryObject
|
||||
);
|
||||
assert.strictEqual(
|
||||
createRecordURL,
|
||||
'/v1/session-recordings/sr_id:download?mime_type=mimeType'
|
||||
);
|
||||
});
|
||||
|
||||
test('it generates correct URLs for retrieving session-recordings', function (assert) {
|
||||
assert.expect(1);
|
||||
const adapter = this.owner.lookup('adapter:session-recording');
|
||||
const createRecordURL = adapter.buildURL(
|
||||
'session-recording',
|
||||
'sr_id',
|
||||
{},
|
||||
'findRecord'
|
||||
);
|
||||
assert.strictEqual(createRecordURL, '/v1/session-recordings/sr_id');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
|
||||
module('Unit | Adapter | storage bucket', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test('it generates correct createRecord URLs for storage buckets with extra query parameter ?plugin_name', function (assert) {
|
||||
assert.expect(1);
|
||||
const scopeID = 'o_1';
|
||||
const mockSnapshot = {
|
||||
adapterOptions: {
|
||||
scopeID,
|
||||
},
|
||||
attr() {
|
||||
return { name: 'aws_s3' };
|
||||
},
|
||||
};
|
||||
const adapter = this.owner.lookup('adapter:storage-bucket');
|
||||
const createRecordURL = adapter.buildURL(
|
||||
'storage-bucket',
|
||||
null,
|
||||
mockSnapshot,
|
||||
'createRecord'
|
||||
);
|
||||
assert.strictEqual(
|
||||
createRecordURL,
|
||||
'/v1/storage-buckets?plugin_name=aws_s3'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
import { MIME_TYPE_ASCIICAST } from 'api/models/channel-recording';
|
||||
|
||||
module('Unit | Model | channel-recording', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test('it exists', function (assert) {
|
||||
let store = this.owner.lookup('service:store');
|
||||
let model = store.createRecord('channel-recording', {});
|
||||
assert.ok(model);
|
||||
});
|
||||
|
||||
test('it has isAsciicast property and returns the expected values', async function (assert) {
|
||||
assert.expect(2);
|
||||
const store = this.owner.lookup('service:store');
|
||||
const modelA = store.createRecord('channel-recording', {
|
||||
mime_types: [MIME_TYPE_ASCIICAST],
|
||||
});
|
||||
const modelB = store.createRecord('channel-recording', {
|
||||
mime_types: ['random'],
|
||||
});
|
||||
assert.true(modelA.isAsciicast);
|
||||
assert.false(modelB.isAsciicast);
|
||||
});
|
||||
|
||||
test('it has isAsciicast property and returns the expected values when mime_types are not present', async function (assert) {
|
||||
assert.expect(2);
|
||||
const store = this.owner.lookup('service:store');
|
||||
const modelA = store.createRecord('channel-recording', {
|
||||
mime_types: undefined,
|
||||
});
|
||||
const modelB = store.createRecord('channel-recording', {
|
||||
mime_types: [],
|
||||
});
|
||||
assert.false(modelA.isAsciicast);
|
||||
assert.false(modelB.isAsciicast);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
|
||||
module('Unit | Model | connection-recording', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test('it exists', function (assert) {
|
||||
let store = this.owner.lookup('service:store');
|
||||
let model = store.createRecord('connection-recording', {});
|
||||
assert.ok(model);
|
||||
});
|
||||
|
||||
test('it has channels as a relationship', async function (assert) {
|
||||
assert.expect(1);
|
||||
const store = this.owner.lookup('service:store');
|
||||
const model = store.createRecord('connection-recording', {
|
||||
channel_recordings: [store.createRecord('channel-recording')],
|
||||
});
|
||||
|
||||
const channel_recordings = await model.channel_recordings;
|
||||
assert.strictEqual(channel_recordings.length, 1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,132 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
import { TYPE_SESSION_RECORDING_SSH } from 'api/models/session-recording';
|
||||
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
|
||||
import {
|
||||
STATE_SESSION_RECORDING_AVAILABLE,
|
||||
STATE_SESSION_RECORDING_STARTED,
|
||||
STATE_SESSION_RECORDING_UNKNOWN,
|
||||
} from 'api/models/session-recording';
|
||||
|
||||
module('Unit | Model | session-recording', function (hooks) {
|
||||
setupTest(hooks);
|
||||
setupMirage(hooks);
|
||||
|
||||
test('it exists', function (assert) {
|
||||
let store = this.owner.lookup('service:store');
|
||||
let model = store.createRecord('session-recording', {});
|
||||
assert.ok(model);
|
||||
});
|
||||
|
||||
test('it has isUnknown property and returns the expected values', async function (assert) {
|
||||
assert.expect(2);
|
||||
const store = this.owner.lookup('service:store');
|
||||
const modelA = store.createRecord('session-recording', {
|
||||
type: TYPE_SESSION_RECORDING_SSH,
|
||||
});
|
||||
const modelB = store.createRecord('session-recording', {
|
||||
type: 'no-such-type',
|
||||
});
|
||||
assert.false(modelA.isUnknown);
|
||||
assert.true(modelB.isUnknown);
|
||||
});
|
||||
|
||||
test('it has isSSH property and returns the expected values', async function (assert) {
|
||||
assert.expect(2);
|
||||
const store = this.owner.lookup('service:store');
|
||||
const modelA = store.createRecord('session-recording', {
|
||||
type: TYPE_SESSION_RECORDING_SSH,
|
||||
});
|
||||
const modelB = store.createRecord('session-recording', {
|
||||
type: 'random',
|
||||
});
|
||||
assert.true(modelA.isSSH);
|
||||
assert.false(modelB.isSSH);
|
||||
});
|
||||
|
||||
test('it has isAvailable property and returns the expected values', async function (assert) {
|
||||
assert.expect(3);
|
||||
const store = this.owner.lookup('service:store');
|
||||
const modelA = store.createRecord('session-recording', {
|
||||
state: STATE_SESSION_RECORDING_AVAILABLE,
|
||||
});
|
||||
const modelB = store.createRecord('session-recording', {
|
||||
state: STATE_SESSION_RECORDING_STARTED,
|
||||
});
|
||||
const modelC = store.createRecord('session-recording', {
|
||||
state: STATE_SESSION_RECORDING_UNKNOWN,
|
||||
});
|
||||
assert.true(modelA.isAvailable);
|
||||
assert.false(modelB.isAvailable);
|
||||
assert.false(modelC.isAvailable);
|
||||
});
|
||||
|
||||
test('it has connections as a relationship', async function (assert) {
|
||||
assert.expect(1);
|
||||
const store = this.owner.lookup('service:store');
|
||||
const model = store.createRecord('session-recording', {
|
||||
type: TYPE_SESSION_RECORDING_SSH,
|
||||
connection_recordings: [store.createRecord('connection-recording')],
|
||||
});
|
||||
|
||||
const connection_recordings = await model.connection_recordings;
|
||||
assert.strictEqual(connection_recordings.length, 1);
|
||||
});
|
||||
|
||||
test('it has targetScopeDisplayName and returns the expected values', async function (assert) {
|
||||
assert.expect(2);
|
||||
const store = this.owner.lookup('service:store');
|
||||
const fakeName = 'Scope name';
|
||||
const fakeId = 's_123456789';
|
||||
|
||||
const modelA = store.createRecord('session-recording', {
|
||||
create_time_values: {
|
||||
target: {
|
||||
scope: {
|
||||
name: fakeName,
|
||||
id: fakeId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const modelB = store.createRecord('session-recording', {
|
||||
create_time_values: {
|
||||
target: {
|
||||
scope: {
|
||||
id: fakeId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(modelA.targetScopeDisplayName, fakeName);
|
||||
assert.strictEqual(modelB.targetScopeDisplayName, fakeId);
|
||||
});
|
||||
|
||||
test('it has userDisplayName and returns the expected values', async function (assert) {
|
||||
assert.expect(2);
|
||||
const store = this.owner.lookup('service:store');
|
||||
const fakeUserName = 'User name';
|
||||
const fakeUserId = 'userid';
|
||||
|
||||
const userModelA = store.createRecord('session-recording', {
|
||||
create_time_values: {
|
||||
user: {
|
||||
name: fakeUserName,
|
||||
id: fakeUserId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const userModelB = store.createRecord('session-recording', {
|
||||
create_time_values: {
|
||||
user: {
|
||||
id: fakeUserId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(userModelA.userDisplayName, fakeUserName);
|
||||
assert.strictEqual(userModelB.userDisplayName, fakeUserId);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,80 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
import { TYPE_STORAGE_BUCKET_PLUGIN_AWS_S3 } from 'api/models/storage-bucket';
|
||||
|
||||
module('Unit | Model | storage bucket', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test('it exists', function (assert) {
|
||||
let store = this.owner.lookup('service:store');
|
||||
let model = store.createRecord('storage-bucket', {});
|
||||
assert.ok(model);
|
||||
});
|
||||
|
||||
test('it has the isPlugin property and returns the expected values', async function (assert) {
|
||||
assert.expect(1);
|
||||
const store = this.owner.lookup('service:store');
|
||||
const modelPlugin = store.createRecord('storage-bucket', {
|
||||
type: 'plugin',
|
||||
plugin: {
|
||||
name: TYPE_STORAGE_BUCKET_PLUGIN_AWS_S3,
|
||||
},
|
||||
});
|
||||
assert.true(modelPlugin.isPlugin);
|
||||
});
|
||||
|
||||
test('it has isUnknown property and returns the expected values', async function (assert) {
|
||||
assert.expect(2);
|
||||
const store = this.owner.lookup('service:store');
|
||||
const modelA = store.createRecord('storage-bucket', {
|
||||
compositeType: TYPE_STORAGE_BUCKET_PLUGIN_AWS_S3,
|
||||
});
|
||||
const modelB = store.createRecord('storage-bucket', {
|
||||
compositeType: 'no-such-type',
|
||||
});
|
||||
assert.false(modelA.isUnknown);
|
||||
assert.true(modelB.isUnknown);
|
||||
});
|
||||
|
||||
test('it has isAWS property and returns the expected values', async function (assert) {
|
||||
assert.expect(2);
|
||||
const store = this.owner.lookup('service:store');
|
||||
const modelAws = store.createRecord('storage-bucket', {
|
||||
type: 'plugin',
|
||||
plugin: { name: TYPE_STORAGE_BUCKET_PLUGIN_AWS_S3 },
|
||||
});
|
||||
const modelRandom = store.createRecord('storage-bucket', {
|
||||
plugin: { name: 'random' },
|
||||
});
|
||||
assert.true(modelAws.isAWS);
|
||||
assert.false(modelRandom.isAWS);
|
||||
});
|
||||
|
||||
test('get compositeType returns expected values', async function (assert) {
|
||||
assert.expect(2);
|
||||
const store = this.owner.lookup('service:store');
|
||||
const modelA = store.createRecord('storage-bucket', {
|
||||
type: 'plugin',
|
||||
plugin: { name: TYPE_STORAGE_BUCKET_PLUGIN_AWS_S3 },
|
||||
});
|
||||
const modelB = store.createRecord('storage-bucket', {
|
||||
type: 'plugin',
|
||||
plugin: { name: 'no-such-type' },
|
||||
});
|
||||
assert.strictEqual(modelA.compositeType, TYPE_STORAGE_BUCKET_PLUGIN_AWS_S3);
|
||||
assert.strictEqual(modelB.compositeType, 'unknown');
|
||||
});
|
||||
|
||||
test('set compositeType sets expected values', async function (assert) {
|
||||
assert.expect(2);
|
||||
const store = this.owner.lookup('service:store');
|
||||
const modelPlugin = store.createRecord('storage-bucket', {
|
||||
compositeType: TYPE_STORAGE_BUCKET_PLUGIN_AWS_S3,
|
||||
});
|
||||
assert.strictEqual(modelPlugin.type, 'plugin');
|
||||
assert.strictEqual(
|
||||
modelPlugin.plugin.name,
|
||||
TYPE_STORAGE_BUCKET_PLUGIN_AWS_S3
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
|
||||
module('Unit | Serializer | connection-recording-recording', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let store = this.owner.lookup('service:store');
|
||||
let serializer = store.serializerFor('connection-recording');
|
||||
|
||||
assert.ok(serializer);
|
||||
});
|
||||
|
||||
test('it serializes records', function (assert) {
|
||||
let store = this.owner.lookup('service:store');
|
||||
let record = store.createRecord('connection-recording', {});
|
||||
|
||||
let serializedRecord = record.serialize();
|
||||
|
||||
assert.ok(serializedRecord);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
|
||||
module('Unit | Serializer | session-recording', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let store = this.owner.lookup('service:store');
|
||||
let serializer = store.serializerFor('session-recording');
|
||||
|
||||
assert.ok(serializer);
|
||||
});
|
||||
|
||||
test('it serializes records', function (assert) {
|
||||
let store = this.owner.lookup('service:store');
|
||||
let record = store.createRecord('session-recording', {});
|
||||
|
||||
let serializedRecord = record.serialize();
|
||||
|
||||
assert.ok(serializedRecord);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,152 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
import {
|
||||
TYPE_STORAGE_BUCKET_PLUGIN_AWS_S3,
|
||||
TYPE_STORAGE_BUCKET_PLUGIN,
|
||||
} from 'api/models/storage-bucket';
|
||||
|
||||
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 aws plugin as expected', async function (assert) {
|
||||
assert.expect(1);
|
||||
const store = this.owner.lookup('service:store');
|
||||
const record = store.createRecord('storage-bucket', {
|
||||
compositeType: TYPE_STORAGE_BUCKET_PLUGIN_AWS_S3,
|
||||
name: 'AWS',
|
||||
description: 'this has an aws plugin',
|
||||
bucket_name: 'bucketname',
|
||||
bucket_prefix: 'bucketprefix',
|
||||
worker_filter: 'workerfilter',
|
||||
region: 'eu-west-1',
|
||||
access_key_id: 'foobars',
|
||||
secret_access_key: 'testing',
|
||||
secrets_hmac: 'hmac',
|
||||
disable_credential_rotation: true,
|
||||
});
|
||||
const expectedResult = {
|
||||
name: 'AWS',
|
||||
description: 'this has an aws plugin',
|
||||
type: TYPE_STORAGE_BUCKET_PLUGIN,
|
||||
bucket_name: 'bucketname',
|
||||
bucket_prefix: 'bucketprefix',
|
||||
worker_filter: 'workerfilter',
|
||||
attributes: {
|
||||
region: 'eu-west-1',
|
||||
disable_credential_rotation: true,
|
||||
},
|
||||
secrets: {
|
||||
access_key_id: 'foobars',
|
||||
secret_access_key: 'testing',
|
||||
},
|
||||
};
|
||||
assert.deepEqual(record.serialize(), expectedResult);
|
||||
});
|
||||
|
||||
test('it serializes when updating correctly', async function (assert) {
|
||||
assert.expect(1);
|
||||
const store = this.owner.lookup('service:store');
|
||||
const serializer = store.serializerFor('storage-bucket');
|
||||
store.push({
|
||||
data: {
|
||||
id: '1',
|
||||
type: 'storage-bucket',
|
||||
attributes: {
|
||||
type: TYPE_STORAGE_BUCKET_PLUGIN,
|
||||
compositeType: TYPE_STORAGE_BUCKET_PLUGIN_AWS_S3,
|
||||
name: 'AWS',
|
||||
description: 'this has an aws plugin',
|
||||
bucket_name: 'bucketname',
|
||||
bucket_prefix: 'bucketprefix',
|
||||
worker_filter: 'workerfilter',
|
||||
region: 'eu-west-1',
|
||||
access_key_id: 'foobars',
|
||||
secret_access_key: 'testing',
|
||||
secrets_hmac: 'hmac',
|
||||
disable_credential_rotation: true,
|
||||
version: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
const record = store.peekRecord('storage-bucket', '1');
|
||||
const snapshot = record._createSnapshot();
|
||||
const serializedRecord = serializer.serialize(snapshot);
|
||||
|
||||
const expectedResult = {
|
||||
name: 'AWS',
|
||||
description: 'this has an aws plugin',
|
||||
type: TYPE_STORAGE_BUCKET_PLUGIN,
|
||||
worker_filter: 'workerfilter',
|
||||
attributes: {
|
||||
region: 'eu-west-1',
|
||||
disable_credential_rotation: true,
|
||||
},
|
||||
secrets: {
|
||||
access_key_id: 'foobars',
|
||||
secret_access_key: 'testing',
|
||||
},
|
||||
version: 1,
|
||||
};
|
||||
assert.deepEqual(serializedRecord, expectedResult);
|
||||
});
|
||||
|
||||
test('it normalizes correctly', function (assert) {
|
||||
assert.expect(1);
|
||||
const store = this.owner.lookup('service:store');
|
||||
const serializer = store.serializerFor('storage-bucket');
|
||||
const hostSetModelClass = store.createRecord('storage-bucket').constructor;
|
||||
const payload = {
|
||||
id: '1',
|
||||
version: 1,
|
||||
name: 'AWS',
|
||||
description: 'this has an aws plugin',
|
||||
type: TYPE_STORAGE_BUCKET_PLUGIN,
|
||||
bucket_name: 'bucketname',
|
||||
bucket_prefix: 'bucketprefix',
|
||||
worker_filter: 'workerfilter',
|
||||
attributes: {
|
||||
region: 'eu-west-1',
|
||||
disable_credential_rotation: true,
|
||||
},
|
||||
secrets_hmac: 'this is secret',
|
||||
};
|
||||
const normalized = serializer.normalizeSingleResponse(
|
||||
store,
|
||||
hostSetModelClass,
|
||||
payload
|
||||
);
|
||||
|
||||
assert.deepEqual(normalized, {
|
||||
included: [],
|
||||
data: {
|
||||
id: '1',
|
||||
attributes: {
|
||||
type: TYPE_STORAGE_BUCKET_PLUGIN,
|
||||
version: 1,
|
||||
authorized_actions: [],
|
||||
name: 'AWS',
|
||||
description: 'this has an aws plugin',
|
||||
bucket_name: 'bucketname',
|
||||
bucket_prefix: 'bucketprefix',
|
||||
worker_filter: 'workerfilter',
|
||||
secrets_hmac: 'this is secret',
|
||||
region: 'eu-west-1',
|
||||
disable_credential_rotation: true,
|
||||
access_key_id: null,
|
||||
secret_access_key: null,
|
||||
},
|
||||
type: 'storage-bucket',
|
||||
relationships: {},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -53,6 +53,8 @@ module('Unit | Serializer | target', function (hooks) {
|
||||
attributes: {
|
||||
default_port: 1234,
|
||||
default_client_port: 4321,
|
||||
enable_session_recording: false,
|
||||
storage_bucket_id: null,
|
||||
},
|
||||
address: '0.0.0.0',
|
||||
});
|
||||
@@ -129,6 +131,45 @@ module('Unit | Serializer | target', function (hooks) {
|
||||
});
|
||||
});
|
||||
|
||||
test('it serializes the storage_bucket_id attribute if present', function (assert) {
|
||||
assert.expect(1);
|
||||
const store = this.owner.lookup('service:store');
|
||||
const serializer = store.serializerFor('target');
|
||||
const record = store.createRecord('target', {
|
||||
name: 'User',
|
||||
description: 'Description',
|
||||
version: 1,
|
||||
type: TYPE_TARGET_SSH,
|
||||
enable_session_recording: true,
|
||||
storage_bucket_id: 'bucketID',
|
||||
default_port: 1234,
|
||||
scope: {
|
||||
scope_id: 'org_1',
|
||||
},
|
||||
});
|
||||
const snapshot = record._createSnapshot();
|
||||
const serializedRecord = serializer.serialize(snapshot);
|
||||
assert.deepEqual(serializedRecord, {
|
||||
name: 'User',
|
||||
description: 'Description',
|
||||
version: 1,
|
||||
type: TYPE_TARGET_SSH,
|
||||
scope_id: 'org_1',
|
||||
session_max_seconds: 28800,
|
||||
session_connection_limit: null,
|
||||
worker_filter: null,
|
||||
egress_worker_filter: null,
|
||||
ingress_worker_filter: null,
|
||||
attributes: {
|
||||
default_port: 1234,
|
||||
default_client_port: null,
|
||||
enable_session_recording: true,
|
||||
storage_bucket_id: 'bucketID',
|
||||
},
|
||||
address: null,
|
||||
});
|
||||
});
|
||||
|
||||
test('it serializes the worker_filter attribute if present', function (assert) {
|
||||
assert.expect(1);
|
||||
const store = this.owner.lookup('service:store');
|
||||
@@ -160,6 +201,8 @@ module('Unit | Serializer | target', function (hooks) {
|
||||
attributes: {
|
||||
default_port: 1234,
|
||||
default_client_port: null,
|
||||
enable_session_recording: false,
|
||||
storage_bucket_id: null,
|
||||
},
|
||||
address: null,
|
||||
});
|
||||
@@ -196,6 +239,8 @@ module('Unit | Serializer | target', function (hooks) {
|
||||
attributes: {
|
||||
default_port: 1234,
|
||||
default_client_port: null,
|
||||
enable_session_recording: false,
|
||||
storage_bucket_id: null,
|
||||
},
|
||||
address: null,
|
||||
});
|
||||
@@ -232,6 +277,8 @@ module('Unit | Serializer | target', function (hooks) {
|
||||
attributes: {
|
||||
default_port: 1234,
|
||||
default_client_port: null,
|
||||
enable_session_recording: false,
|
||||
storage_bucket_id: null,
|
||||
},
|
||||
address: null,
|
||||
});
|
||||
@@ -443,4 +490,39 @@ module('Unit | Serializer | target', function (hooks) {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('it normalizes the storage_bucket_id attribute if present', function (assert) {
|
||||
assert.expect(1);
|
||||
const store = this.owner.lookup('service:store');
|
||||
const serializer = store.serializerFor('target');
|
||||
const target = store.createRecord('target').constructor;
|
||||
const payload = {
|
||||
id: '1',
|
||||
name: 'Target 1',
|
||||
storage_bucket_id: 'bucketID',
|
||||
enable_session_recording: true,
|
||||
};
|
||||
const normalized = serializer.normalizeSingleResponse(
|
||||
store,
|
||||
target,
|
||||
payload
|
||||
);
|
||||
assert.deepEqual(normalized, {
|
||||
included: [],
|
||||
data: {
|
||||
id: '1',
|
||||
type: 'target',
|
||||
attributes: {
|
||||
authorized_actions: [],
|
||||
name: 'Target 1',
|
||||
host_sources: [],
|
||||
brokered_credential_source_ids: [],
|
||||
injected_application_credential_source_ids: [],
|
||||
storage_bucket_id: 'bucketID',
|
||||
enable_session_recording: true,
|
||||
},
|
||||
relationships: {},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
|
||||
module('Unit | Transform | duration', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test('it deserializes a duration string', function (assert) {
|
||||
assert.expect(8);
|
||||
const transform = this.owner.lookup('transform:duration');
|
||||
|
||||
const num1 = transform.deserialize('0s');
|
||||
const num2 = transform.deserialize(undefined);
|
||||
const num3 = transform.deserialize('3s');
|
||||
const num4 = transform.deserialize('15.003s');
|
||||
const num5 = transform.deserialize('15.0035s');
|
||||
const num6 = transform.deserialize('15.0034s');
|
||||
const num7 = transform.deserialize('3.000000001s');
|
||||
const num8 = transform.deserialize('300000000001s');
|
||||
|
||||
assert.strictEqual(num1, 0);
|
||||
assert.strictEqual(num2, 0);
|
||||
assert.strictEqual(num3, 3000);
|
||||
assert.strictEqual(num4, 15003);
|
||||
assert.strictEqual(num5, 15004);
|
||||
assert.strictEqual(num6, 15003);
|
||||
assert.strictEqual(num7, 3000);
|
||||
assert.strictEqual(num8, 300000000001000);
|
||||
});
|
||||
|
||||
test('it serializes a duration number', function (assert) {
|
||||
assert.expect(7);
|
||||
const transform = this.owner.lookup('transform:duration');
|
||||
const num1 = transform.serialize(0);
|
||||
const num2 = transform.serialize(undefined);
|
||||
const num3 = transform.serialize(3000);
|
||||
const num4 = transform.serialize(15003);
|
||||
const num5 = transform.serialize(15000000000001);
|
||||
const num6 = transform.serialize(0.000001);
|
||||
const num7 = transform.serialize(0.0000001);
|
||||
|
||||
assert.strictEqual(num1, '0s');
|
||||
assert.strictEqual(num2, '0s');
|
||||
assert.strictEqual(num3, '3s');
|
||||
assert.strictEqual(num4, '15.003s');
|
||||
assert.strictEqual(num5, '15000000000.001s');
|
||||
assert.strictEqual(num6, '0.000000001s');
|
||||
assert.strictEqual(num7, '0s');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
import { helper } from '@ember/component/helper';
|
||||
import { filesize } from 'filesize';
|
||||
|
||||
/**
|
||||
* Takes a int expressing file size in bytes and returns a conversion
|
||||
* into a human-readable string.
|
||||
* "e.g. {{format-bytes-size 164730000}} // 164,73 MB"
|
||||
*/
|
||||
export default helper(function formatBytesSize([sizeInBytes]) {
|
||||
const numberOfBytes = Number(sizeInBytes);
|
||||
return filesize(numberOfBytes);
|
||||
});
|
||||
@@ -11,7 +11,7 @@ import { helper } from '@ember/component/helper';
|
||||
*/
|
||||
export default helper(function formatDateIsoHuman(params /*, hash*/) {
|
||||
return params[0]
|
||||
.toISOString()
|
||||
?.toISOString()
|
||||
.replace('T', ' ')
|
||||
.replace(/\.\d*Z/, '');
|
||||
});
|
||||
@@ -10,5 +10,5 @@ import { helper } from '@ember/component/helper';
|
||||
* e.g. "2020-01-01T00:00:00.999Z".
|
||||
*/
|
||||
export default helper(function formatDateIso(params /*, hash*/) {
|
||||
return params[0].toISOString();
|
||||
return params[0]?.toISOString();
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
import Helper from '@ember/component/helper';
|
||||
import { inject as service } from '@ember/service';
|
||||
import DurationUnitFormat from 'intl-unofficial-duration-unit-format';
|
||||
|
||||
export default class extends Helper {
|
||||
// =services
|
||||
@service intl;
|
||||
|
||||
// =methods
|
||||
/**
|
||||
*
|
||||
* @param {number} durationInMs Duration in milliseconds
|
||||
* @returns localized human-readable duration string. For example, in English "1 hr 23 min 20 sec"
|
||||
*/
|
||||
compute([durationInMs]) {
|
||||
const durationInSeconds = durationInMs / 1000;
|
||||
const durationFormat = new DurationUnitFormat(this.intl.primaryLocale, {
|
||||
style: 'narrow',
|
||||
format: '{days} {hours} {minutes} {seconds}',
|
||||
round: true,
|
||||
});
|
||||
|
||||
return durationFormat.format(durationInSeconds);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import Helper from '@ember/component/helper';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export function indexedDisplayName(intl, translation, array, item) {
|
||||
let index = array.indexOf(item) + 1;
|
||||
if (index === 0) index = null;
|
||||
|
||||
return intl.t(translation, { index });
|
||||
}
|
||||
|
||||
export default class IndexedDisplayName extends Helper {
|
||||
// =services
|
||||
@service intl;
|
||||
|
||||
// =methods
|
||||
/**
|
||||
*
|
||||
* @param {string} translation
|
||||
* @param {Array} array parent array of item
|
||||
* @param {object} item object that you want to find the index of
|
||||
* indexOf will not do a deep equality check with item, just if the object exist in the array
|
||||
* @returns localized name for resource with indexed position. For example, "Channel 1" for first item in a list.
|
||||
*/
|
||||
compute([translation, array, item]) {
|
||||
return indexedDisplayName(this.intl, translation, array, item);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'core/helpers/format-bytes-size';
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'core/helpers/format-time-duration';
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'core/helpers/indexed-display-name';
|
||||
@@ -40,7 +40,9 @@
|
||||
"ember-feature-flags": "^6.0.0",
|
||||
"ember-intl": "^6.0.0-beta.4",
|
||||
"ember-loading": "^2.0.0",
|
||||
"ember-truth-helpers": "^3.0.0"
|
||||
"ember-truth-helpers": "^3.0.0",
|
||||
"filesize": "^10.0.7",
|
||||
"intl-unofficial-duration-unit-format": "^3.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/eslint-parser": "^7.18.2",
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'dummy/tests/helpers';
|
||||
import { render } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
|
||||
module('Integration | Helper | format-bytes-size', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
test('it renders file size in human-readable string', async function (assert) {
|
||||
assert.expect(18);
|
||||
// Size to B
|
||||
const expectedSizeB = '1 B';
|
||||
this.set('inputNumberSizeB', 1);
|
||||
this.set('inputStringSizeB', '1');
|
||||
|
||||
await render(hbs`{{format-bytes-size this.inputNumberSizeB}}`);
|
||||
assert.dom(this.element).hasText(expectedSizeB);
|
||||
await render(hbs`{{format-bytes-size this.inputStringSizeB}}`);
|
||||
assert.dom(this.element).hasText(expectedSizeB);
|
||||
|
||||
// Size to kB
|
||||
const expectedSizeKb = '108.93 kB';
|
||||
this.set('inputNumberSizeKb', 108930);
|
||||
this.set('inputStringSizeKb', '108930');
|
||||
|
||||
await render(hbs`{{format-bytes-size this.inputNumberSizeKb}}`);
|
||||
assert.dom(this.element).hasText(expectedSizeKb);
|
||||
await render(hbs`{{format-bytes-size this.inputStringSizeKb}}`);
|
||||
assert.dom(this.element).hasText(expectedSizeKb);
|
||||
|
||||
// Size to MB
|
||||
const expectedSizeMb = '164.73 MB';
|
||||
this.set('inputNumberSizeMb', 164730000);
|
||||
this.set('inputStringSizeMb', '164730000');
|
||||
|
||||
await render(hbs`{{format-bytes-size this.inputNumberSizeMb}}`);
|
||||
assert.dom(this.element).hasText(expectedSizeMb);
|
||||
await render(hbs`{{format-bytes-size this.inputStringSizeMb}}`);
|
||||
assert.dom(this.element).hasText(expectedSizeMb);
|
||||
|
||||
// Size to GB
|
||||
const expectedSizeGb = '2.15 GB';
|
||||
this.set('inputNumberSizeGb', 2147483647);
|
||||
this.set('inputStringSizeGb', '2147483647');
|
||||
|
||||
await render(hbs`{{format-bytes-size this.inputNumberSizeGb}}`);
|
||||
assert.dom(this.element).hasText(expectedSizeGb);
|
||||
await render(hbs`{{format-bytes-size this.inputStringSizeGb}}`);
|
||||
assert.dom(this.element).hasText(expectedSizeGb);
|
||||
|
||||
// Size to TB
|
||||
const expectedSizeTb = '12.37 TB';
|
||||
this.set('inputNumberSizeTb', 12370000000000);
|
||||
this.set('inputStringSizeTb', '12370000000000');
|
||||
|
||||
await render(hbs`{{format-bytes-size this.inputNumberSizeTb}}`);
|
||||
assert.dom(this.element).hasText(expectedSizeTb);
|
||||
await render(hbs`{{format-bytes-size this.inputStringSizeTb}}`);
|
||||
assert.dom(this.element).hasText(expectedSizeTb);
|
||||
|
||||
// Size to PB
|
||||
const expectedSizePb = '9.01 PB';
|
||||
this.set('inputNumberSizePb', 9007199254740991);
|
||||
this.set('inputStringSizePb', '9007199254740991');
|
||||
|
||||
await render(hbs`{{format-bytes-size this.inputNumberSizePb}}`);
|
||||
assert.dom(this.element).hasText(expectedSizePb);
|
||||
await render(hbs`{{format-bytes-size this.inputStringSizePb}}`);
|
||||
assert.dom(this.element).hasText(expectedSizePb);
|
||||
|
||||
/* eslint-disable no-loss-of-precision */
|
||||
// Size to EB
|
||||
const expectedSizeEb = '1.24 EB';
|
||||
this.set('inputNumberSizeEb', 1237000000000000000);
|
||||
this.set('inputStringSizeEb', '1237000000000000000');
|
||||
|
||||
await render(hbs`{{format-bytes-size this.inputNumberSizeEb}}`);
|
||||
assert.dom(this.element).hasText(expectedSizeEb);
|
||||
await render(hbs`{{format-bytes-size this.inputStringSizeEb}}`);
|
||||
assert.dom(this.element).hasText(expectedSizeEb);
|
||||
|
||||
// Size to ZB
|
||||
const expectedSizeZb = '900.72 ZB';
|
||||
this.set('inputNumberSizeZb', 900719985474899195498367);
|
||||
this.set('inputStringSizeZb', '900719985474899195498367');
|
||||
|
||||
await render(hbs`{{format-bytes-size this.inputNumberSizeZb}}`);
|
||||
assert.dom(this.element).hasText(expectedSizeZb);
|
||||
await render(hbs`{{format-bytes-size this.inputStringSizeZb}}`);
|
||||
assert.dom(this.element).hasText(expectedSizeZb);
|
||||
|
||||
// Size to YB
|
||||
const expectedSizeYb = '90.07 YB';
|
||||
this.set('inputNumberSizeYb', 90071992547409919549836723);
|
||||
this.set('inputStringSizeYb', '90071992547409919549836723');
|
||||
|
||||
await render(hbs`{{format-bytes-size this.inputNumberSizeYb}}`);
|
||||
assert.dom(this.element).hasText(expectedSizeYb);
|
||||
await render(hbs`{{format-bytes-size this.inputStringSizeYb}}`);
|
||||
assert.dom(this.element).hasText(expectedSizeYb);
|
||||
});
|
||||
});
|
||||
@@ -18,4 +18,12 @@ module('Integration | Helper | format-date-iso-human', function (hooks) {
|
||||
|
||||
assert.strictEqual(this.element.textContent.trim(), '2020-01-01 00:00:00');
|
||||
});
|
||||
|
||||
test('it renders when null', async function (assert) {
|
||||
this.set('inputValue', null);
|
||||
|
||||
await render(hbs`{{format-date-iso-human this.inputValue }}`);
|
||||
|
||||
assert.strictEqual(this.element.textContent.trim(), '');
|
||||
});
|
||||
});
|
||||
@@ -21,4 +21,12 @@ module('Integration | Helper | format-date-iso', function (hooks) {
|
||||
'2020-01-01T00:00:00.999Z'
|
||||
);
|
||||
});
|
||||
|
||||
test('it renders when null', async function (assert) {
|
||||
this.set('inputValue', null);
|
||||
|
||||
await render(hbs`{{format-date-iso this.inputValue}}`);
|
||||
|
||||
assert.strictEqual(this.element.textContent.trim(), '');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'dummy/tests/helpers';
|
||||
import { render } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
import { setupIntl } from 'ember-intl/test-support';
|
||||
|
||||
module('Integration | Helper | format-time-duration', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupIntl(hooks);
|
||||
|
||||
test('It renders in human-readable duration strings given different durations in milliseconds', async function (assert) {
|
||||
// Set mock input (in milliseconds) durations
|
||||
this.set('inputDurationSeconds', 3000);
|
||||
this.set('inputDurationMinutes', 2700000);
|
||||
this.set('inputDurationHours', 5000000);
|
||||
this.set('inputDurationDays', 177800000);
|
||||
|
||||
await render(hbs`{{format-time-duration this.inputDurationSeconds}}`);
|
||||
assert.dom(this.element).hasText('3s');
|
||||
|
||||
await render(hbs`{{format-time-duration this.inputDurationMinutes}}`);
|
||||
assert.dom(this.element).hasText('45m');
|
||||
|
||||
await render(hbs`{{format-time-duration this.inputDurationHours}}`);
|
||||
assert.dom(this.element).hasText('1h 23m 20s');
|
||||
|
||||
await render(hbs`{{format-time-duration this.inputDurationDays}}`);
|
||||
assert.dom(this.element).hasText('2d 1h 23m 20s');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'dummy/tests/helpers';
|
||||
import { render } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
import { setupIntl } from 'ember-intl/test-support';
|
||||
|
||||
module('Integration | Helper | indexed-display-name', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupIntl(hooks);
|
||||
|
||||
test('it renders the resource name with its position in parent array', async function (assert) {
|
||||
this.set('array', ['Boundary', 'is', 'cool!']);
|
||||
|
||||
await render(hbs`{{indexed-display-name
|
||||
'resources.session-recording.connection.title_index'
|
||||
this.array
|
||||
'Boundary'
|
||||
}}`);
|
||||
|
||||
assert.dom(this.element).hasText('Connection 1');
|
||||
|
||||
await render(hbs`{{indexed-display-name
|
||||
'resources.session-recording.connection.title_index'
|
||||
this.array
|
||||
'is'
|
||||
}}`);
|
||||
|
||||
assert.dom(this.element).hasText('Connection 2');
|
||||
|
||||
await render(hbs`{{indexed-display-name
|
||||
'resources.session-recording.connection.title_index'
|
||||
this.array
|
||||
'cool!'
|
||||
}}`);
|
||||
|
||||
assert.dom(this.element).hasText('Connection 3');
|
||||
});
|
||||
|
||||
test('it does not render the position if the index cannot be found', async function (assert) {
|
||||
this.set('array', ['Boundary', 'is', 'cool!']);
|
||||
|
||||
await render(hbs`{{indexed-display-name
|
||||
'resources.session-recording.connection.title_index'
|
||||
this.array
|
||||
'Watchtower'
|
||||
}}`);
|
||||
|
||||
assert.dom(this.element).hasText('Connection');
|
||||
});
|
||||
});
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
add: Add
|
||||
cancel: Cancel
|
||||
edit: Edit
|
||||
delete: Delete
|
||||
remove: Remove
|
||||
discard: Discard
|
||||
@@ -18,7 +19,9 @@ deny: Deny
|
||||
help: Need help?
|
||||
next: Next
|
||||
back: Back
|
||||
view: View
|
||||
retry: Retry
|
||||
play: Play
|
||||
get-topic-help: Get help for this topic
|
||||
copy-to-clipboard: Copy to Clipboard
|
||||
copy-error-detail-to-clipboard: Copy error detail to clipboard
|
||||
|
||||
@@ -43,6 +43,7 @@ descriptions:
|
||||
cluster-url-initialization: To get started, please enter your cluster URL
|
||||
create-but-not-list: You do not have permission to list {resource}, but you can create new instances.
|
||||
neither-list-nor-create: You do not have permission to list or create {resource}.
|
||||
cannot-download: You do not have permission to download and playback {resource}.
|
||||
no-results: We found no matching records.
|
||||
choose-a-provider: There are several built-in plugins for the most common providers. You can choose from one of the included plugins or use a custom plugin.
|
||||
provider: This is the provider for this host catalog. The format of the host set filters are specific to each provider.
|
||||
|
||||
@@ -170,3 +170,5 @@ tags:
|
||||
secret-editor:
|
||||
details: This secret is saved but won't be displayed
|
||||
edit: Click to replace this secret
|
||||
created:
|
||||
label: Created
|
||||
@@ -213,6 +213,115 @@ org:
|
||||
new: New Org
|
||||
actions:
|
||||
delete: Delete Org
|
||||
session-recording:
|
||||
title: Session Recording
|
||||
title_plural: Session Recordings
|
||||
description: Protocol-aware recordings capture data and actions during user sessions. Operators may review recordings for auditing and analysis.
|
||||
details: Session details
|
||||
related: Related
|
||||
up: '{bytes} up'
|
||||
down: '{bytes} down'
|
||||
duration: '{time} duration'
|
||||
connection:
|
||||
title: Connection
|
||||
title_index: Connection {index}
|
||||
title_plural: Connections
|
||||
form:
|
||||
channel:
|
||||
label: Channel
|
||||
summary:
|
||||
label: Summary
|
||||
channels-by-connection:
|
||||
title: Channels by connection
|
||||
messages:
|
||||
no-channels: There are no channels for this connection
|
||||
started-no-connections:
|
||||
title: Session Recording In Progress
|
||||
description: The Session has started but no connections have been made.
|
||||
unknown-no-connections:
|
||||
title: Session Recording Failure
|
||||
description: This Session cannot be played back because no connections were established, and the Session has failed.
|
||||
channel:
|
||||
title: Channel
|
||||
title_index: Channel {index}
|
||||
title_plural: Channels
|
||||
details: Channel details
|
||||
messages:
|
||||
none:
|
||||
title: Channel Playback
|
||||
missing:
|
||||
title: We can't play back this channel because the file is missing
|
||||
description: This likely means the connected storage bucket is misconfigured or the file was deleted.
|
||||
link: Retrieving lost session recordings
|
||||
not-supported:
|
||||
title: Playback is not supported for this channel
|
||||
description: The contents of this channel cannot be replayed.
|
||||
player:
|
||||
back-navigation: Back to channels
|
||||
loading: Loading recording…
|
||||
session:
|
||||
title: Session Playback
|
||||
form:
|
||||
time:
|
||||
label: Time
|
||||
user:
|
||||
label: User
|
||||
target:
|
||||
label: Target
|
||||
duration:
|
||||
label: Duration
|
||||
status:
|
||||
label: Status
|
||||
messages:
|
||||
none:
|
||||
title: Welcome to session recording
|
||||
description: Review, playback, and download recorded sessions. To get started, configure recording on a target and start a session.
|
||||
no-storage-bucket-description: To start using session recordings, create a storage bucket and configure recording on a target.
|
||||
storage-bucket:
|
||||
title: Storage Bucket
|
||||
title_plural: Storage Buckets
|
||||
description: External services store data from session recordings.
|
||||
messages:
|
||||
none:
|
||||
title: No storage buckets yet
|
||||
description: To start using session recording, you need to have an Amazon S3 bucket.
|
||||
link: Add a new storage bucket
|
||||
titles:
|
||||
new: New Storage Bucket
|
||||
amazon_s3: Amazon S3
|
||||
actions:
|
||||
delete: Delete Storage Bucket
|
||||
types:
|
||||
aws_s3: AWS S3
|
||||
form:
|
||||
scope:
|
||||
label: Scope
|
||||
help: This storage bucket can be associated with targets in this scope.
|
||||
provider:
|
||||
label: Provider
|
||||
help: Boundary currently only supports Amazon S3 storage.
|
||||
bucket_name:
|
||||
label: Bucket name
|
||||
help: The name of this bucket in AWS
|
||||
bucket_prefix:
|
||||
label: Bucket prefix
|
||||
help: The base path where session recordings will be stored.
|
||||
region:
|
||||
label: Region
|
||||
help: The AWS region to use
|
||||
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.
|
||||
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.
|
||||
worker_filter:
|
||||
label: Worker filter
|
||||
help: Filter for Boundary workers that have network access to your storage. Boolean expression.
|
||||
questions:
|
||||
delete-storage-bucket:
|
||||
title: Delete associated recordings?
|
||||
message: This storage bucket may have session recordings associated with it. Deleting this storage bucket will delete these recordings when you save your changes.
|
||||
project:
|
||||
title: Project
|
||||
title_plural: Projects
|
||||
@@ -310,6 +419,8 @@ target:
|
||||
add-brokered-credential-sources: Add Brokered Credentials
|
||||
add-injected-application-credential-sources: Add Injected Application Credentials
|
||||
remove-address: Remove address and save
|
||||
enable-session-recording: Enable Session Recording
|
||||
enable-recording: Enable recording
|
||||
types:
|
||||
tcp: Generic TCP
|
||||
ssh: SSH
|
||||
@@ -332,6 +443,10 @@ target:
|
||||
default_client_port:
|
||||
label: Default Client Port
|
||||
help: The local proxy port on which to listen by default when a session is started on a client.
|
||||
sidebar:
|
||||
label: Session Recording
|
||||
help: Record SSH sessions on this target and save them to an Amazon S3 bucket.
|
||||
link: Session Recording settings
|
||||
questions:
|
||||
delete-host-sources:
|
||||
title: Remove associated host sources?
|
||||
@@ -353,6 +468,17 @@ target:
|
||||
delete-address:
|
||||
title: Remove target address?
|
||||
message: This target has an assigned address. Adding a host source will remove the assigned address from the target when you save your changes.
|
||||
enable-session-recording:
|
||||
title: Enable Session Recording
|
||||
messages:
|
||||
add:
|
||||
title: Enable Session Recording for Target
|
||||
form:
|
||||
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.
|
||||
brokered-credential-source:
|
||||
title: Brokered Credential
|
||||
title_plural: Brokered Credentials
|
||||
@@ -604,6 +730,14 @@ worker:
|
||||
worker_tags:
|
||||
label: Worker Tags
|
||||
help: Boundary can use tags to define key-value pairs which targets can use to determine where they should route connections.
|
||||
local_session_recording_storage:
|
||||
label: Local Session Recording Storage
|
||||
help: To record sessions using this worker, specify a local storage path and ensure that it is provisioned with sufficient capacity. In progress recordings are stored locally until they complete, and then moved to the remote storage bucket associated with their target.
|
||||
enable_recording_storage_path:
|
||||
label: Enable Recording Storage
|
||||
recording_storage_path:
|
||||
label: Recording Storage Path
|
||||
help: Local path on the worker node for temporary file storage.
|
||||
steps:
|
||||
1:
|
||||
title: 1. Configure your new worker
|
||||
|
||||
@@ -11,3 +11,6 @@ primary: Primary
|
||||
inactive: Inactive
|
||||
active-private: Private
|
||||
active-public: Public
|
||||
recording: Recording
|
||||
completed: Completed
|
||||
failed: Failed
|
||||
@@ -102,6 +102,9 @@
|
||||
--token-color-palette-neutral-200
|
||||
);
|
||||
--token-form-indicator-optional-color: var(--token-color-palette-neutral-200);
|
||||
--token-color-foreground-high-contrast: var(
|
||||
--token-color-palette-neutral-700
|
||||
);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
|
||||
+1
-1
@@ -36,7 +36,7 @@
|
||||
"test:addons-rose": "yarn --cwd addons/rose test",
|
||||
"test:ui-admin": "yarn --cwd ui/admin test",
|
||||
"test:ui-desktop": "yarn --cwd ui/desktop test",
|
||||
"compliance:licenses": "license-checker --onlyAllow 'Apache*;Apache License, Version 2.0;Apache-2.0;Apache 2.0;Artistic-2.0;BSD;BSD-3-Clause;CC-BY-3.0;CC-BY-4.0;CC0-1.0;ISC;MIT;MPL-2.0;Public Domain;Python-2.0;Unicode-TOU;Unlicense;WTFPL' --excludePackages 'boundary-ui;admin@0.0.0;desktop@0.0.0;base64-js@0.0.2;cycle@1.0.3;cldr-core@41.0.0;make-plural@7.2.0'",
|
||||
"compliance:licenses": "license-checker --onlyAllow 'Apache*;Apache License, Version 2.0;Apache-2.0;Apache 2.0;Artistic-2.0;BSD;BSD-3-Clause;CC-BY-3.0;CC-BY-4.0;CC0-1.0;ISC;MIT;MPL-2.0;Public Domain;Python-2.0;Unicode-TOU;Unlicense;WTFPL' --excludePackages 'boundary-ui;admin@0.0.0;desktop@0.0.0;base64-js@0.0.2;cycle@1.0.3;cldr-core@41.0.0;make-plural@7.2.0;@hashicorp/ember-asciinema-player@0.0.0'",
|
||||
"doc:toc": "doctoc README.md",
|
||||
"prepare": "husky install"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
import OverrideModelAbility from './model';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class OverrideScopeAbility extends OverrideModelAbility {
|
||||
// =services
|
||||
|
||||
@service features;
|
||||
|
||||
// =attributes
|
||||
|
||||
/**
|
||||
* Creating a resource is allowed only if a create grant is present
|
||||
* and collection-specific criteria are met.
|
||||
* @type {boolean}
|
||||
*/
|
||||
get canCreate() {
|
||||
switch (this.collection) {
|
||||
case 'storage-buckets':
|
||||
return (
|
||||
this.features.isEnabled('ssh-session-recording') && super.canCreate
|
||||
);
|
||||
default:
|
||||
return super.canCreate;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Listing a resource is allowed only if a list grant is present
|
||||
* and collection-specific criteria are met.
|
||||
* @type {boolean}
|
||||
*/
|
||||
get canList() {
|
||||
switch (this.collection) {
|
||||
case 'session-recordings':
|
||||
case 'storage-buckets':
|
||||
return (
|
||||
this.features.isEnabled('ssh-session-recording') && super.canList
|
||||
);
|
||||
default:
|
||||
return super.canList;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
import SessionRecordingAbility from 'api/abilities/session-recording';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class OverrideSessionRecordingAbility extends SessionRecordingAbility {
|
||||
// =service
|
||||
@service features;
|
||||
|
||||
/**
|
||||
* This override ensures that session recordings may be read only if the
|
||||
* session-recording feature flag is enabled.
|
||||
*/
|
||||
get canRead() {
|
||||
return this.features.isEnabled('ssh-session-recording')
|
||||
? super.canRead
|
||||
: false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
import StorageBucketAbility from 'api/abilities/storage-bucket';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class OverrideStorageBucketAbility extends StorageBucketAbility {
|
||||
// =service
|
||||
@service features;
|
||||
|
||||
/**
|
||||
* This override ensures that storage buckets may be read only if the
|
||||
* session-recording feature flag is enabled.
|
||||
*/
|
||||
get canRead() {
|
||||
return this.features.isEnabled('ssh-session-recording')
|
||||
? super.canRead
|
||||
: false;
|
||||
}
|
||||
|
||||
/**
|
||||
* This override ensures that storage buckets may be updated only if the
|
||||
* session-recording feature flag is enabled.
|
||||
*/
|
||||
get canUpdate() {
|
||||
return this.features.isEnabled('ssh-session-recording')
|
||||
? super.canUpdate
|
||||
: false;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: Ensure that storage buckets may be deleted only if the
|
||||
* session-recording feature flag is enabled.
|
||||
* For now a storage bucket cannot be deleted.
|
||||
*/
|
||||
get canDelete() {
|
||||
return false;
|
||||
// return this.features.isEnabled('ssh-session-recording')
|
||||
// ? super.canDelete
|
||||
// : false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
{{!
|
||||
Copyright (c) HashiCorp, Inc.
|
||||
SPDX-License-Identifier: MPL-2.0
|
||||
}}
|
||||
|
||||
<div class='secret-input'>
|
||||
{{#if (or this.isDisabled this.isActionable)}}
|
||||
<span class='overlay'></span>
|
||||
{{/if}}
|
||||
<Hds::Form::TextInput::Field
|
||||
@isRequired={{@isRequired}}
|
||||
@isInvalid={{@isInvalid}}
|
||||
@value={{@value}}
|
||||
@type={{(or @type 'password')}}
|
||||
name={{@name}}
|
||||
disabled={{or @isDisabled (not this.isEditing)}}
|
||||
...attributes
|
||||
as |F|
|
||||
>
|
||||
{{yield F}}
|
||||
</Hds::Form::TextInput::Field>
|
||||
|
||||
{{#if this.isDisabled}}
|
||||
<Hds::Button
|
||||
@text={{t 'actions.edit'}}
|
||||
@icon='edit'
|
||||
disabled={{@isDisabled}}
|
||||
/>
|
||||
{{else if this.isActionable}}
|
||||
<Hds::Button
|
||||
@text={{t 'actions.edit'}}
|
||||
@icon='edit'
|
||||
@color='secondary'
|
||||
{{on 'click' this.enableEdit}}
|
||||
/>
|
||||
{{else}}
|
||||
<Hds::Button
|
||||
@text={{t 'actions.cancel'}}
|
||||
@color='secondary'
|
||||
{{on 'click' this.cancelEdit}}
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { action } from '@ember/object';
|
||||
|
||||
export default class FormFieldSecretInputIndexComponent extends Component {
|
||||
// =properties
|
||||
/**
|
||||
* Tracks when secret can be updated
|
||||
* @type {boolean}
|
||||
*/
|
||||
@tracked isEditing = false;
|
||||
|
||||
/**
|
||||
* Returns true if the resource form is in read only mode
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isDisabled() {
|
||||
return this.args.isDisabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the resource form is in edit mode and
|
||||
* `edit` button is enabled for the secret field.
|
||||
* This state lets the user to update the secret fields if they like
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isActionable() {
|
||||
return this.args.showEditButton && !this.isEditing;
|
||||
}
|
||||
|
||||
// =actions
|
||||
|
||||
/**
|
||||
* Sets the secret field to be updated
|
||||
* when `edit` button is clicked in Actionable view
|
||||
*/
|
||||
@action
|
||||
enableEdit() {
|
||||
if (!this.args.isDisabled) this.isEditing = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears updated secret when cancel button is
|
||||
* clicked in Actionable view.
|
||||
* Sets the secret field back to disabled mode
|
||||
*/
|
||||
@action
|
||||
cancelEdit() {
|
||||
this.isEditing = false;
|
||||
this.args.cancel();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
{{!
|
||||
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
|
||||
@isOptional={{true}}
|
||||
@value={{@model.name}}
|
||||
@isInvalid={{@model.errors.name}}
|
||||
@type='text'
|
||||
name='name'
|
||||
disabled={{form.disabled}}
|
||||
{{on 'input' (set-from-event @model 'name')}}
|
||||
as |F|
|
||||
>
|
||||
<F.Label>{{t 'form.name.label'}}</F.Label>
|
||||
<F.HelperText>{{t 'form.name.help'}}</F.HelperText>
|
||||
{{#if @model.errors.name}}
|
||||
<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
|
||||
@isOptional={{true}}
|
||||
@value={{@model.description}}
|
||||
@isInvalid={{@model.errors.description}}
|
||||
name='description'
|
||||
disabled={{form.disabled}}
|
||||
as |F|
|
||||
>
|
||||
<F.Label>{{t 'form.description.label'}}</F.Label>
|
||||
<F.HelperText>{{t 'form.description.help'}}</F.HelperText>
|
||||
{{#if @model.errors.description}}
|
||||
<F.Error as |E|>
|
||||
{{#each @model.errors.description as |error|}}
|
||||
<E.Message>{{error.message}}</E.Message>
|
||||
{{/each}}
|
||||
</F.Error>
|
||||
{{/if}}
|
||||
</Hds::Form::Textarea::Field>
|
||||
|
||||
{{#if @model.isNew}}
|
||||
<Hds::Form::Select::Field
|
||||
@isRequired={{true}}
|
||||
@width='100%'
|
||||
disabled={{form.disabled}}
|
||||
name='scope'
|
||||
{{on 'change' this.updateScope}}
|
||||
as |F|
|
||||
>
|
||||
<F.Label>{{t 'resources.storage-bucket.form.scope.label'}}</F.Label>
|
||||
<F.HelperText>
|
||||
{{t 'resources.storage-bucket.form.scope.help'}}
|
||||
</F.HelperText>
|
||||
<F.Options>
|
||||
{{#each @scopes as |scope|}}
|
||||
<option
|
||||
value={{scope.model.id}}
|
||||
selected={{eq @model.scopeModel.id scope.model.id}}
|
||||
>
|
||||
{{scope.model.displayName}}
|
||||
</option>
|
||||
{{/each}}
|
||||
</F.Options>
|
||||
</Hds::Form::Select::Field>
|
||||
{{else}}
|
||||
<InfoField
|
||||
@value={{@model.scopeModel.displayName}}
|
||||
@icon={{if @model.scopeModel.isGlobal 'globe' 'org'}}
|
||||
disabled={{form.disabled}}
|
||||
as |F|
|
||||
>
|
||||
<F.Label>{{t 'resources.storage-bucket.form.scope.label'}}</F.Label>
|
||||
<F.HelperText>{{t
|
||||
'resources.storage-bucket.form.scope.help'
|
||||
}}</F.HelperText>
|
||||
</InfoField>
|
||||
{{/if}}
|
||||
|
||||
<InfoField
|
||||
@value={{t 'resources.storage-bucket.titles.amazon_s3'}}
|
||||
@icon='aws-color'
|
||||
disabled={{form.disabled}}
|
||||
as |F|
|
||||
>
|
||||
<F.Label>{{t 'titles.provider'}}</F.Label>
|
||||
<F.HelperText>{{t
|
||||
'resources.storage-bucket.form.provider.help'
|
||||
}}</F.HelperText>
|
||||
</InfoField>
|
||||
|
||||
<Hds::Form::TextInput::Field
|
||||
@isRequired={{true}}
|
||||
@value={{@model.bucket_name}}
|
||||
@isInvalid={{@model.errors.bucket_name}}
|
||||
@type='text'
|
||||
name='bucket_name'
|
||||
disabled={{form.disabled}}
|
||||
readOnly={{not @model.isNew}}
|
||||
{{on 'input' (set-from-event @model 'bucket_name')}}
|
||||
as |F|
|
||||
>
|
||||
<F.Label>{{t 'resources.storage-bucket.form.bucket_name.label'}}</F.Label>
|
||||
<F.HelperText>{{t
|
||||
'resources.storage-bucket.form.bucket_name.help'
|
||||
}}</F.HelperText>
|
||||
{{#if @model.errors.bucket_name}}
|
||||
<F.Error as |E|>
|
||||
{{#each @model.errors.bucket_name as |error|}}
|
||||
<E.Message>{{error.message}}</E.Message>
|
||||
{{/each}}
|
||||
</F.Error>
|
||||
{{/if}}
|
||||
</Hds::Form::TextInput::Field>
|
||||
|
||||
<Hds::Form::TextInput::Field
|
||||
@isOptional={{true}}
|
||||
@value={{@model.bucket_prefix}}
|
||||
@isInvalid={{@model.errors.bucket_prefix}}
|
||||
@type='text'
|
||||
name='bucket_prefix'
|
||||
disabled={{form.disabled}}
|
||||
readOnly={{not @model.isNew}}
|
||||
{{on 'input' (set-from-event @model 'bucket_prefix')}}
|
||||
as |F|
|
||||
>
|
||||
<F.Label>{{t 'resources.storage-bucket.form.bucket_prefix.label'}}</F.Label>
|
||||
<F.HelperText>{{t
|
||||
'resources.storage-bucket.form.bucket_prefix.help'
|
||||
}}</F.HelperText>
|
||||
{{#if @model.errors.bucket_prefix}}
|
||||
<F.Error as |E|>
|
||||
{{#each @model.errors.bucket_prefix as |error|}}
|
||||
<E.Message>{{error.message}}</E.Message>
|
||||
{{/each}}
|
||||
</F.Error>
|
||||
{{/if}}
|
||||
</Hds::Form::TextInput::Field>
|
||||
|
||||
<Hds::Form::TextInput::Field
|
||||
@isRequired={{true}}
|
||||
@value={{@model.region}}
|
||||
@isInvalid={{@model.errors.region}}
|
||||
@type='text'
|
||||
name='region'
|
||||
disabled={{form.disabled}}
|
||||
{{on 'input' (set-from-event @model 'region')}}
|
||||
as |F|
|
||||
>
|
||||
<F.Label>{{t 'resources.storage-bucket.form.region.label'}}</F.Label>
|
||||
<F.HelperText>{{t
|
||||
'resources.storage-bucket.form.region.help'
|
||||
}}</F.HelperText>
|
||||
{{#if @model.errors.region}}
|
||||
<F.Error as |E|>
|
||||
{{#each @model.errors.region as |error|}}
|
||||
<E.Message>{{error.message}}</E.Message>
|
||||
{{/each}}
|
||||
</F.Error>
|
||||
{{/if}}
|
||||
</Hds::Form::TextInput::Field>
|
||||
|
||||
{{#if @model.isNew}}
|
||||
<Hds::Form::TextInput::Field
|
||||
@isRequired={{true}}
|
||||
@value={{@model.access_key_id}}
|
||||
@isInvalid={{@model.errors.access_key_id}}
|
||||
@type='password'
|
||||
name='access_key_id'
|
||||
{{on 'input' (set-from-event @model 'access_key_id')}}
|
||||
as |F|
|
||||
>
|
||||
<F.Label>{{t
|
||||
'resources.storage-bucket.form.access_key_id.label'
|
||||
}}</F.Label>
|
||||
<F.HelperText>{{t
|
||||
'resources.storage-bucket.form.access_key_id.help'
|
||||
}}</F.HelperText>
|
||||
{{#if @model.errors.access_key_id}}
|
||||
<F.Error as |E|>
|
||||
{{#each @model.errors.access_key_id as |error|}}
|
||||
<E.Message>{{error.message}}</E.Message>
|
||||
{{/each}}
|
||||
</F.Error>
|
||||
{{/if}}
|
||||
</Hds::Form::TextInput::Field>
|
||||
|
||||
<Hds::Form::TextInput::Field
|
||||
@isRequired={{true}}
|
||||
@value={{@model.secret_access_key}}
|
||||
@isInvalid={{@model.errors.secret_access_key}}
|
||||
@type='password'
|
||||
name='secret_access_key'
|
||||
disabled={{form.disabled}}
|
||||
{{on 'input' (set-from-event @model 'secret_access_key')}}
|
||||
as |F|
|
||||
>
|
||||
<F.Label>{{t
|
||||
'resources.storage-bucket.form.secret_access_key.label'
|
||||
}}</F.Label>
|
||||
<F.HelperText>{{t
|
||||
'resources.storage-bucket.form.secret_access_key.help'
|
||||
}}</F.HelperText>
|
||||
{{#if @model.errors.secret_access_key}}
|
||||
<F.Error as |E|>
|
||||
{{#each @model.errors.secret_access_key as |error|}}
|
||||
<E.Message>{{error.message}}</E.Message>
|
||||
{{/each}}
|
||||
</F.Error>
|
||||
{{/if}}
|
||||
</Hds::Form::TextInput::Field>
|
||||
{{else}}
|
||||
<Form::Field::SecretInput
|
||||
@isRequired={{true}}
|
||||
@value={{@model.access_key_id}}
|
||||
@isInvalid={{@model.errors.access_key_id}}
|
||||
@name='access_key_id'
|
||||
@isDisabled={{form.disabled}}
|
||||
@showEditButton={{true}}
|
||||
@cancel={{fn this.rollbackSecretAttrs 'access_key_id'}}
|
||||
{{on 'input' (set-from-event @model 'access_key_id')}}
|
||||
as |F|
|
||||
>
|
||||
<F.Label>{{t
|
||||
'resources.storage-bucket.form.access_key_id.label'
|
||||
}}</F.Label>
|
||||
<F.HelperText>{{t
|
||||
'resources.storage-bucket.form.access_key_id.help'
|
||||
}}</F.HelperText>
|
||||
{{#if @model.errors.access_key_id}}
|
||||
<F.Error as |E|>
|
||||
{{#each @model.errors.access_key_id as |error|}}
|
||||
<E.Message>{{error.message}}</E.Message>
|
||||
{{/each}}
|
||||
</F.Error>
|
||||
{{/if}}
|
||||
</Form::Field::SecretInput>
|
||||
|
||||
<Form::Field::SecretInput
|
||||
@isRequired={{true}}
|
||||
@value={{@model.secret_access_key}}
|
||||
@isInvalid={{@model.errors.secret_access_key}}
|
||||
@name='secret_access_key'
|
||||
@isDisabled={{form.disabled}}
|
||||
@showEditButton={{true}}
|
||||
@cancel={{fn this.rollbackSecretAttrs 'secret_access_key'}}
|
||||
{{on 'input' (set-from-event @model 'secret_access_key')}}
|
||||
as |F|
|
||||
>
|
||||
<F.Label>{{t
|
||||
'resources.storage-bucket.form.secret_access_key.label'
|
||||
}}</F.Label>
|
||||
<F.HelperText>{{t
|
||||
'resources.storage-bucket.form.secret_access_key.help'
|
||||
}}</F.HelperText>
|
||||
{{#if @model.errors.secret_access_key}}
|
||||
<F.Error as |E|>
|
||||
{{#each @model.errors.secret_access_key as |error|}}
|
||||
<E.Message>{{error.message}}</E.Message>
|
||||
{{/each}}
|
||||
</F.Error>
|
||||
{{/if}}
|
||||
</Form::Field::SecretInput>
|
||||
{{/if}}
|
||||
|
||||
<Hds::Form::TextInput::Field
|
||||
@isRequired={{true}}
|
||||
@value={{@model.worker_filter}}
|
||||
@isInvalid={{@model.errors.worker_filter}}
|
||||
@type='text'
|
||||
name='worker_filter'
|
||||
disabled={{form.disabled}}
|
||||
{{on 'input' (set-from-event @model 'worker_filter')}}
|
||||
as |F|
|
||||
>
|
||||
<F.Label>{{t 'resources.storage-bucket.form.worker_filter.label'}}</F.Label>
|
||||
<F.HelperText>
|
||||
{{t 'resources.storage-bucket.form.worker_filter.help'}}
|
||||
{{! TODO: Link to the correct url for worker-filters }}
|
||||
<Hds::Link::Inline @href={{doc-url 'worker-filters'}}>{{t
|
||||
'actions.learn-more'
|
||||
}}</Hds::Link::Inline>
|
||||
</F.HelperText>
|
||||
{{#if @model.errors.worker_filter}}
|
||||
<F.Error as |E|>
|
||||
{{#each @model.errors.worker_filter as |error|}}
|
||||
<E.Message>{{error.message}}</E.Message>
|
||||
{{/each}}
|
||||
</F.Error>
|
||||
{{/if}}
|
||||
</Hds::Form::TextInput::Field>
|
||||
|
||||
<Hds::Form::Checkbox::Field
|
||||
name='disable_credential_rotation'
|
||||
disabled={{form.disabled}}
|
||||
checked={{@model.disable_credential_rotation}}
|
||||
{{on 'change' this.toggleDisableCredentialRotation}}
|
||||
as |F|
|
||||
>
|
||||
<F.Label>{{t 'form.disable_credential_rotation.label'}}</F.Label>
|
||||
</Hds::Form::Checkbox::Field>
|
||||
|
||||
{{#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,36 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
import { action } from '@ember/object';
|
||||
import { set } from '@ember/object';
|
||||
|
||||
export default class FormStorageBucketComponent extends Component {
|
||||
// =actions
|
||||
|
||||
@action
|
||||
updateScope(event) {
|
||||
const selectedScopeId = event.target.value;
|
||||
const selectedScopeModel = this.args.scopes.find(
|
||||
(element) => element.model.id === selectedScopeId
|
||||
).model;
|
||||
this.args.model.scopeModel = selectedScopeModel;
|
||||
}
|
||||
|
||||
@action
|
||||
rollbackSecretAttrs(currentAttr) {
|
||||
const changedAttrs = this.args.model.changedAttributes();
|
||||
if (currentAttr in changedAttrs) {
|
||||
const [oldVal] = changedAttrs[currentAttr];
|
||||
set(this.args.model, currentAttr, oldVal);
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
toggleDisableCredentialRotation() {
|
||||
this.args.model.disable_credential_rotation =
|
||||
!this.args.model.disable_credential_rotation;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
}}
|
||||
|
||||
<Rose::Form
|
||||
class={{unless @model.isNew 'full-width'}}
|
||||
@onSubmit={{@submit}}
|
||||
@cancel={{this.cancel}}
|
||||
@disabled={{@model.isSaving}}
|
||||
|
||||
Loaded 100 of 210 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user