From 1f44bec0c082f9380f2266b5982299a7f2861a16 Mon Sep 17 00:00:00 2001 From: Seyi Adebajo Date: Thu, 12 Apr 2018 23:03:35 -0700 Subject: [PATCH 1/3] create tag row components. create compliance row rollup component. adds compliance field tag factory. makes the policymodificationtime on the schemafieldtopolicyvalue type optional. adds styling for row components. enables some actions for newly create components --- .../dataset-compliance-field-tag.ts | 235 ++++++++++++++++++ .../dataset-compliance-rollup-row.ts | 101 ++++++++ .../app/constants/dataset-compliance.ts | 27 +- .../dataset-compliance/_compliance-table.scss | 83 +++++++ .../dataset-compliance-field-tag.hbs | 12 + .../dataset-compliance-rollup-row.hbs | 11 + .../app/typings/app/dataset-compliance.d.ts | 2 +- 7 files changed, 466 insertions(+), 5 deletions(-) create mode 100644 wherehows-web/app/components/dataset-compliance-field-tag.ts create mode 100644 wherehows-web/app/components/dataset-compliance-rollup-row.ts create mode 100644 wherehows-web/app/templates/components/dataset-compliance-field-tag.hbs create mode 100644 wherehows-web/app/templates/components/dataset-compliance-rollup-row.hbs diff --git a/wherehows-web/app/components/dataset-compliance-field-tag.ts b/wherehows-web/app/components/dataset-compliance-field-tag.ts new file mode 100644 index 0000000000..7fecd7755b --- /dev/null +++ b/wherehows-web/app/components/dataset-compliance-field-tag.ts @@ -0,0 +1,235 @@ +import Component from '@ember/component'; +import ComputedProperty from '@ember/object/computed'; +import { get, getProperties, computed, getWithDefault } from '@ember/object'; +import { + Classification, + ComplianceFieldIdValue, + getDefaultSecurityClassification, + idTypeFieldHasLogicalType, + isFieldIdType, + SuggestionIntent +} from 'wherehows-web/constants'; +import { + IComplianceChangeSet, + IComplianceFieldFormatOption, + IDropDownOption +} from 'wherehows-web/typings/app/dataset-compliance'; +import { IComplianceDataType } from 'wherehows-web/typings/api/list/compliance-datatypes'; +import { action } from 'ember-decorators/object'; +import { getFieldSuggestions } from 'wherehows-web/utils/datasets/compliance-suggestions'; + +/** + * Constant definition for an unselected field format + * @type {IDropDownOption} + */ +const unSelectedFieldFormatValue: IDropDownOption = { + value: null, + label: 'Select Field Format...', + isDisabled: true +}; + +export default class DatasetComplianceFieldTag extends Component { + tagName = 'tr'; + + /** + * Describes action interface for `onSuggestionIntent` action + * @memberof DatasetComplianceFieldTag + */ + onSuggestionIntent: (tag: IComplianceChangeSet, intent?: SuggestionIntent) => void; + + /** + * Describes action interface for `onTagIdentifierTypeChange` action + * @memberof DatasetComplianceFieldTag + */ + onTagIdentifierTypeChange: (tag: IComplianceChangeSet, option: { value: ComplianceFieldIdValue | null }) => void; + + /** + * References the change set item / tag to be added to the parent field + * @type {IComplianceChangeSet} + * @memberof DatasetComplianceFieldTag + */ + tag: IComplianceChangeSet; + + /** + * Reference to the compliance data types + * @type {Array} + */ + complianceDataTypes: Array; + + /** + * Flag indicating that this tag has an identifier type of idType that is true + * @type {ComputedProperty} + * @memberof DatasetComplianceFieldTag + */ + isIdType: ComputedProperty = computed('tag.identifierType', 'complianceDataTypes', function( + this: DatasetComplianceFieldTag + ): boolean { + const { tag, complianceDataTypes } = getProperties(this, ['tag', 'complianceDataTypes']); + return isFieldIdType(complianceDataTypes)(tag); + }); + + /** + * A list of field formats that are determined based on the tag identifierType + * @type ComputedProperty> + * @memberof DatasetComplianceFieldTag + */ + fieldFormats: ComputedProperty> = computed( + 'isIdType', + 'complianceDataTypes', + function(this: DatasetComplianceFieldTag): Array { + const identifierType = get(this, 'tag')['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; + } + ); + + /** + * Checks if the field format / logical type for this tag is missing, if the field is of ID type + * @type {ComputedProperty} + * @memberof DatasetComplianceFieldTag + */ + isTagFormatMissing = computed('isIdType', 'tag.logicalType', function(this: DatasetComplianceFieldTag): boolean { + return get(this, 'isIdType') && !idTypeFieldHasLogicalType(get(this, 'tag')); + }); + + /** + * The tag's security classification + * Retrieves the tag security classification from the compliance tag 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 DatasetComplianceFieldTag + */ + tagClassification = computed('tag.classification', 'tag.identifierType', 'complianceDataTypes', function( + this: DatasetComplianceFieldTag + ): IComplianceChangeSet['securityClassification'] { + const { tag: { identifierType, securityClassification }, complianceDataTypes } = getProperties(this, [ + 'tag', + 'complianceDataTypes' + ]); + + return securityClassification || getDefaultSecurityClassification(complianceDataTypes, identifierType); + }); + + /** + * Flag indicating that this tag has an identifier type that is of pii type + * @type {ComputedProperty} + * @memberof DatasetComplianceFieldTag + */ + isPiiType = computed('tag.identifierType', function(this: DatasetComplianceFieldTag): boolean { + const { identifierType } = get(this, 'tag'); + const isDefinedIdentifierType = identifierType !== null || identifierType !== ComplianceFieldIdValue.None; + + // If identifierType exists, and tag is not idType or None or null + return !!identifierType && !get(this, 'isIdType') && isDefinedIdentifierType; + }); + + /** + * Extracts the tag suggestions into a cached computed property, if a suggestion exists + * @type {(ComputedProperty<{ identifierType: ComplianceFieldIdValue; logicalType: string; confidence: number } | void>)} + * @memberof DatasetComplianceFieldTag + */ + prediction = computed('tag.suggestion', 'tag.suggestionAuthority', function( + this: DatasetComplianceFieldTag + ): { + identifierType: IComplianceChangeSet['identifierType']; + logicalType: IComplianceChangeSet['logicalType']; + confidence: number; + } | void { + return getFieldSuggestions(getWithDefault(this, 'tag', {})); + }); + + /** + * Handles UI changes to the tag identifierType + * @param {{ value: ComplianceFieldIdValue }} { value } + */ + @action + tagIdentifierTypeDidChange(this: DatasetComplianceFieldTag, { value }: { value: ComplianceFieldIdValue | null }) { + const onTagIdentifierTypeChange = get(this, 'onTagIdentifierTypeChange'); + + if (typeof onTagIdentifierTypeChange === 'function') { + // if the field has a predicted value, but the user changes the identifier type, + // ignore the suggestion + if (get(this, 'prediction')) { + this.onSuggestionAction(SuggestionIntent.ignore); + } + + onTagIdentifierTypeChange(get(this, 'tag'), { value }); + } + } + + /** + * Handles the updates when the tag's logical type changes on this tag + * @param {(IComplianceChangeSet['logicalType'])} value contains the selected drop-down value + */ + @action + tagLogicalTypeDidChange(this: DatasetComplianceFieldTag, { value }: { value: IComplianceChangeSet['logicalType'] }) { + console.log(value); + } + + /** + * Handles UI change to field security classification + * @param {({ value: '' | Classification })} { value } contains the changed classification value + */ + @action + tagClassificationDidChange(this: DatasetComplianceFieldTag, { value }: { value: '' | Classification }) { + // const onFieldClassificationChange = get(this, 'onFieldClassificationChange'); + // if (typeof onFieldClassificationChange === 'function') { + // onFieldClassificationChange(get(this, 'tag'), { value }); + // } + console.log(value); + } + + /** + * Handles the nonOwner flag update on the tag + * @param {boolean} nonOwner + */ + @action + tagOwnerDidChange(this: DatasetComplianceFieldTag, nonOwner: boolean) { + // get(this, 'onFieldOwnerChange')(get(this, 'field'), nonOwner); + console.log(nonOwner); + } + + /** + * Handler for user interactions with a suggested value. Applies / ignores the suggestion + * Then invokes the parent supplied suggestion handler + * @param {SuggestionIntent} intent a binary indicator to accept or ignore suggestion + */ + @action + onSuggestionAction(this: DatasetComplianceFieldTag, intent: SuggestionIntent = SuggestionIntent.ignore) { + 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.tagIdentifierTypeDidChange.call(this, { value: identifierType }); + } + + if (logicalType) { + this.actions.tagLogicalTypeDidChange.call(this, logicalType); + } + } + + // Invokes parent handle to runtime ignore future suggesting this suggestion + if (typeof onSuggestionIntent === 'function') { + onSuggestionIntent(get(this, 'tag'), intent); + } + } +} diff --git a/wherehows-web/app/components/dataset-compliance-rollup-row.ts b/wherehows-web/app/components/dataset-compliance-rollup-row.ts new file mode 100644 index 0000000000..db1adaff21 --- /dev/null +++ b/wherehows-web/app/components/dataset-compliance-rollup-row.ts @@ -0,0 +1,101 @@ +import Component from '@ember/component'; +import ComputedProperty, { alias } from '@ember/object/computed'; +import { get, set, getProperties, computed } from '@ember/object'; +import { action } from 'ember-decorators/object'; +import { + IComplianceChangeSet, + IdentifierFieldWithFieldChangeSetTuple +} from 'wherehows-web/typings/app/dataset-compliance'; +import { complianceFieldTagFactory } from 'wherehows-web/constants'; + +export default class DatasetComplianceRollupRow extends Component.extend({ + tagName: '' +}) { + /** + * References the parent external action to add a tag to the list of change sets + */ + onFieldTagAdded: (tag: IComplianceChangeSet) => IComplianceChangeSet; + + /** + * Flag indicating if the row is expanded or collapsed + * @type {boolean} + * @memberof DatasetComplianceRollupRow + */ + isRowExpanded: boolean; + + /** + * References the compliance field tuple containing the field name and the field change set properties + * @type {IdentifierFieldWithFieldChangeSetTuple} + * @memberof DatasetComplianceRollupRow + */ + field: IdentifierFieldWithFieldChangeSetTuple; + + constructor() { + super(...arguments); + const isDirty: boolean = !!get(this, 'isRowDirty'); + + // if any tag is dirty, then expand the parent row on instantiation + this.isRowExpanded || (this.isRowExpanded = isDirty); + } + + /** + * References the first item in the IdentifierFieldWithFieldChangeSetTuple tuple, which is the field name + * @type {ComputedProperty} + * @memberof DatasetComplianceRollupRow + */ + identifierField: ComputedProperty = alias('field.firstObject'); + + /** + * References the second item in the IdentifierFieldWithFieldChangeSetTuple type, this is the list of tags + * for this field + * @type {ComputedProperty>} + * @memberof DatasetComplianceRollupRow + */ + fieldChangeSet: ComputedProperty> = alias('field.1'); + + /** + * Aliases the dataType property on the first item in the field change set, this should available + * regardless of if the field already exists on the compliance policy or otherwise + * @type {ComputedProperty} + * @memberof DatasetComplianceRollupRow + */ + dataType: ComputedProperty = alias('fieldChangeSet.firstObject.dataType'); + + /** + * Checks if any of the field tags for this row are dirty + * @type {ComputedProperty} + * @memberof DatasetComplianceRollupRow + */ + isRowDirty: ComputedProperty = computed('fieldChangeSet', function( + this: DatasetComplianceRollupRow + ): boolean { + return get(this, 'fieldChangeSet').some(tag => tag.isDirty); + }); + + /** + * Toggles the expansion / collapse of the row expansion flag + * @memberof DatasetComplianceRollupRow + */ + @action + onToggleRowExpansion() { + this.toggleProperty('isRowExpanded'); + } + + /** + * Handles adding a field tag when the user indicates the action through the UI + * @memberof DatasetComplianceRollupRow + */ + @action + onAddFieldTag(this: DatasetComplianceRollupRow) { + const { identifierField, dataType, onFieldTagAdded } = getProperties(this, [ + 'identifierField', + 'dataType', + 'onFieldTagAdded' + ]); + + if (typeof onFieldTagAdded === 'function') { + onFieldTagAdded(complianceFieldTagFactory({ identifierField, dataType })); + set(this, 'isRowExpanded', true); + } + } +} diff --git a/wherehows-web/app/constants/dataset-compliance.ts b/wherehows-web/app/constants/dataset-compliance.ts index 595c7f7bff..f16f826962 100644 --- a/wherehows-web/app/constants/dataset-compliance.ts +++ b/wherehows-web/app/constants/dataset-compliance.ts @@ -12,7 +12,8 @@ import { IdentifierFieldWithFieldChangeSetTuple, IIdentifierFieldWithFieldChangeSetObject, ISchemaFieldsToPolicy, - ISchemaFieldsToSuggested + ISchemaFieldsToSuggested, + SchemaFieldToPolicyValue } from 'wherehows-web/typings/app/dataset-compliance'; import { IColumnFieldProps, @@ -228,7 +229,7 @@ const mergeMappedColumnFieldsWithSuggestions = ( fieldSuggestionMap: ISchemaFieldsToSuggested = {} ): Array => Object.keys(mappedColumnFields).map(fieldName => { - const field = pick(mappedColumnFields[fieldName], [ + const field: IComplianceChangeSet = pick(mappedColumnFields[fieldName], [ 'identifierField', 'dataType', 'identifierType', @@ -263,7 +264,7 @@ const foldComplianceChangeSetToField = ( changeSet: IComplianceChangeSet ): IIdentifierFieldWithFieldChangeSetObject => ({ ...identifierFieldMap, - [changeSet.identifierField]: [...identifierFieldMap[changeSet.identifierField], changeSet] + [changeSet.identifierField]: [...(identifierFieldMap[changeSet.identifierField] || []), changeSet] }); /** @@ -339,6 +340,23 @@ const mapSchemaColumnPropsToCurrentPrivacyPolicy = ({ }: ISchemaColumnMappingProps): ISchemaFieldsToPolicy => arrayReduce(columnToPolicyReducingFn(complianceEntities, policyModificationTime), {})(columnProps); +/** + * Creates a new tag / change set item for a compliance entity / field with default properties + * @param {IColumnFieldProps} { identifierField, dataType } the runtime properties to apply to the created instance + * @returns {SchemaFieldToPolicyValue} + */ +const complianceFieldTagFactory = ({ identifierField, dataType }: IColumnFieldProps): SchemaFieldToPolicyValue => ({ + identifierField, + dataType, + identifierType: null, + logicalType: null, + securityClassification: null, + nonOwner: null, + readonly: false, + privacyPolicyExists: false, + isDirty: true +}); + /** * Takes the current compliance entities, and mod time and returns a reducer that consumes a list of IColumnFieldProps * instances and maps each entry to a compliance entity on the current compliance policy @@ -392,5 +410,6 @@ export { changeSetFieldsRequiringReview, changeSetReviewableAttributeTriggers, mapSchemaColumnPropsToCurrentPrivacyPolicy, - foldComplianceChangeSets + foldComplianceChangeSets, + complianceFieldTagFactory }; diff --git a/wherehows-web/app/styles/components/dataset-compliance/_compliance-table.scss b/wherehows-web/app/styles/components/dataset-compliance/_compliance-table.scss index 39e725cb3c..76e2aa4e41 100644 --- a/wherehows-web/app/styles/components/dataset-compliance/_compliance-table.scss +++ b/wherehows-web/app/styles/components/dataset-compliance/_compliance-table.scss @@ -3,6 +3,10 @@ $compliance-readonly-color: get-color(red7); $compliance-review-required-color: get-color(blue5); $compliance-ok-color: get-color(green5); + @mixin select-wrapper { + max-width: item-spacing(9) * 2; + display: inline-flex; + } &__has-suggestions { color: $compliance-suggestion-color; @@ -17,6 +21,14 @@ width: 5%; } + &__identifier-column { + width: 20%; + } + + &__identifier-cell { + text-align: right; + } + &__classification-column { width: 20%; } @@ -77,6 +89,77 @@ } } } + + &__add-field { + &#{&} { + font-weight: fw(normal, 4); + } + } + + &__rollup-toggle { + &#{&} { + color: get-color(gray7); + } + } + + &__remove-tag { + &#{&} { + font-weight: fw(normal, 2); + font-size: item-spacing(6); + background-color: transparent; + text-align: center; + text-decoration: none; + vertical-align: middle; + cursor: pointer; + box-sizing: border-box; + color: get-color(black, 0.7); + border: 0; + } + } + + &__select-wrapper { + @include select-wrapper; + } + + &__identifier-select { + $border: 1px solid get-color(black, 0.7); + + @include select-wrapper; + position: relative; + + &:before { + content: ' '; + display: block; + position: absolute; + width: item-spacing(2); + height: 50%; + border-bottom: $border; + border-left: $border; + top: 2px; + left: -(item-spacing(3)); + } + } + + &__tag-options { + display: flex; + align-items: center; + white-space: nowrap; + } + + &__tag-label { + margin: item-spacing(0 2); + font-weight: fw(normal, 2); + } + + &__tag-row { + background-color: get-color(gray0); + + &#{&} { + td { + border-bottom: 0; + } + } + } } .compliance-depends { diff --git a/wherehows-web/app/templates/components/dataset-compliance-field-tag.hbs b/wherehows-web/app/templates/components/dataset-compliance-field-tag.hbs new file mode 100644 index 0000000000..2b73e2329c --- /dev/null +++ b/wherehows-web/app/templates/components/dataset-compliance-field-tag.hbs @@ -0,0 +1,12 @@ +{{yield (hash + rowId=elementId + isIdType=isIdType + isPiiType=isPiiType + fieldFormats=fieldFormats + isTagFormatMissing=isTagFormatMissing + tagClassification=tagClassification + tagIdentifierTypeDidChange=(action "tagIdentifierTypeDidChange") + tagLogicalTypeDidChange=(action "tagLogicalTypeDidChange") + tagOwnerDidChange=(action "tagOwnerDidChange") + tagClassificationDidChange=(action "tagClassificationDidChange") + )}} \ No newline at end of file diff --git a/wherehows-web/app/templates/components/dataset-compliance-rollup-row.hbs b/wherehows-web/app/templates/components/dataset-compliance-rollup-row.hbs new file mode 100644 index 0000000000..af5255ef5f --- /dev/null +++ b/wherehows-web/app/templates/components/dataset-compliance-rollup-row.hbs @@ -0,0 +1,11 @@ +{{yield (hash + cell=(component 'dataset-table-cell') + isRowExpanded=isRowExpanded + fieldChangeSet=fieldChangeSet + identifierField=identifierField + dataType=dataType + isIdType=isIdType + fieldFormats=fieldFormats + onToggleRowExpansion=(action "onToggleRowExpansion") + onAddFieldTag=(action "onAddFieldTag") + )}} \ No newline at end of file diff --git a/wherehows-web/app/typings/app/dataset-compliance.d.ts b/wherehows-web/app/typings/app/dataset-compliance.d.ts index faf1a704ec..f21e4c35c2 100644 --- a/wherehows-web/app/typings/app/dataset-compliance.d.ts +++ b/wherehows-web/app/typings/app/dataset-compliance.d.ts @@ -30,7 +30,7 @@ type SchemaFieldToPolicyValue = Pick< privacyPolicyExists: boolean; // flag indicating the field changeSet has been modified on the client isDirty: boolean; - policyModificationTime: IComplianceInfo['modifiedTime']; + policyModificationTime?: IComplianceInfo['modifiedTime']; dataType: string; }; From 4ad0d44954270a2266aa68ee3dd0e720ae6497df Mon Sep 17 00:00:00 2001 From: Seyi Adebajo Date: Fri, 13 Apr 2018 00:38:53 -0700 Subject: [PATCH 2/3] adds actions for removing a field tag. actions for field tag operations including classification change, owner change, logical type change. inverts display and logic for nonOwner attribute --- .../dataset-compliance-field-tag.ts | 34 +++- .../dataset-compliance-rollup-row.ts | 25 ++- .../app/components/dataset-compliance.ts | 174 +++++++++--------- .../dataset-compliance-field-tag.hbs | 20 +- .../dataset-compliance-rollup-row.hbs | 19 +- 5 files changed, 154 insertions(+), 118 deletions(-) diff --git a/wherehows-web/app/components/dataset-compliance-field-tag.ts b/wherehows-web/app/components/dataset-compliance-field-tag.ts index 7fecd7755b..5cc98ae3c0 100644 --- a/wherehows-web/app/components/dataset-compliance-field-tag.ts +++ b/wherehows-web/app/components/dataset-compliance-field-tag.ts @@ -43,6 +43,21 @@ export default class DatasetComplianceFieldTag extends Component { */ onTagIdentifierTypeChange: (tag: IComplianceChangeSet, option: { value: ComplianceFieldIdValue | null }) => void; + /** + * Describes the parent action interface for `onTagLogicalTypeChange` + */ + onTagLogicalTypeChange: (tag: IComplianceChangeSet, value: IComplianceChangeSet['logicalType']) => void; + + /** + * Describes the parent action interface for `onTagClassificationChange` + */ + onTagClassificationChange: (tag: IComplianceChangeSet, option: { value: '' | Classification }) => void; + + /** + * Describes the parent action interface for `onTagOwnerChange` + */ + onTagOwnerChange: (tag: IComplianceChangeSet, nonOwner: boolean) => void; + /** * References the change set item / tag to be added to the parent field * @type {IComplianceChangeSet} @@ -176,7 +191,11 @@ export default class DatasetComplianceFieldTag extends Component { */ @action tagLogicalTypeDidChange(this: DatasetComplianceFieldTag, { value }: { value: IComplianceChangeSet['logicalType'] }) { - console.log(value); + const onTagLogicalTypeChange = get(this, 'onTagLogicalTypeChange'); + + if (typeof onTagLogicalTypeChange === 'function') { + onTagLogicalTypeChange(get(this, 'tag'), value); + } } /** @@ -185,11 +204,10 @@ export default class DatasetComplianceFieldTag extends Component { */ @action tagClassificationDidChange(this: DatasetComplianceFieldTag, { value }: { value: '' | Classification }) { - // const onFieldClassificationChange = get(this, 'onFieldClassificationChange'); - // if (typeof onFieldClassificationChange === 'function') { - // onFieldClassificationChange(get(this, 'tag'), { value }); - // } - console.log(value); + const onTagClassificationChange = get(this, 'onTagClassificationChange'); + if (typeof onTagClassificationChange === 'function') { + onTagClassificationChange(get(this, 'tag'), { value }); + } } /** @@ -198,8 +216,8 @@ export default class DatasetComplianceFieldTag extends Component { */ @action tagOwnerDidChange(this: DatasetComplianceFieldTag, nonOwner: boolean) { - // get(this, 'onFieldOwnerChange')(get(this, 'field'), nonOwner); - console.log(nonOwner); + // inverts the value of nonOwner, toggle is shown in the UI as `Owner` i.e. not nonOwner + get(this, 'onTagOwnerChange')(get(this, 'tag'), !nonOwner); } /** diff --git a/wherehows-web/app/components/dataset-compliance-rollup-row.ts b/wherehows-web/app/components/dataset-compliance-rollup-row.ts index db1adaff21..32d870e094 100644 --- a/wherehows-web/app/components/dataset-compliance-rollup-row.ts +++ b/wherehows-web/app/components/dataset-compliance-rollup-row.ts @@ -1,6 +1,6 @@ import Component from '@ember/component'; import ComputedProperty, { alias } from '@ember/object/computed'; -import { get, set, getProperties, computed } from '@ember/object'; +import { get, getProperties, computed } from '@ember/object'; import { action } from 'ember-decorators/object'; import { IComplianceChangeSet, @@ -16,6 +16,11 @@ export default class DatasetComplianceRollupRow extends Component.extend({ */ onFieldTagAdded: (tag: IComplianceChangeSet) => IComplianceChangeSet; + /** + * References the parent external action to add a tag to the list of change sets + */ + onFieldTagRemoved: (tag: IComplianceChangeSet) => IComplianceChangeSet; + /** * Flag indicating if the row is expanded or collapsed * @type {boolean} @@ -95,7 +100,23 @@ export default class DatasetComplianceRollupRow extends Component.extend({ if (typeof onFieldTagAdded === 'function') { onFieldTagAdded(complianceFieldTagFactory({ identifierField, dataType })); - set(this, 'isRowExpanded', true); + } + } + + /** + * Handles the removal of a field tag from the list of change set items + * @param {IComplianceChangeSet} tag + * @memberof DatasetComplianceRollupRow + */ + @action + onRemoveFieldTag(this: DatasetComplianceRollupRow, tag: IComplianceChangeSet) { + const onFieldTagRemoved = get(this, 'onFieldTagRemoved'); + //@ts-ignore dot notation access is ts limitation with ember object model + const isSoleTag = get(this, 'fieldChangeSet.length') === 1; + ``; + + if (typeof onFieldTagRemoved === 'function' && !isSoleTag) { + onFieldTagRemoved(tag); } } } diff --git a/wherehows-web/app/components/dataset-compliance.ts b/wherehows-web/app/components/dataset-compliance.ts index 1566885992..1b08561fed 100644 --- a/wherehows-web/app/components/dataset-compliance.ts +++ b/wherehows-web/app/components/dataset-compliance.ts @@ -746,8 +746,8 @@ export default class DatasetCompliance extends Component { ); /** - * Sets the default classification for the given identifier field - * Using the identifierType, determine the field's default security classification based on a values + * Sets the default classification for the given identifier field's tag + * Using the identifierType, determine the tag's default security classification based on a values * supplied by complianceDataTypes endpoint * @param {string} identifierField the field for which the default classification should apply * @param {ComplianceFieldIdValue} identifierType the value of the field's identifier type @@ -759,7 +759,7 @@ export default class DatasetCompliance extends Component { const complianceDataTypes = get(this, 'complianceDataTypes'); const defaultSecurityClassification = getDefaultSecurityClassification(complianceDataTypes, identifierType); - this.actions.onFieldClassificationChange.call(this, { identifierField }, { value: defaultSecurityClassification }); + this.actions.tagClassificationChanged.call(this, { identifierField }, { value: defaultSecurityClassification }); } /** @@ -932,6 +932,88 @@ export default class DatasetCompliance extends Component { } actions: IDatasetComplianceActions = { + /** + * Adds a new field tag to the list of compliance change set items + * @param {IComplianceChangeSet} tag properties for new field tag + * @return {IComplianceChangeSet} + */ + onFieldTagAdded(this: DatasetCompliance, tag: IComplianceChangeSet): IComplianceChangeSet { + return get(this, 'compliancePolicyChangeSet').addObject(tag); + }, + + /** + * Removes a field tag from the list of compliance change set items + * @param {IComplianceChangeSet} tag + * @return {IComplianceChangeSet} + */ + onFieldTagRemoved(this: DatasetCompliance, tag: IComplianceChangeSet): IComplianceChangeSet { + return get(this, 'compliancePolicyChangeSet').removeObject(tag); + }, + + /** + * When a user updates the identifierFieldType, update working copy + * @param {IComplianceChangeSet} tag + * @param {ComplianceFieldIdValue} identifierType + */ + tagIdentifierChanged( + this: DatasetCompliance, + tag: IComplianceChangeSet, + { value: identifierType }: { value: ComplianceFieldIdValue } + ) { + const { identifierField } = tag; + if (tag) { + setProperties(tag, { + identifierType, + logicalType: null, + nonOwner: null, + isDirty: true + }); + } + + this.setDefaultClassification({ identifierField, identifierType }); + }, + + /** + * Updates the logical type for a tag + * @param {IComplianceChangeSet} tag the tag to be updated + * @param {IComplianceChangeSet.logicalType} logicalType the updated logical type + */ + tagLogicalTypeChanged( + this: DatasetCompliance, + tag: IComplianceChangeSet, + logicalType: IComplianceChangeSet['logicalType'] + ) { + setProperties(tag, { logicalType, isDirty: true }); + }, + + /** + * Updates the security classification on a field tag + * @param {IComplianceChangeSet} tag the tag to be updated + * @param {IComplianceChangeSet.securityClassification} securityClassification the updated security classification value + */ + tagClassificationChanged( + this: DatasetCompliance, + tag: IComplianceChangeSet, + { value: securityClassification = null }: { value: IComplianceChangeSet['securityClassification'] } + ) { + setProperties(tag, { + securityClassification, + isDirty: true + }); + }, + + /** + * Updates the nonOwner property on the tag + * @param {IComplianceChangeSet} tag the field tag to be updated + * @param {IComplianceChangeSet.nonOwner} nonOwner flag indicating the field property is a nonOwner + */ + tagOwnerChanged(this: DatasetCompliance, tag: IComplianceChangeSet, nonOwner: IComplianceChangeSet['nonOwner']) { + setProperties(tag, { + nonOwner, + isDirty: true + }); + }, + /** * Sets each datasetClassification value as false * @returns {Promise} @@ -1170,92 +1252,6 @@ export default class DatasetCompliance extends Component { anchor.click(); }, - /** - * When a user updates the identifierFieldType in the DOM, update the backing store - * @param {String} identifierField - * @param {String} logicalType - * @param {String} identifierType - */ - onFieldIdentifierTypeChange( - this: DatasetCompliance, - { identifierField }: IComplianceChangeSet, - { value: identifierType }: { value: ComplianceFieldIdValue } - ) { - const complianceEntitiesChangeSet = get(this, 'compliancePolicyChangeSet'); - // A reference to the current field in the compliance list, it should exist even for empty complianceEntities - // since this is a reference created in the working copy: compliancePolicyChangeSet - const changeSetComplianceField = complianceEntitiesChangeSet.findBy('identifierField', identifierField); - - // Reset field attributes on change to field in change set - if (changeSetComplianceField) { - setProperties(changeSetComplianceField, { - identifierType, - logicalType: null, - nonOwner: false, - isDirty: true - }); - } - - // Set the defaultClassification for the identifierField, - this.setDefaultClassification({ identifierField, identifierType }); - }, - - /** - * Updates the logical type for the given identifierField - * @param {IComplianceChangeSet} field - * @param {IComplianceChangeSet.logicalType} logicalType - */ - onFieldLogicalTypeChange( - this: DatasetCompliance, - field: IComplianceChangeSet, - logicalType: IComplianceChangeSet['logicalType'] - ) { - setProperties(field, { logicalType, isDirty: true }); - }, - - /** - * Updates the field security classification - * @param {IComplianceChangeSet} { identifierField } the identifier field to update the classification for - * @param {{value: IComplianceChangeSet.classification}} { value: classification = null } - */ - onFieldClassificationChange( - this: DatasetCompliance, - { identifierField }: IComplianceChangeSet, - { value: securityClassification = null }: { value: IComplianceChangeSet['securityClassification'] } - ) { - const currentFieldInComplianceList = get(this, 'compliancePolicyChangeSet').findBy( - 'identifierField', - identifierField - ); - - // Apply the updated classification value to the current instance of the field in working copy - if (currentFieldInComplianceList) { - setProperties(currentFieldInComplianceList, { - securityClassification, - isDirty: true - }); - } - }, - - /** - * Updates the field non owner flag - * @param {IComplianceChangeSet} { identifierField } - * @param {IComplianceChangeSet.nonOwner} nonOwner - */ - onFieldOwnerChange( - this: DatasetCompliance, - { identifierField }: IComplianceChangeSet, - nonOwner: IComplianceChangeSet['nonOwner'] - ) { - const currentFieldInComplianceList = get(this, 'compliancePolicyChangeSet').findBy( - 'identifierField', - identifierField - ); - if (currentFieldInComplianceList) { - setProperties(currentFieldInComplianceList, { nonOwner, isDirty: true }); - } - }, - /** * Updates the source object representing the current datasetClassification map * @param {keyof typeof DatasetClassifiers} classifier the property on the datasetClassification to update diff --git a/wherehows-web/app/templates/components/dataset-compliance-field-tag.hbs b/wherehows-web/app/templates/components/dataset-compliance-field-tag.hbs index 2b73e2329c..d6fd53c9eb 100644 --- a/wherehows-web/app/templates/components/dataset-compliance-field-tag.hbs +++ b/wherehows-web/app/templates/components/dataset-compliance-field-tag.hbs @@ -1,12 +1,12 @@ {{yield (hash - rowId=elementId - isIdType=isIdType - isPiiType=isPiiType - fieldFormats=fieldFormats - isTagFormatMissing=isTagFormatMissing - tagClassification=tagClassification - tagIdentifierTypeDidChange=(action "tagIdentifierTypeDidChange") - tagLogicalTypeDidChange=(action "tagLogicalTypeDidChange") - tagOwnerDidChange=(action "tagOwnerDidChange") - tagClassificationDidChange=(action "tagClassificationDidChange") + rowId=elementId + isIdType=isIdType + isPiiType=isPiiType + fieldFormats=fieldFormats + isTagFormatMissing=isTagFormatMissing + tagClassification=tagClassification + tagIdentifierTypeDidChange=(action "tagIdentifierTypeDidChange") + tagLogicalTypeDidChange=(action "tagLogicalTypeDidChange") + tagOwnerDidChange=(action "tagOwnerDidChange") + tagClassificationDidChange=(action "tagClassificationDidChange") )}} \ No newline at end of file diff --git a/wherehows-web/app/templates/components/dataset-compliance-rollup-row.hbs b/wherehows-web/app/templates/components/dataset-compliance-rollup-row.hbs index af5255ef5f..982c21a4aa 100644 --- a/wherehows-web/app/templates/components/dataset-compliance-rollup-row.hbs +++ b/wherehows-web/app/templates/components/dataset-compliance-rollup-row.hbs @@ -1,11 +1,12 @@ {{yield (hash - cell=(component 'dataset-table-cell') - isRowExpanded=isRowExpanded - fieldChangeSet=fieldChangeSet - identifierField=identifierField - dataType=dataType - isIdType=isIdType - fieldFormats=fieldFormats - onToggleRowExpansion=(action "onToggleRowExpansion") - onAddFieldTag=(action "onAddFieldTag") + cell=(component 'dataset-table-cell') + isRowExpanded=isRowExpanded + fieldChangeSet=fieldChangeSet + identifierField=identifierField + dataType=dataType + isIdType=isIdType + fieldFormats=fieldFormats + onToggleRowExpansion=(action "onToggleRowExpansion") + onAddFieldTag=(action "onAddFieldTag") + onRemoveFieldTag=(action "onRemoveFieldTag") )}} \ No newline at end of file From 2615543051294af7be4df3d89b358e772106d453 Mon Sep 17 00:00:00 2001 From: Seyi Adebajo Date: Fri, 13 Apr 2018 01:52:51 -0700 Subject: [PATCH 3/3] removes uniqueness check for compliance field entities and fixes validation check for field length greater than compliance entities with distinct --- .../app/components/dataset-compliance.ts | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/wherehows-web/app/components/dataset-compliance.ts b/wherehows-web/app/components/dataset-compliance.ts index 1b08561fed..63061f1fc0 100644 --- a/wherehows-web/app/components/dataset-compliance.ts +++ b/wherehows-web/app/components/dataset-compliance.ts @@ -57,6 +57,7 @@ import { ISecurityClassificationOption, ShowAllShowReview } from 'wherehows-web/typings/app/dataset-compliance'; +import { uniqBy } from 'lodash'; const { complianceDataException, @@ -535,11 +536,14 @@ export default class DatasetCompliance extends Component { * to what is available on the dataset schema * @return {boolean} */ - isSchemaFieldLengthGreaterThanComplianceEntities(this: DatasetCompliance): boolean { + isSchemaFieldLengthGreaterThanUniqComplianceEntities(this: DatasetCompliance): boolean { const complianceInfo = get(this, 'complianceInfo'); if (complianceInfo) { const { length: columnFieldsLength } = getWithDefault(this, 'schemaFieldNamesMappedToDataTypes', []); - const { length: complianceListLength } = get(complianceInfo, 'complianceEntities') || []; + const { length: complianceListLength } = uniqBy( + get(complianceInfo, 'complianceEntities') || [], + 'identifierField' + ); return columnFieldsLength >= complianceListLength; } @@ -825,7 +829,7 @@ export default class DatasetCompliance extends Component { // Create confirmation dialog get(this, 'notifications').notify(NotificationEvent.confirm, { - header: 'Confirm fields marked as `none`', + header: 'Confirm fields to tagged as `none` field type', content: `There are ${unformatted.length} non-ID fields. `, dialogActions: dialogActions }); @@ -856,15 +860,9 @@ export default class DatasetCompliance extends Component { // Validation operations const idFieldsHaveValidLogicalType: boolean = idTypeFieldsHaveLogicalType(idTypeComplianceEntities); - const fieldIdentifiersAreUnique: boolean = isListUnique(complianceEntities.mapBy('identifierField')); - const schemaFieldLengthGreaterThanComplianceEntities: boolean = this.isSchemaFieldLengthGreaterThanComplianceEntities(); + const isSchemaFieldLengthGreaterThanUniqComplianceEntities: boolean = this.isSchemaFieldLengthGreaterThanUniqComplianceEntities(); - if (!fieldIdentifiersAreUnique) { - notify(NotificationEvent.error, { content: complianceFieldNotUnique }); - return Promise.reject(new Error(complianceFieldNotUnique)); - } - - if (!schemaFieldLengthGreaterThanComplianceEntities) { + if (!isSchemaFieldLengthGreaterThanUniqComplianceEntities) { notify(NotificationEvent.error, { content: complianceDataException }); return Promise.reject(new Error(complianceFieldNotUnique)); }