import { IComplianceChangeSet } from 'wherehows-web/components/dataset-compliance'; import DatasetTableRow from 'wherehows-web/components/dataset-table-row'; import ComputedProperty, { alias, bool } from '@ember/object/computed'; import { computed, get, getProperties, getWithDefault } from '@ember/object'; import { Classification, ComplianceFieldIdValue, SuggestionIntent, getDefaultSecurityClassification, IComplianceFieldFormatOption, IComplianceFieldIdentifierOption, IFieldIdentifierOption } from 'wherehows-web/constants'; import { IComplianceDataType } from 'wherehows-web/typings/api/list/compliance-datatypes'; import { fieldChangeSetRequiresReview } from 'wherehows-web/utils/datasets/compliance-policy'; import { getFieldSuggestions } from 'wherehows-web/utils/datasets/compliance-suggestions'; import noop from 'wherehows-web/utils/noop'; import { hasEnumerableKeys } from 'wherehows-web/utils/object'; /** * Constant definition for an unselected field format * @type {IFieldIdentifierOption} */ const unSelectedFieldFormatValue: IFieldIdentifierOption = { value: null, label: 'Select Field Format...', isDisabled: true }; export default class DatasetComplianceRow extends DatasetTableRow { classNameBindings = ['isReadonlyEntity:dataset-compliance-fields--readonly']; /** * Declares the field property on a DatasetTableRow. Contains attributes for a compliance field record * @type {IComplianceChangeSet} * @memberof DatasetComplianceRow */ field: IComplianceChangeSet; /** * Reference to the compliance data types * @type {Array} */ complianceDataTypes: Array; /** * Reference to the compliance `onFieldOwnerChange` action * @memberof DatasetComplianceRow */ onFieldOwnerChange: (field: IComplianceChangeSet, nonOwner: boolean) => void; /** * Describes action interface for `onFieldIdentifierTypeChange` action * @memberof DatasetComplianceRow */ onFieldIdentifierTypeChange: (field: IComplianceChangeSet, option: { value: ComplianceFieldIdValue | null }) => void; /** * Describes action interface for `onFieldLogicalTypeChange` action * @memberof DatasetComplianceRow */ onFieldLogicalTypeChange: (field: IComplianceChangeSet, value: IComplianceChangeSet['logicalType']) => void; /** * Describes action interface for `onFieldClassificationChange` action * @memberof DatasetComplianceRow */ onFieldClassificationChange: (field: IComplianceChangeSet, option: { value: '' | Classification }) => void; /** * Describes action interface for `onSuggestionIntent` action * @memberof DatasetComplianceRow */ onSuggestionIntent: (field: IComplianceChangeSet, intent?: SuggestionIntent) => void; /** * The field identifier attribute * @type {ComputedProperty} * @memberof DatasetComplianceRow */ identifierField: ComputedProperty = alias('field.identifierField'); /** * Flag indicating if this field is a non owner or owner * @type {ComputedProperty} * @memberof DatasetComplianceRow */ nonOwner: ComputedProperty = alias('field.nonOwner').readOnly(); /** * The field's dataType attribute * @type {ComputedProperty} * @memberof DatasetComplianceRow */ dataType: ComputedProperty = alias('field.dataType'); /** * Aliases a fields readonly attribute as a boolean computed property * @type {ComputedProperty} * @memberof DatasetComplianceRow */ isReadonlyEntity: ComputedProperty = bool('field.readonly'); /** * Dropdown options for each compliance field / record * @type {Array} * @memberof DatasetComplianceRow */ complianceFieldIdDropdownOptions: Array; /** * Reference to the current value of the field's SuggestionIntent if present * indicates that the provided suggestion is either accepted or ignored * @type {(ComputedProperty)} * @memberof DatasetComplianceRow */ suggestionAuthority: ComputedProperty = alias( 'field.suggestionAuthority' ); /** * Maps the suggestion response, if present, to a string resolution * @type ComputedProperty * @memberof DatasetComplianceRow */ suggestionResolution = computed('suggestionAuthority', function(this: DatasetComplianceRow): string | void { const suggestionAuthority = get(this, 'suggestionAuthority'); if (suggestionAuthority) { return { [SuggestionIntent.accept]: 'Accepted', [SuggestionIntent.ignore]: 'Discarded' }[suggestionAuthority]; } }); /** * Checks that the field does not have a current policy value * @type {ComputedProperty} * @memberof DatasetComplianceRow */ isReviewRequested = computed('field.{isDirty,suggestion,privacyPolicyExists,suggestionAuthority}', function( this: DatasetComplianceRow ): boolean { return fieldChangeSetRequiresReview(get(this, 'field')); }); /** * Checks if the field format drop-down should be disabled based on the type of the field * *WIP * @type {ComputedProperty} * @memberof DatasetComplianceRow */ isFieldFormatDisabled = computed('field.identifierType', noop).readOnly(); /** * Takes a field property and extracts the value on the current policy if a suggestion currently exists for the field * @param {('logicalType' | 'identifierType')} fieldProp * @returns {(string | null)} * @memberof DatasetComplianceRow */ getCurrentValueBeforeSuggestion( this: DatasetComplianceRow, fieldProp: 'logicalType' | 'identifierType' ): string | null { /** * Current value on policy prior to the suggested value * @type {string} */ const value = get(this, 'field')[fieldProp]; if (hasEnumerableKeys(get(this, 'prediction')) && value) { const { label: currentIdType } = getWithDefault(this, 'complianceFieldIdDropdownOptions', []).findBy( 'value', value ) || { label: null }; const { value: currentLogicalType }: Pick = getWithDefault( this, 'fieldFormats', [] ).findBy('value', value) || { value: null }; return { identifierType: currentIdType, logicalType: currentLogicalType }[fieldProp]; } return null; } /** * Returns a computed value for the field identifierType * @type {ComputedProperty} * @memberof DatasetComplianceRow */ identifierType = computed('field.identifierType', 'prediction', function( this: DatasetComplianceRow ): IComplianceChangeSet['identifierType'] { /** * Describes the interface for the options bag param passed into the getIdentifierType function below * @interface IGetIdentParams */ interface IGetIdentParams { identifierType: IComplianceChangeSet['identifierType']; prediction: { identifierType: IComplianceChangeSet['identifierType'] } | void; } const { field: { identifierType }, prediction } = getProperties(this, ['field', 'prediction']); /** * Inner function takes the field.identifierType and prediction values, and * returns the identifierType to be rendered in the ui * @param {IGetIdentParams} params * @returns {IComplianceChangeSet.identifierType} */ const getIdentifierType = (params: IGetIdentParams): IComplianceChangeSet['identifierType'] => { const { identifierType, prediction } = params; return prediction ? prediction.identifierType : identifierType; }; return getIdentifierType({ identifierType, prediction }); }).readOnly(); /** * Gets the identifierType on the compliance policy before the suggested value * @type {ComputedProperty} * @memberof DatasetComplianceRow */ identifierTypeBeforeSuggestion = computed('identifierType', function(): string | null { return this.getCurrentValueBeforeSuggestion('identifierType'); }); /** * Gets the logicalType on the compliance policy before the suggested value * @type {ComputedProperty} * @memberof DatasetComplianceRow */ logicalTypeBeforeSuggestion = computed('logicalType', function(): string | null { return this.getCurrentValueBeforeSuggestion('logicalType'); }); /** * A list of field formats that are determined based on the field identifierType * @type ComputedProperty> * @memberof DatasetComplianceRow */ fieldFormats = computed('isIdType', function(this: DatasetComplianceRow): Array { const identifierType = get(this, 'field')['identifierType'] || ''; const { isIdType, complianceDataTypes } = getProperties(this, ['isIdType', 'complianceDataTypes']); const complianceDataType = complianceDataTypes.findBy('id', identifierType); let fieldFormatOptions: Array = []; if (complianceDataType && isIdType) { const supportedFieldFormats = complianceDataType.supportedFieldFormats || []; const supportedFormatOptions = supportedFieldFormats.map(format => ({ value: format, label: format })); return supportedFormatOptions.length ? [unSelectedFieldFormatValue, ...supportedFormatOptions] : supportedFormatOptions; } return fieldFormatOptions; }); /** * Flag indicating that this field has an identifier type of idType that is true * @type {ComputedProperty} * @memberof DatasetComplianceRow */ isIdType: ComputedProperty = computed('field.identifierType', function(this: DatasetComplianceRow): boolean { const { field: { identifierType }, complianceDataTypes } = getProperties(this, ['field', 'complianceDataTypes']); const { idType } = complianceDataTypes.findBy('id', identifierType || '') || { idType: false }; return idType; }); /** * Flag indicating that this field has an identifier type that is of pii type * @type {ComputedProperty} * @memberof DatasetComplianceRow */ isPiiType = computed('field.identifierType', function(this: DatasetComplianceRow): boolean { const { identifierType } = get(this, 'field'); const isDefinedIdentifierType = identifierType !== null || identifierType !== ComplianceFieldIdValue.None; // If identifierType exists, and field is not idType or None or null return !!identifierType && !get(this, 'isIdType') && isDefinedIdentifierType; }); /** * The fields logical type, rendered as an Object * If a prediction exists for this field, the predicted value is shown instead * @type {(ComputedProperty)} * @memberof DatasetComplianceRow */ logicalType = computed('field.logicalType', 'prediction', function( this: DatasetComplianceRow ): IComplianceChangeSet['logicalType'] { const { field: { logicalType }, prediction: { logicalType: suggestedLogicalType } = { logicalType: null } } = getProperties(this, ['field', 'prediction']); return suggestedLogicalType || logicalType; }); /** * The field's security classification * Retrieves the field security classification from the compliance field if it exists, otherwise * defaults to the default security classification for the identifier type * in other words, the field must have a security classification if it has an identifier type * @type {ComputedProperty} * @memberof DatasetComplianceRow */ classification = computed('field.classification', 'field.identifierType', 'complianceDataTypes', function( this: DatasetComplianceRow ): IComplianceChangeSet['securityClassification'] { const { field: { identifierType, securityClassification }, complianceDataTypes } = getProperties(this, [ 'field', 'complianceDataTypes' ]); return securityClassification || getDefaultSecurityClassification(complianceDataTypes, identifierType); }); /** * Extracts the field suggestions into a cached computed property, if a suggestion exists * @type {(ComputedProperty<{ identifierType: ComplianceFieldIdValue; logicalType: string; confidence: number } | void>)} * @memberof DatasetComplianceRow */ prediction = computed('field.suggestion', 'field.suggestionAuthority', function( this: DatasetComplianceRow ): { identifierType: IComplianceChangeSet['identifierType']; logicalType: IComplianceChangeSet['logicalType']; confidence: number; } | void { return getFieldSuggestions(getWithDefault(this, 'field', {})); }); actions = { /** * Handles UI changes to the field identifierType * @param {{ value: ComplianceFieldIdValue }} { value } */ onFieldIdentifierTypeChange(this: DatasetComplianceRow, { value }: { value: ComplianceFieldIdValue | null }) { const onFieldIdentifierTypeChange = get(this, 'onFieldIdentifierTypeChange'); if (typeof onFieldIdentifierTypeChange === 'function') { onFieldIdentifierTypeChange(get(this, 'field'), { value }); } }, /** * Handles the updates when the field logical type changes on this field * @param {(IComplianceChangeSet['logicalType'])} value contains the selected drop-down value */ onFieldLogicalTypeChange(this: DatasetComplianceRow, { value }: { value: IComplianceChangeSet['logicalType'] }) { const onFieldLogicalTypeChange = get(this, 'onFieldLogicalTypeChange'); if (typeof onFieldLogicalTypeChange === 'function') { onFieldLogicalTypeChange(get(this, 'field'), value); } }, /** * Handles UI change to field security classification * @param {({ value: '' | Classification })} { value } contains the changed classification value */ onFieldClassificationChange(this: DatasetComplianceRow, { value }: { value: '' | Classification }) { const onFieldClassificationChange = get(this, 'onFieldClassificationChange'); if (typeof onFieldClassificationChange === 'function') { onFieldClassificationChange(get(this, 'field'), { value }); } }, /** * Handles the nonOwner flag update on the field * @param {boolean} nonOwner */ onOwnerChange(this: DatasetComplianceRow, nonOwner: boolean) { get(this, 'onFieldOwnerChange')(get(this, 'field'), nonOwner); }, /** * Handler for user interactions with a suggested value. Applies / ignores the suggestion * Then invokes the parent supplied suggestion handler * @param {string | void} intent a binary indicator to accept or ignore suggestion * @param {SuggestionIntent} intent */ onSuggestionAction(this: DatasetComplianceRow, intent?: SuggestionIntent) { const onSuggestionIntent = get(this, 'onSuggestionIntent'); // Accept the suggestion for either identifierType and/or logicalType if (intent === SuggestionIntent.accept) { const { identifierType, logicalType } = get(this, 'prediction') || { identifierType: void 0, logicalType: void 0 }; if (identifierType) { this.actions.onFieldIdentifierTypeChange.call(this, { value: identifierType }); } if (logicalType) { this.actions.onFieldLogicalTypeChange.call(this, logicalType); } } // Invokes parent handle to runtime ignore future suggesting this suggestion if (typeof onSuggestionIntent === 'function') { onSuggestionIntent(get(this, 'field'), intent); } } }; }