implements advanced edit mode for compliance policy updates

This commit is contained in:
Seyi Adebajo 2018-06-13 15:12:23 -07:00
parent ace801d1c1
commit ce97189833
15 changed files with 624 additions and 381 deletions

View File

@ -67,6 +67,10 @@ import { IdLogicalType, NonIdLogicalType } from 'wherehows-web/constants/dataset
import { pick } from 'lodash';
import { trackableEvent, TrackableEventCategory } from 'wherehows-web/constants/analytics/event-tracking';
import { notificationDialogActionFactory } from 'wherehows-web/utils/notifications/notifications';
import validateMetadataObject, {
complianceEntitiesTaxonomy
} from 'wherehows-web/utils/datasets/compliance/metadata-schema';
import { fleece } from 'wherehows-web/utils/object';
const {
complianceDataException,
@ -102,14 +106,74 @@ export default class DatasetCompliance extends Component {
sortDirection: string;
searchTerm: string;
helpText = helpText;
watchers: Array<{ stateChange: (fn: () => void) => void; watchItem: Element; destroy?: Function }>;
complianceWatchers: WeakMap<Element, {}>;
_hasBadData: boolean;
platform: IDatasetView['platform'];
isCompliancePolicyAvailable: boolean = false;
showAllDatasetMemberData: boolean;
complianceInfo: undefined | IComplianceInfo;
/**
* Lists the compliance entities that are entered via the advanced edititing interface
* @type {Pick<IComplianceInfo, 'complianceEntities'>}
* @memberof DatasetCompliance
*/
manuallyEnteredComplianceEntities: Pick<IComplianceInfo, 'complianceEntities'>;
/**
* Flag enabling or disabling the manual apply button
* @type {boolean}
* @memberof DatasetCompliance
*/
isManualApplyDisabled: boolean = false;
/**
* Flag indicating the current compliance policy edit-view mode
* @type {boolean}
*/
showGuidedComplianceEditMode: boolean = true;
/**
* Formatted JSON string representing the compliance entities for this dataset
* @type {ComputedProperty<string>}
*/
jsonComplianceEntities: ComputedProperty<string> = computed('complianceInfo.complianceEntities.[]', function(
this: DatasetCompliance
): string {
//@ts-ignore property access path using dot notation limitation
const entities: Array<IComplianceEntity> = get(this, 'complianceInfo.complianceEntities');
const entitiesWithModifiableKeys = arrayMap(fleece<IComplianceEntity, 'readonly' | 'pii'>(['readonly', 'pii']))(
entities
);
return JSON.stringify(entitiesWithModifiableKeys, null, '\t');
});
/**
* Convenience computed property flag indicates if current edit step is the first step in the wizard flow
* @type {ComputedProperty<boolean>}
* @memberof DatasetCompliance
*/
isInitialEditStep = computed('editStep', 'editSteps.0.name', function(this: DatasetCompliance): boolean {
const { editStep, editSteps } = getProperties(this, ['editStep', 'editSteps']);
const [initialStep] = editSteps;
return editStep.name === initialStep.name;
});
/**
* Flag indicating if the Guided vs Advanced mode should be shown for the initial edit step
* @type {ComputedProperty<boolean>}
* @memberof DatasetCompliance
*/
showAdvancedEditApplyStep = computed('isInitialEditStep', 'showGuidedComplianceEditMode', function(
this: DatasetCompliance
): boolean {
const { isInitialEditStep, showGuidedComplianceEditMode } = getProperties(this, [
'isInitialEditStep',
'showGuidedComplianceEditMode'
]);
return isInitialEditStep && !showGuidedComplianceEditMode;
});
/**
* Flag indicating the readonly confirmation dialog should not be shown again for this compliance form
* @type {boolean}
@ -132,7 +196,10 @@ export default class DatasetCompliance extends Component {
onReset: <T>() => Promise<T>;
onSave: <T>() => Promise<T>;
onComplianceUpload: (jsonString: string) => void;
/**
* External action to handle manual compliance entity metadata entry
*/
onComplianceJsonUpdate: (jsonString: string) => Promise<void>;
notifyOnChangeSetSuggestions: (hasSuggestions: boolean) => void;
notifyOnChangeSetRequiresReview: (hasChangeSetDrift: boolean) => void;
@ -386,7 +453,7 @@ export default class DatasetCompliance extends Component {
* Holds a reference to the current step in the compliance edit wizard flow
* @type {{ name: string }}
*/
editStep: { name: string };
editStep: { name: string } = { name: '' };
/**
* A list of ui values and labels for review filter drop-down
@ -902,6 +969,56 @@ export default class DatasetCompliance extends Component {
}
actions: IDatasetComplianceActions = {
/**
* Toggle the visibility of the guided compliance edit view vs the advanced edit view modes
* @param {boolean} toggle flag ,if true, show guided edit mode, otherwise, advanced
*/
onShowGuidedEditMode(this: DatasetCompliance, toggle: boolean): void {
const isShowingGuidedEditMode = set(this, 'showGuidedComplianceEditMode', toggle);
if (!isShowingGuidedEditMode) {
this.actions.onManualComplianceUpdate.call(this, get(this, 'jsonComplianceEntities'));
}
},
/**
* Handles updating the list of compliance entities when a user manually enters values
* for the compliance entity metadata
* @param {string} updatedEntities json string of entities
*/
onManualComplianceUpdate(this: DatasetCompliance, updatedEntities: string): void {
try {
// check if the string is parseable as a JSON object
const entities = JSON.parse(updatedEntities);
const metadataObject = {
complianceEntities: entities
};
const isValid = validateMetadataObject(metadataObject, complianceEntitiesTaxonomy);
set(this, 'isManualApplyDisabled', !isValid);
if (isValid) {
set(this, 'manuallyEnteredComplianceEntities', metadataObject);
}
} catch {
set(this, 'isManualApplyDisabled', true);
}
},
/**
* Handler to apply manually entered compliance entities to the actual list of
* compliance metadata entities to be saved
*/
async onApplyComplianceJson(this: DatasetCompliance) {
try {
await get(this, 'onComplianceJsonUpdate')(JSON.stringify(get(this, 'manuallyEnteredComplianceEntities')));
// Proceed to next step if application of entites is successful
this.actions.nextStep.call(this);
} catch {
noop();
}
},
/**
* Action handles wizard step cancellation
*/
@ -1254,7 +1371,7 @@ export default class DatasetCompliance extends Component {
* @param {string} jsonString string representation for the JSON file
*/
onComplianceJsonUpload(this: DatasetCompliance, jsonString: string): void {
get(this, 'onComplianceUpload')(jsonString);
get(this, 'onComplianceJsonUpdate')(jsonString);
},
/**

View File

@ -29,7 +29,7 @@ import {
} from 'wherehows-web/constants';
import { iterateArrayAsync } from 'wherehows-web/utils/array';
import validateMetadataObject, {
complianceMetadataTaxonomy
complianceEntitiesTaxonomy
} from 'wherehows-web/utils/datasets/compliance/metadata-schema';
import { notificationDialogActionFactory } from 'wherehows-web/utils/notifications/notifications';
@ -347,7 +347,7 @@ export default class DatasetComplianceContainer extends Component {
* @memberof DatasetComplianceContainer
*/
@action
onComplianceUpload(this: DatasetComplianceContainer, jsonString: string): void {
async onComplianceJsonUpdate(this: DatasetComplianceContainer, jsonString: string): Promise<void> {
const {
complianceInfo,
notifications: { notify }
@ -357,35 +357,38 @@ export default class DatasetComplianceContainer extends Component {
* Inner function to wrap call to notify method of notification service
* @return {void}
*/
const metadataInvalid = (): void =>
const updateError = (error: string): void => {
notify(NotificationEvent.error, {
content: invalidPolicyData
content: error
});
throw new Error(error);
};
if (complianceInfo) {
try {
const policy = JSON.parse(jsonString);
const entityMetadata: Pick<IComplianceInfo, 'complianceEntities'> = JSON.parse(jsonString);
if (validateMetadataObject(policy, complianceMetadataTaxonomy)) {
const { complianceEntities, datasetClassification } = policy;
const resolvedComplianceInfo = { ...complianceInfo, complianceEntities, datasetClassification };
const { dialogActions } = notificationDialogActionFactory();
if (validateMetadataObject(entityMetadata, complianceEntitiesTaxonomy)) {
const { complianceEntities } = entityMetadata;
const resolvedComplianceInfo = { ...complianceInfo, complianceEntities };
const { dialogActions, dismissedOrConfirmed } = notificationDialogActionFactory();
set(this, 'complianceInfo', resolvedComplianceInfo);
set(this, 'complianceInfo', resolvedComplianceInfo);
return notify(NotificationEvent.confirm, {
header: 'Successfully applied uploaded metadata',
content: successUploading,
dialogActions,
dismissButtonText: false,
confirmButtonText: 'Dismiss'
});
}
notify(NotificationEvent.confirm, {
header: 'Successfully applied compliance entity metadata',
content: successUploading,
dialogActions,
dismissButtonText: false,
confirmButtonText: 'Next'
});
metadataInvalid();
} catch (e) {
metadataInvalid();
return await dismissedOrConfirmed;
}
return updateError(invalidPolicyData);
}
updateError('No Compliance policy found');
}
}

View File

@ -4,6 +4,7 @@
enum ComplianceEvent {
Cancel = 'CancelEditComplianceMetadata',
Next = 'NextComplianceMetadataStep',
ManualApply = 'AdvancedEditComplianceMetadataStep',
Previous = 'PreviousComplianceMetadataStep',
Edit = 'BeginEditComplianceMetadata',
Download = 'DownloadComplianceMetadata',

View File

@ -4,6 +4,7 @@ import { delay } from 'wherehows-web/utils/promise-delay';
import { action } from '@ember-decorators/object';
import { fleece } from 'wherehows-web/utils/object';
import { notificationDialogActionFactory } from 'wherehows-web/utils/notifications/notifications';
import noop from 'wherehows-web/utils/noop';
/**
* Flag indicating the current notification queue is being processed
@ -152,7 +153,7 @@ const notificationHandlers: INotificationHandler = {
};
// Set default values for button text if none are provided by consumer
props = { dismissButtonText: 'No', confirmButtonText: 'Yes', ...props };
const { dismissButtonText, confirmButtonText } = props;
const { dismissButtonText, confirmButtonText, onDialogToggle } = props;
// Removes dismiss or confirm buttons if set to false
let resolvedProps: IConfirmOptions =
dismissButtonText === false
@ -162,6 +163,7 @@ const notificationHandlers: INotificationHandler = {
confirmButtonText === false
? <IConfirmOptions>fleece<IConfirmOptions, 'confirmButtonText'>(['confirmButtonText'])(props)
: props;
resolvedProps = typeof onDialogToggle === 'function' ? props : { ...props, onDialogToggle: noop };
return {
props: resolvedProps,

View File

@ -303,6 +303,12 @@
}
}
.dataset-compliance-editor {
pre {
border-radius: item-spacing(1) / 2;
}
}
.compliance-depends {
display: none;
opacity: 0;

View File

@ -22,16 +22,35 @@
<div class="container action-bar__content">
{{#if (has-next editStep editSteps)}}
{{#track-ui-event category=trackableCategory.Compliance action=trackableEvent.Compliance.Next
name=editStep.name as |metrics|}}
{{#if showAdvancedEditApplyStep}}
{{#track-ui-event category=trackableCategory.Compliance action=trackableEvent.Compliance.ManualApply
name=editStep.name as |metrics|}}
<button
class="nacho-button nacho-button--large-inverse action-bar__item"
title="Next"
onclick={{action metrics.trackOnAction (action "nextStep")}}
disabled={{and (gt changeSetReviewCount 0) (eq editStep.name editSteps.0.name)}}>
Next
title="Apply JSON"
onclick={{action metrics.trackOnAction (action "onApplyComplianceJson")}}
disabled={{isManualApplyDisabled}}>
Apply
</button>
{{/track-ui-event}}
{{/track-ui-event}}
{{else}}
{{#track-ui-event category=trackableCategory.Compliance action=trackableEvent.Compliance.Next
name=editStep.name as |metrics|}}
<button
class="nacho-button nacho-button--large-inverse action-bar__item"
title="Next"
onclick={{action metrics.trackOnAction (action "nextStep")}}
disabled={{and (gt changeSetReviewCount 0) isInitialEditStep}}>
Next
</button>
{{/track-ui-event}}
{{/if}}
{{else}}
{{#track-ui-event category=trackableCategory.Compliance
@ -125,7 +144,7 @@
{{/if}}
{{#unless schemaless}}
{{#if (and (eq editStep.name editSteps.0.name) (not _hasBadData))}}
{{#if (and isInitialEditStep (not _hasBadData))}}
{{#track-ui-event category=trackableCategory.Compliance
action=trackableEvent.Compliance.Upload as |metrics|}}
@ -192,7 +211,7 @@
{{#if schemaless}}
{{#if (or isReadOnly (eq editStep.name editSteps.0.name))}}
{{#if (or isReadOnly isInitialEditStep)}}
{{datasets/schemaless-tagging
isEditable=(not isReadOnly)
classification=(readonly complianceInfo.confidentiality)
@ -204,7 +223,7 @@
{{else}}
{{#if (or isReadOnly (eq editStep.name editSteps.0.name))}}
{{#if (or isReadOnly isInitialEditStep)}}
{{partial "datasets/dataset-compliance/dataset-compliance-entities"}}
{{/if}}

View File

@ -33,7 +33,7 @@
notifyOnChangeSetRequiresReview=(action "onCompliancePolicyChangeSetDrift")
onSave=(action "savePrivacyCompliancePolicy")
onReset=(action "resetPrivacyCompliancePolicy")
onComplianceUpload=(action "onComplianceUpload")
onComplianceJsonUpdate=(action "onComplianceJsonUpdate")
}}
{{/if}}

View File

@ -15,36 +15,63 @@
</section>
<section class="compliance-entities-meta">
{{ember-selector
values=fieldReviewOptions
selected=(readonly fieldReviewOption)
selectionDidChange=(action "onFieldReviewChange")
}}
<button
class="nacho-button nacho-button{{if showGuidedComplianceEditMode '--inverse' '--secondary'}}"
onclick={{action "onShowGuidedEditMode" true}}>
{{tooltip-on-element
text="Show Guided View"
}}
{{#if changeSetReviewCount}}
<span class="dataset-compliance-fields__has-suggestions">
{{fa-icon "table" aria-label="Show Guided View"}}
</button>
<button
class="nacho-button nacho-button{{if showGuidedComplianceEditMode '--secondary' '--inverse'}}"
onclick={{action "onShowGuidedEditMode" false}}>
{{tooltip-on-element
text="Show Advanced View"
}}
{{fa-icon "code" aria-label="Show Advanced View"}}
</button>
</section>
<section class="compliance-entities-meta">
{{#if showGuidedComplianceEditMode}}
{{ember-selector
values=fieldReviewOptions
selected=(readonly fieldReviewOption)
selectionDidChange=(action "onFieldReviewChange")
}}
{{#if changeSetReviewCount}}
<span class="dataset-compliance-fields__has-suggestions">
{{changeSetReviewCount}} fields to be reviewed
</span>
{{/if}}
{{/if}}
{{#if (and isEditing unspecifiedTags)}}
{{#if isEditing}}
<div class="compliance-entities-meta__secondary">
<p class="set-fields-to-none-text">Set all unspecified field types to {{ComplianceFieldIdValue.None}}</p>
{{#if (and unspecifiedTags showGuidedComplianceEditMode)}}
<p class="set-fields-to-none-text">Set all unspecified field types to {{ComplianceFieldIdValue.None}}</p>
{{#track-ui-event category=trackableCategory.Compliance
action=trackableEvent.Compliance.SetUnspecifiedAsNone as |metrics|}}
<button
class="nacho-button nacho-button--large nacho-button--secondary action-bar__item"
onclick={{action metrics.trackOnAction (perform setUnspecifiedTagsAsNoneTask)}}>
{{#track-ui-event category=trackableCategory.Compliance
action=trackableEvent.Compliance.SetUnspecifiedAsNone as |metrics|}}
<button
class="nacho-button nacho-button--large nacho-button--secondary action-bar__item"
onclick={{action metrics.trackOnAction (perform setUnspecifiedTagsAsNoneTask)}}>
{{#if setUnspecifiedTagsAsNoneTask.isRunning}}
{{pendulum-ellipsis-animation}}
{{else}}
Set {{pluralize unspecifiedTags.length "tag"}} to {{ComplianceFieldIdValue.None}}
{{/if}}
{{#if setUnspecifiedTagsAsNoneTask.isRunning}}
{{pendulum-ellipsis-animation}}
{{else}}
Set {{pluralize unspecifiedTags.length "tag"}} to {{ComplianceFieldIdValue.None}}
{{/if}}
</button>
{{/track-ui-event}}
</button>
{{/track-ui-event}}
{{/if}}
</div>
{{/if}}
@ -62,381 +89,400 @@
{{/if}}
</section>
{{#if foldedChangeSet.length}}
{{#dataset-table
class="dataset-compliance-fields"
fields=foldedChangeSet
filterBy=filterBy
tableRowComponent='dataset-compliance-rollup-row'
searchTerm=searchTerm as |table|
}}
{{#if showGuidedComplianceEditMode}}
{{#if foldedChangeSet.length}}
{{#dataset-table
class="dataset-compliance-fields"
fields=foldedChangeSet
filterBy=filterBy
tableRowComponent='dataset-compliance-rollup-row'
searchTerm=searchTerm as |table|
}}
{{#table.head as |head|}}
{{#head.column class="dataset-compliance-fields__notification-column"}}{{/head.column}}
{{#head.column class="dataset-compliance-fields__identifier-column"}}
Field
{{#table.head as |head|}}
{{#head.column class="dataset-compliance-fields__notification-column"}}{{/head.column}}
{{#head.column class="dataset-compliance-fields__identifier-column"}}
Field
{{more-info
link="http://go/tms-schema"
tooltip="Click for more information on Schema"
}}
{{/head.column}}
{{#head.column class="nacho-table-cell-wrapped"}}Compliance Information{{/head.column}}
{{#head.column class="nacho-table-cell-wrapped"}}
System Suggestion & Confidence
{{/head.column}}
{{/table.head}}
<tr>
<th>{{!--spacer--}}</th>
<th colspan="3">
<div class="dataset-compliance-fields__actions">
{{disable-bubble-input
title="Search field names"
placeholder="Search field names"
value=table.searchTerm
class="dataset-compliance-fields__search"
on-input=(action table.filterDidChange value="target.value")
{{more-info
link="http://go/tms-schema"
tooltip="Click for more information on Schema"
}}
</div>
</th>
</tr>
{{/head.column}}
{{#head.column class="nacho-table-cell-wrapped"}}Compliance Information{{/head.column}}
{{#head.column class="nacho-table-cell-wrapped"}}
System Suggestion & Confidence
{{/head.column}}
{{/table.head}}
{{#table.body as |body|}}
{{#each table.data as |field|}}
{{#body.row
field=field
isNewComplianceInfo=isNewComplianceInfo
complianceDataTypes=complianceDataTypes
onFieldDblClick=(action "onFieldDblClick")
onFieldTagAdded=(action "onFieldTagAdded")
onFieldTagRemoved=(action "onFieldTagRemoved")
onTagReadOnlyDisable=(action "onTagReadOnlyDisable")
onTagIdentifierTypeChange=(action "tagIdentifierChanged")
onSuggestionIntent=(action "onFieldSuggestionIntentChange") as |row|
}}
<tr class="{{if row.isReadonly 'dataset-compliance-fields--readonly'}}" ondblclick={{action
row.onFragmentDblClick}}>
{{#row.cell}}
{{#if
(and row.suggestion (and (not row.suggestionMatchesCurrentValue) (not row.suggestionResolution)))}}
<tr>
<th>{{!--spacer--}}</th>
<th colspan="3">
<div class="dataset-compliance-fields__actions">
{{disable-bubble-input
title="Search field names"
placeholder="Search field names"
value=table.searchTerm
class="dataset-compliance-fields__search"
on-input=(action table.filterDidChange value="target.value")
}}
</div>
</th>
</tr>
<span class="nacho-tooltip" title="Has suggestions">
{{#table.body as |body|}}
{{#each table.data as |field|}}
{{#body.row
field=field
isNewComplianceInfo=isNewComplianceInfo
complianceDataTypes=complianceDataTypes
onFieldDblClick=(action "onFieldDblClick")
onFieldTagAdded=(action "onFieldTagAdded")
onFieldTagRemoved=(action "onFieldTagRemoved")
onTagReadOnlyDisable=(action "onTagReadOnlyDisable")
onTagIdentifierTypeChange=(action "tagIdentifierChanged")
onSuggestionIntent=(action "onFieldSuggestionIntentChange") as |row|
}}
<tr class="{{if row.isReadonly 'dataset-compliance-fields--readonly'}}" ondblclick={{action
row.onFragmentDblClick}}>
{{#row.cell}}
{{#if
(and row.suggestion (and (not row.suggestionMatchesCurrentValue) (not row.suggestionResolution)))}}
<span class="nacho-tooltip" title="Has suggestions">
<i class="fa fa-exclamation dataset-compliance-fields__has-suggestions__icon"
title="Compliance field has suggested values"></i>
</span>
{{else}}
{{else}}
{{#if row.isReviewRequested}}
{{#if row.isReviewRequested}}
<span class="nacho-tooltip" title="Please review">
<span class="nacho-tooltip" title="Please review">
<i class="fa fa-question dataset-compliance-fields--review-required__icon"
title="Compliance policy information needs review"></i>
</span>
{{else}}
{{else}}
<span class="nacho-tooltip" title="All good!">
<span class="nacho-tooltip" title="All good!">
<i class="fa fa-check dataset-compliance-fields--ok__icon" title="All good!"></i>
</span>
{{/if}}
{{/if}}
{{/row.cell}}
{{/if}}
{{/row.cell}}
{{#row.cell}}
<div class="dataset-compliance-fields__id-field-wrap">
<div title="{{row.identifierField}}">
<strong>
{{if isShowingFullFieldNames row.identifierField (split-text row.identifierField 23)}}
</strong>
</div>
{{#row.cell}}
<div class="dataset-compliance-fields__id-field-wrap">
<div title="{{row.identifierField}}">
<strong>
{{if isShowingFullFieldNames row.identifierField (split-text row.identifierField 23)}}
</strong>
<div title="{{row.dataType}}">
{{if isShowingFullFieldNames row.dataType (split-text row.dataType 23)}}
</div>
</div>
{{/row.cell}}
<div title="{{row.dataType}}">
{{if isShowingFullFieldNames row.dataType (split-text row.dataType 23)}}
</div>
</div>
{{/row.cell}}
{{#row.cell}}
{{#if (and isEditing (not row.isReadonly))}}
{{#each row.fieldChangeSet as |tag|}}
{{#row.cell}}
{{#if (and isEditing (not row.isReadonly))}}
{{#each row.fieldChangeSet as |tag|}}
{{#basic-dropdown as |tagDrop|}}
{{#tagDrop.trigger
class="dataset-compliance-fields__tag-info dataset-compliance-fields__tag-info--editable"}}
{{#basic-dropdown as |tagDrop|}}
{{#tagDrop.trigger
class="dataset-compliance-fields__tag-info dataset-compliance-fields__tag-info--editable"}}
<p class="dataset-compliance-fields__tag-info__text">
{{#if tag.identifierType}}
{{tag.identifierType}}{{if tag.logicalType (concat ", " tag.logicalType)}}
{{else}}
<span
class="dataset-compliance-fields__tag-info__text dataset-compliance-fields__tag-info__text--obscure">
<p class="dataset-compliance-fields__tag-info__text">
{{#if tag.identifierType}}
{{tag.identifierType}}{{if tag.logicalType (concat ", " tag.logicalType)}}
{{else}}
<span
class="dataset-compliance-fields__tag-info__text dataset-compliance-fields__tag-info__text--obscure">
Select Field Type ...
</span>
{{/if}}
</p>
{{/if}}
</p>
{{/tagDrop.trigger}}
{{/tagDrop.trigger}}
{{#tagDrop.content overlay=true class="dataset-compliance-fields__guided-modal"}}
{{#dataset-compliance-field-tag
tag=tag
parentHasSingleTag=row.hasSingleTag
onTagIdentifierTypeChange=(action "tagIdentifierChanged")
onTagLogicalTypeChange=(action "tagLogicalTypeChanged")
onTagValuePatternChange=(action "tagValuePatternChanged")
onTagOwnerChange=(action "tagOwnerChanged")
complianceFieldIdDropdownOptions=complianceFieldIdDropdownOptions
complianceDataTypes=complianceDataTypes as |tagRowComponent|
}}
<section class="dataset-compliance-fields__compliance-info-column">
<header class="dataset-compliance-fields__compliance-info-column__title">
<strong>Select field type</strong>
</header>
<div class="dataset-compliance-fields__compliance-info-column__content">
{{#track-ui-event category=trackableCategory.Compliance
action=trackableEvent.Compliance.FieldIndentifier
name=tag.identifierType as |metrics|}}
{{#each tagRowComponent.tagIdOptions as |tagOption|}}
{{#radio-button-composer
value=tagOption.value
name="datasetFieldClassification"
groupValue=(readonly tag.identifierType)
class="dataset-compliance-fields__tag-radio"
disabled=tagOption.isDisabled
disabledClass="dataset-compliance-fields__tag-radio--disabled"
checkedClass="dataset-compliance-fields__tag-radio--checked"
onMouseEnter=(action tagRowComponent.onFieldTagIdentifierEnter)
onMouseLeave=(action tagRowComponent.onFieldTagIdentifierLeave)
changed=(action metrics.trackOnAction (action tagRowComponent.tagIdentifierTypeDidChange))}}
{{tagOption.label}}
{{/radio-button-composer}}
{{/each}}
{{/track-ui-event}}
</div>
</section>
{{#if tagRowComponent.quickDesc}}
{{#tagDrop.content overlay=true class="dataset-compliance-fields__guided-modal"}}
{{#dataset-compliance-field-tag
tag=tag
parentHasSingleTag=row.hasSingleTag
onTagIdentifierTypeChange=(action "tagIdentifierChanged")
onTagLogicalTypeChange=(action "tagLogicalTypeChanged")
onTagValuePatternChange=(action "tagValuePatternChanged")
onTagOwnerChange=(action "tagOwnerChanged")
complianceFieldIdDropdownOptions=complianceFieldIdDropdownOptions
complianceDataTypes=complianceDataTypes as |tagRowComponent|
}}
<section class="dataset-compliance-fields__compliance-info-column">
<div class="dataset-compliance-fields__field-tag__quick-desc">
<strong>
{{tagRowComponent.quickDesc.title}}:
</strong>
<header class="dataset-compliance-fields__compliance-info-column__title">
<strong>Select field type</strong>
</header>
<p>
{{tagRowComponent.quickDesc.description}}
</p>
<div class="dataset-compliance-fields__compliance-info-column__content">
{{#track-ui-event category=trackableCategory.Compliance
action=trackableEvent.Compliance.FieldIndentifier
name=tag.identifierType as |metrics|}}
{{#each tagRowComponent.tagIdOptions as |tagOption|}}
{{#radio-button-composer
value=tagOption.value
name="datasetFieldClassification"
groupValue=(readonly tag.identifierType)
class="dataset-compliance-fields__tag-radio"
disabled=tagOption.isDisabled
disabledClass="dataset-compliance-fields__tag-radio--disabled"
checkedClass="dataset-compliance-fields__tag-radio--checked"
onMouseEnter=(action tagRowComponent.onFieldTagIdentifierEnter)
onMouseLeave=(action tagRowComponent.onFieldTagIdentifierLeave)
changed=(action metrics.trackOnAction (action tagRowComponent.tagIdentifierTypeDidChange))}}
{{tagOption.label}}
{{/radio-button-composer}}
{{/each}}
{{/track-ui-event}}
</div>
</section>
{{else}}
{{#if tagRowComponent.quickDesc}}
{{#if tagRowComponent.isIdType}}
<section class="dataset-compliance-fields__compliance-info-column">
<header class="dataset-compliance-fields__compliance-info-column__title">
<strong>Select field format</strong>
</header>
<div class="dataset-compliance-fields__field-tag__quick-desc">
<strong>
{{tagRowComponent.quickDesc.title}}:
</strong>
<div class="dataset-compliance-fields__compliance-info-column__content">
{{#track-ui-event category=trackableCategory.Compliance
action=trackableEvent.Compliance.FieldFormat
name=tag.logicalType as |metrics|}}
{{#each tagRowComponent.fieldFormats as |fieldFormat|}}
{{#radio-button-composer
value=fieldFormat.value
name="datasetFieldFieldFormat"
groupValue=(readonly tag.logicalType)
class="dataset-compliance-fields__tag-radio"
checkedClass="dataset-compliance-fields__tag-radio--checked"
changed=(action metrics.trackOnAction (action tagRowComponent.tagLogicalTypeDidChange))}}
{{fieldFormat.label}}
{{/radio-button-composer}}
{{/each}}
{{/track-ui-event}}
<p>
{{tagRowComponent.quickDesc.description}}
</p>
</div>
</section>
{{#if tagRowComponent.showCustomInput}}
{{else}}
{{#if tagRowComponent.isIdType}}
<section class="dataset-compliance-fields__compliance-info-column">
<header class="dataset-compliance-fields__compliance-info-column__title">
<strong>Custom RegEx</strong>
</header>
<div
class="dataset-compliance-fields__compliance-info-column__content">
<div class="dataset-compliance-fields__text-pattern-wrap">
<div class="dataset-compliance-fields__text-pattern-wrap--input">
<input
placeholder="Enter regex"
value="{{readonly tag.valuePattern}}"
disabled={{or (not isEditing) row.isReadonly}}
class="dataset-compliance-fields__text-pattern {{if
tagRowComponent.valuePatternError
'dataset-compliance-fields--missing-selection'}}"
oninput={{action tagRowComponent.tagValuePatternDidChange value="target.value"}}
>
</div>
{{more-info
link="http://go/metadata-custom-regex"
tooltip="Click for more information on RegExp format"
}}
{{#if tagRowComponent.valuePatternError}}
<div class="dataset-compliance-fields__text-pattern-wrap--error">
{{tagRowComponent.valuePatternError}}
</div>
{{/if}}
</div>
</div>
</section>
{{/if}}
{{#unless tagRowComponent.isTagFormatMissing}}
<section class="dataset-compliance-fields__compliance-info-column">
<header class="dataset-compliance-fields__compliance-info-column__title">
<strong>Has Ownership?</strong>
<strong>Select field format</strong>
</header>
<div class="dataset-compliance-fields__compliance-info-column__content">
{{#track-ui-event category=trackableCategory.Compliance
action=trackableEvent.Compliance.FieldFormat
name=tag.logicalType as |metrics|}}
{{#radio-button-composer
value=false
name="datasetFieldOwnerToggle"
groupValue=(readonly tag.nonOwner)
class="dataset-compliance-fields__tag-radio"
checkedClass="dataset-compliance-fields__tag-radio--checked"
changed=(action tagRowComponent.tagOwnerDidChange)}}
Yes
{{/radio-button-composer}}
{{#each tagRowComponent.fieldFormats as |fieldFormat|}}
{{#radio-button-composer
value=fieldFormat.value
name="datasetFieldFieldFormat"
groupValue=(readonly tag.logicalType)
class="dataset-compliance-fields__tag-radio"
checkedClass="dataset-compliance-fields__tag-radio--checked"
changed=(action metrics.trackOnAction (action tagRowComponent.tagLogicalTypeDidChange))}}
{{fieldFormat.label}}
{{/radio-button-composer}}
{{/each}}
{{#radio-button-composer
value=true
name="datasetFieldOwnerToggle"
groupValue=(readonly tag.nonOwner)
class="dataset-compliance-fields__tag-radio"
checkedClass="dataset-compliance-fields__tag-radio--checked"
changed=(action tagRowComponent.tagOwnerDidChange)}}
No
{{/radio-button-composer}}
{{/track-ui-event}}
</div>
</section>
{{/unless}}
{{#if tagRowComponent.showCustomInput}}
<section class="dataset-compliance-fields__compliance-info-column">
<header class="dataset-compliance-fields__compliance-info-column__title">
<strong>Custom RegEx</strong>
</header>
<div
class="dataset-compliance-fields__compliance-info-column__content">
<div class="dataset-compliance-fields__text-pattern-wrap">
<div class="dataset-compliance-fields__text-pattern-wrap--input">
<input
placeholder="Enter regex"
value="{{readonly tag.valuePattern}}"
disabled={{or (not isEditing) row.isReadonly}}
class="dataset-compliance-fields__text-pattern {{if
tagRowComponent.valuePatternError
'dataset-compliance-fields--missing-selection'}}"
oninput={{action tagRowComponent.tagValuePatternDidChange value="target.value"}}
>
</div>
{{more-info
link="http://go/metadata-custom-regex"
tooltip="Click for more information on RegExp format"
}}
{{#if tagRowComponent.valuePatternError}}
<div class="dataset-compliance-fields__text-pattern-wrap--error">
{{tagRowComponent.valuePatternError}}
</div>
{{/if}}
</div>
</div>
</section>
{{/if}}
{{#unless tagRowComponent.isTagFormatMissing}}
<section class="dataset-compliance-fields__compliance-info-column">
<header class="dataset-compliance-fields__compliance-info-column__title">
<strong>Has Ownership?</strong>
</header>
<div class="dataset-compliance-fields__compliance-info-column__content">
{{#radio-button-composer
value=false
name="datasetFieldOwnerToggle"
groupValue=(readonly tag.nonOwner)
class="dataset-compliance-fields__tag-radio"
checkedClass="dataset-compliance-fields__tag-radio--checked"
changed=(action tagRowComponent.tagOwnerDidChange)}}
Yes
{{/radio-button-composer}}
{{#radio-button-composer
value=true
name="datasetFieldOwnerToggle"
groupValue=(readonly tag.nonOwner)
class="dataset-compliance-fields__tag-radio"
checkedClass="dataset-compliance-fields__tag-radio--checked"
changed=(action tagRowComponent.tagOwnerDidChange)}}
No
{{/radio-button-composer}}
</div>
</section>
{{/unless}}
{{/if}}
{{/if}}
{{/if}}
{{/dataset-compliance-field-tag}}
{{/tagDrop.content}}
{{/basic-dropdown}}
{{/dataset-compliance-field-tag}}
{{/tagDrop.content}}
{{/basic-dropdown}}
{{#if (and isEditing (not row.hasSingleTag))}}
<span class="nacho-tooltip" title="Delete Tag">
{{#if (and isEditing (not row.hasSingleTag))}}
<span class="nacho-tooltip" title="Delete Tag">
<button class="nacho-button nacho-button--tertiary dataset-compliance-fields__remove-tag"
onclick={{action row.onRemoveFieldTag tag}}>
{{fa-icon "trash"}}
</button>
</span>
{{/if}}
{{/each}}
{{/if}}
{{/each}}
{{#unless row.hasNoneTag}}
<br>
<button
class="nacho-button nacho-button--tertiary dataset-compliance-fields__add-field"
onclick={{action row.onAddFieldTag}}>
+ Add new
</button>
{{/unless}}
{{#unless row.hasNoneTag}}
<br>
<button
class="nacho-button nacho-button--tertiary dataset-compliance-fields__add-field"
onclick={{action row.onAddFieldTag}}>
+ Add new
</button>
{{/unless}}
{{else}}
{{#each row.fieldChangeSet as |tag|}}
{{else}}
{{#each row.fieldChangeSet as |tag|}}
<div class="dataset-compliance-fields__tag-info">
{{tag.identifierType}}{{if tag.logicalType (concat ", " tag.logicalType)}}
<div class="dataset-compliance-fields__tag-info">
{{tag.identifierType}}{{if tag.logicalType (concat ", " tag.logicalType)}}
{{#if row.isReadonly}}
<span class="nacho-tooltip" title="Readonly">
{{#if row.isReadonly}}
<span class="nacho-tooltip" title="Readonly">
<button class="nacho-button nacho-button--tertiary dataset-compliance-fields--readonly__icon"
disabled={{not isEditing}}
onclick={{action row.onEditReadonlyTag tag}}>
onclick={{action row.onEditReadonlyTag tag}}>
{{fa-icon "lock"}}
</button>
</span>
{{/if}}
</div>
{{/if}}
</div>
{{/each}}
{{/if}}
{{/row.cell}}
{{#row.cell}}
<div class="dataset-compliance-fields__suggested-values">
{{#if row.suggestion}}
<span class="dataset-compliance-fields__suggested-value {{unless row.suggestionResolution
'dataset-compliance-fields__suggested-value--no-res'}}">
{{row.suggestion.identifierType}}
</span>
<span class="dataset-compliance-fields__suggested-value {{unless row.suggestionResolution
'dataset-compliance-fields__suggested-value--no-res'}}">
{{row.suggestion.logicalType}}
</span>
{{row.suggestion.confidence}}%
&nbsp;
{{#if isEditing}}
{{#if row.suggestionResolution}}
<div class="dataset-compliance-fields__resolution {{if (eq row.suggestionResolution 'Accepted')
'dataset-compliance-fields__resolution--ok'}}">
{{row.suggestionResolution}}
</div>
{{else}}
{{auto-suggest-action
type="accept"
field=row.fieldProps
feedbackAction=(action notifyOnComplianceSuggestionFeedback)
onSuggestionClick=(action row.onSuggestionClick)
}}
{{auto-suggest-action
field=row.fieldProps
feedbackAction=(action notifyOnComplianceSuggestionFeedback)
onSuggestionClick=(action row.onSuggestionClick)
}}
{{/if}}
{{/if}}
{{else}}
&mdash;
{{/each}}
{{/if}}
</div>
{{/row.cell}}
</tr>
{{/row.cell}}
{{/body.row}}
{{/each}}
{{/table.body}}
{{#row.cell}}
<div class="dataset-compliance-fields__suggested-values">
{{#if row.suggestion}}
<span class="dataset-compliance-fields__suggested-value {{unless row.suggestionResolution
'dataset-compliance-fields__suggested-value--no-res'}}">
{{row.suggestion.identifierType}}
</span>
{{/dataset-table}}
<span class="dataset-compliance-fields__suggested-value {{unless row.suggestionResolution
'dataset-compliance-fields__suggested-value--no-res'}}">
{{row.suggestion.logicalType}}
</span>
{{row.suggestion.confidence}}%
&nbsp;
{{#if isEditing}}
{{#if row.suggestionResolution}}
<div class="dataset-compliance-fields__resolution {{if (eq row.suggestionResolution 'Accepted')
'dataset-compliance-fields__resolution--ok'}}">
{{row.suggestionResolution}}
</div>
{{else}}
{{auto-suggest-action
type="accept"
field=row.fieldProps
feedbackAction=(action notifyOnComplianceSuggestionFeedback)
onSuggestionClick=(action row.onSuggestionClick)
}}
{{auto-suggest-action
field=row.fieldProps
feedbackAction=(action notifyOnComplianceSuggestionFeedback)
onSuggestionClick=(action row.onSuggestionClick)
}}
{{/if}}
{{/if}}
{{else}}
&mdash;
{{/if}}
</div>
{{/row.cell}}
</tr>
{{/body.row}}
{{/each}}
{{/table.body}}
{{/dataset-table}}
{{else}}
{{empty-state
heading="No fields found"
subHead="If you have a filter applied, setting this to the least restrictive option may yield more results."
}}
{{/if}}
{{else}}
{{empty-state
heading="No fields found"
subHead="If you have a filter applied, setting this to the least restrictive option may yield more results."
}}
{{ember-ace
readOnly=isReadOnly
update=(action "onManualComplianceUpdate")
class="dataset-compliance-editor"
value=jsonComplianceEntities
enableAutocompletion=true
enableLiveAutocompletion=true
minLines=10
maxLines=50
showLineNumbers=true
useWrapMode=true
mode="ace/mode/json"
worker="ace/mode/json_worker"
theme="ace/theme/github"}}
{{/if}}

View File

@ -31,8 +31,10 @@ export interface IComplianceEntity {
// Flag indicating that this compliance field is not editable by the end user
// field should also be filtered from persisted policy
readonly readonly?: boolean;
//Optional attribute for the value of a CUSTOM regex. Required for CUSTOM field format
// Optional attribute for the value of a CUSTOM regex. Required for CUSTOM field format
valuePattern?: string | null;
// Flags this entity as containing pii data
pii?: boolean;
}
/**

View File

@ -31,6 +31,7 @@ type IComplianceEntityWithMetadata = Pick<
| 'nonOwner'
| 'readonly'
| 'valuePattern'
| 'pii'
> & {
// flag indicating that the field has a current policy upstream
privacyPolicyExists: boolean;

View File

@ -32,15 +32,10 @@ const datasetClassificationPropType = (prop: string): IMetadataType => ({
});
/**
* Defines the shape of the dataset compliance metadata json object using the IMetadataType interface
* @type {Array<IMetadataType>}
* Lists the types for objects or instances in the the compliance metadata entities list
* @type Array<IMetadataType>
*/
const complianceMetadataTaxonomy: Array<IMetadataType> = [
{
'@type': 'object',
'@name': 'datasetClassification',
'@props': arrayMap(datasetClassificationPropType)(Object.keys(DatasetClassifiers))
},
const complianceEntitiesTaxonomy: Array<IMetadataType> = [
{
'@type': 'array',
'@name': 'complianceEntities',
@ -71,7 +66,20 @@ const complianceMetadataTaxonomy: Array<IMetadataType> = [
'@type': ['string', 'null']
}
]
}
];
/**
* Defines the shape of the dataset compliance metadata json object using the IMetadataType interface
* @type {Array<IMetadataType>}
*/
const complianceMetadataTaxonomy: Array<IMetadataType> = [
{
'@type': 'object',
'@name': 'datasetClassification',
'@props': arrayMap(datasetClassificationPropType)(Object.keys(DatasetClassifiers))
},
...complianceEntitiesTaxonomy,
{
'@type': ['string', 'null'],
'@name': 'compliancePurgeNote'
@ -131,6 +139,20 @@ const keyValueHasMatch = (object: IObject<any>) => (metadataType: IMetadataType)
return rootValueEquiv;
};
/**
* Ensures that the keys on the supplied object are equivalent to the names in the type definition list
* @param {IObject<any>} object
* @param {Array<IMetadataType>} typeMaps
* @return {boolean}
*/
const keysMatchNames = (object: IObject<any>, typeMaps: Array<IMetadataType>): boolean =>
Object.keys(object)
.sort()
.toString() ===
arrayMap((typeMap: IMetadataType) => typeMap['@name'])(typeMaps)
.sort()
.toString();
/**
* Checks each key on an object matches the expected types in the typeMap
* @param {IObject<any>} object the object with keys to check
@ -138,17 +160,8 @@ const keyValueHasMatch = (object: IObject<any>) => (metadataType: IMetadataType)
* @returns {boolean}
*/
const keysEquiv = (object: IObject<any>, typeMaps: Array<IMetadataType>): boolean =>
arrayEvery(keyValueHasMatch(object))(typeMaps);
arrayEvery(keyValueHasMatch(object))(typeMaps) && keysMatchNames(object, typeMaps);
/**
* Checks that a compliance metadata object has a schema that matches the taxonomy / schema provided
* @param {IObject<any>} object an instance of a compliance metadata object
* @param {Array<IMetadataType>} taxonomy schema shape to check against
* @return {boolean}
*/
const validateMetadataObject = (object: IObject<any>, taxonomy: Array<IMetadataType>): boolean =>
keysEquiv(object, taxonomy);
export default keysEquiv;
export default validateMetadataObject;
export { complianceMetadataTaxonomy };
export { complianceMetadataTaxonomy, complianceEntitiesTaxonomy };

View File

@ -6,6 +6,12 @@ const MergeTrees = require('broccoli-merge-trees');
module.exports = function(defaults) {
const app = new EmberApp(defaults, {
ace: {
modes: ['json'],
workers: ['json'],
exts: ['searchbox']
},
babel: {
plugins: ['transform-object-rest-spread', 'transform-class-properties'],
sourceMaps: 'inline'

View File

@ -38,6 +38,7 @@
"broccoli-funnel": "^2.0.1",
"broccoli-merge-trees": "^3.0.0",
"codecov": "^3.0.0",
"ember-ace": "^1.3.1",
"ember-ajax": "^3.0.0",
"ember-basic-dropdown": "^1.0.0",
"ember-cli": "~2.18.0",

View File

@ -231,6 +231,10 @@ accepts@~1.3.4:
mime-types "~2.1.16"
negotiator "0.6.1"
ace-builds@^1.2.8:
version "1.3.3"
resolved "https://registry.yarnpkg.com/ace-builds/-/ace-builds-1.3.3.tgz#c9746028d1485e5d7595fb2e825e665bd6648970"
acorn-globals@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-4.1.0.tgz#ab716025dbe17c54d3ef81d32ece2b2d99fe2538"
@ -2814,6 +2818,17 @@ elegant-spinner@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/elegant-spinner/-/elegant-spinner-1.0.1.tgz#db043521c95d7e303fd8f345bedc3349cfb0729e"
ember-ace@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/ember-ace/-/ember-ace-1.3.1.tgz#b690cd1fe09a65a264dec40f9a54050af982a8da"
dependencies:
ace-builds "^1.2.8"
broccoli-merge-trees "^2.0.0"
broccoli-plugin "^1.2.1"
ember-cli-babel "^6.6.0"
ember-cli-htmlbars "^2.0.2"
ember-cli-node-assets "^0.2.2"
ember-ajax@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/ember-ajax/-/ember-ajax-3.0.0.tgz#8f21e9da0c1d433cf879aa855fce464d517e9ab5"
@ -3131,6 +3146,17 @@ ember-cli-node-assets@^0.1.4, ember-cli-node-assets@^0.1.6:
lodash "^4.5.1"
resolve "^1.1.7"
ember-cli-node-assets@^0.2.2:
version "0.2.2"
resolved "https://registry.yarnpkg.com/ember-cli-node-assets/-/ember-cli-node-assets-0.2.2.tgz#d2d55626e7cc6619f882d7fe55751f9266022708"
dependencies:
broccoli-funnel "^1.0.1"
broccoli-merge-trees "^1.1.1"
broccoli-source "^1.1.0"
debug "^2.2.0"
lodash "^4.5.1"
resolve "^1.1.7"
ember-cli-normalize-entity-name@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/ember-cli-normalize-entity-name/-/ember-cli-normalize-entity-name-1.0.0.tgz#0b14f7bcbc599aa117b5fddc81e4fd03c4bad5b7"