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..5cc98ae3c0 --- /dev/null +++ b/wherehows-web/app/components/dataset-compliance-field-tag.ts @@ -0,0 +1,253 @@ +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; + + /** + * 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} + * @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'] }) { + const onTagLogicalTypeChange = get(this, 'onTagLogicalTypeChange'); + + if (typeof onTagLogicalTypeChange === 'function') { + onTagLogicalTypeChange(get(this, 'tag'), 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 onTagClassificationChange = get(this, 'onTagClassificationChange'); + if (typeof onTagClassificationChange === 'function') { + onTagClassificationChange(get(this, 'tag'), { value }); + } + } + + /** + * Handles the nonOwner flag update on the tag + * @param {boolean} nonOwner + */ + @action + tagOwnerDidChange(this: DatasetComplianceFieldTag, nonOwner: boolean) { + // inverts the value of nonOwner, toggle is shown in the UI as `Owner` i.e. not nonOwner + get(this, 'onTagOwnerChange')(get(this, 'tag'), !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..32d870e094 --- /dev/null +++ b/wherehows-web/app/components/dataset-compliance-rollup-row.ts @@ -0,0 +1,122 @@ +import Component from '@ember/component'; +import ComputedProperty, { alias } from '@ember/object/computed'; +import { get, 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; + + /** + * 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} + * @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 })); + } + } + + /** + * 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..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; } @@ -746,8 +750,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 +763,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 }); } /** @@ -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)); } @@ -932,6 +930,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 +1250,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/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..d6fd53c9eb --- /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..982c21a4aa --- /dev/null +++ b/wherehows-web/app/templates/components/dataset-compliance-rollup-row.hbs @@ -0,0 +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") + onRemoveFieldTag=(action "onRemoveFieldTag") + )}} \ 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; };