updates dataset-compliance compliance component. adds notification component styles. fixes complianceEntities key name

This commit is contained in:
Seyi Adebajo 2017-05-19 08:45:05 -07:00 committed by Mars Lan
parent c76487c167
commit 0da21378fe
7 changed files with 303 additions and 239 deletions

View File

@ -1,22 +1,78 @@
import Ember from 'ember'; import Ember from 'ember';
import isTrackingHeaderField from 'wherehows-web/utils/validators/tracking-headers'; import isTrackingHeaderField from 'wherehows-web/utils/validators/tracking-headers';
import {
classifiers,
datasetClassifiers,
fieldIdentifierTypes,
idLogicalTypes,
genericLogicalTypes,
defaultFieldDataTypeClassification
} from 'wherehows-web/constants';
const { Component, computed, set, get, isBlank, setProperties, getWithDefault, String: { htmlSafe } } = Ember; const {
Component,
computed,
set,
get,
Observable: { mixins: { notifyPropertyChange } },
setProperties,
getWithDefault,
String: { htmlSafe }
} = Ember;
// TODO: DSS-6671 Extract to constants module // TODO: DSS-6671 Extract to constants module
const missingTypes = 'Looks like some fields may contain privacy data but do not have a specified `Field Format`?'; const missingTypes = 'Looks like some fields may contain privacy data but do not have a specified `Field Format`?';
const successUpdating = 'Your changes have been successfully saved!'; const successUpdating = 'Your changes have been successfully saved!';
const failedUpdating = 'Oops! We are having trouble updating this dataset at the moment.'; const failedUpdating = 'Oops! We are having trouble updating this dataset at the moment.';
const hiddenTrackingFieldsMsg = htmlSafe( const hiddenTrackingFieldsMsg = htmlSafe(
'<p>Hey! Just a heads up that some fields in this dataset have been hidden from the table(s) below. ' + '<p>Some fields in this dataset have been hidden from the table(s) below. ' +
"These are tracking fields for which we've been able to predetermine the compliance classification.</p>" + "These are tracking fields for which we've been able to predetermine the compliance classification.</p>" +
'<p>For example: <code>header.memberId</code>, <code>requestHeader</code>. ' + '<p>For example: <code>header.memberId</code>, <code>requestHeader</code>. ' +
'Hopefully, this saves you some scrolling!</p>' 'Hopefully, this saves you some scrolling!</p>'
); );
const helpText = {
classification: 'For each of the fields below, please classify them based on the data handling table found at go/dht. ' +
'The default classification should suffice in most cases.',
subjectOwner: 'A field can contain one or multiple member IDs that do not technically "own" the entire record, ' +
'e.g. the list of recipients of a message. The field should be marked as a "Subject Owner" in this case.'
};
const complianceListKey = 'privacyCompliancePolicy.compliancePurgeEntities'; /**
// TODO: DSS-6671 Extract to constants module * Quick regex to check if a string ends with urn case-insensitive, cached outside scope to avoid recreating
const logicalTypes = ['ID', 'URN', 'REVERSED_URN', 'COMPOSITE_URN']; * on every function invocation
* @type {RegExp}
*/
const endsWithUrnRegex = /.*urn/i;
/**
* Takes a string, returns a formatted string. Niche , single use case
* for now, so no need to make into a helper
* @param {String} string
*/
const formatAsCapitalizedStringWithSpaces = string => string.replace(/[A-Z]/g, match => ` ${match}`).capitalize();
/**
* String constant referencing the datasetClassification on the privacy policy
* @type {String}
*/
const datasetClassificationKey = 'complianceInfo.datasetClassification';
/**
* A list of available keys for the datasetClassification map on the security specification
* @type {Array}
*/
const datasetClassifiersKeys = Object.keys(datasetClassifiers);
/**
* String constant identifying the classified fields on the security spec
* @type {String}
*/
const policyFieldClassificationKey = 'complianceInfo.fieldClassification';
/**
* A reference to the compliance policy entities on the complianceInfo map
* @type {string}
*/
const policyComplianceEntitiesKey = 'complianceInfo.complianceEntities';
/** /**
* Duplicate check using every to short-circuit iteration * Duplicate check using every to short-circuit iteration
* @param {Array} names = [] the list to check for dupes * @param {Array} names = [] the list to check for dupes
@ -25,17 +81,38 @@ const logicalTypes = ['ID', 'URN', 'REVERSED_URN', 'COMPOSITE_URN'];
const fieldNamesAreUnique = (names = []) => names.every((name, index) => names.indexOf(name) === index); const fieldNamesAreUnique = (names = []) => names.every((name, index) => names.indexOf(name) === index);
/** /**
* Returns a computed macro based on a provided type will return a list of * A list of field identifier types mapped to label, value options for select display
* Compliance fields that are of that identifierType or have no type * @type {any[]|Array.<{value: String, label: String}>}
* @param {String} type string to match against identifierType
*/ */
const complianceEntitiesMatchingType = type => const fieldIdentifierOptions = Object.keys(fieldIdentifierTypes).map(fieldIdentifierType => ({
computed('complianceDataFieldsSansHiddenTracking.[]', function() { value: fieldIdentifierTypes[fieldIdentifierType].value,
const fieldRegex = new RegExp(`${type}`, 'i'); label: fieldIdentifierTypes[fieldIdentifierType].displayAs
}));
return get(this, 'complianceDataFieldsSansHiddenTracking').filter(({ identifierType }) => { /**
return fieldRegex.test(identifierType) || isBlank(identifierType); * A list of field identifier types that are Ids i.e member ID, org ID, group ID
}); * @type {any[]|Array.<String>}
*/
const fieldIdentifierTypeIds = Object.keys(fieldIdentifierTypes)
.map(fieldIdentifierType => fieldIdentifierTypes[fieldIdentifierType])
.filter(({ isId }) => isId)
.mapBy('value');
/**
* Caches a list of logicalType mappings for displaying its value and a label by logicalType
* @param {String} logicalType
*/
const cachedLogicalTypes = logicalType =>
computed(function() {
return {
id: idLogicalTypes,
generic: genericLogicalTypes
}[logicalType].map(value => ({
value,
label: value
? value.replace(/_/g, ' ').replace(/([A-Z]{3,})/g, f => f.toLowerCase().capitalize())
: 'Select Format'
}));
}); });
export default Component.extend({ export default Component.extend({
@ -43,20 +120,10 @@ export default Component.extend({
filterBy: 'identifierField', filterBy: 'identifierField',
sortDirection: 'asc', sortDirection: 'asc',
searchTerm: '', searchTerm: '',
helpText,
fieldIdentifierOptions,
hiddenTrackingFields: hiddenTrackingFieldsMsg, hiddenTrackingFields: hiddenTrackingFieldsMsg,
/**
* Map of radio Group state values
* Each initially has an indeterminate state, as the user
* progresses through the prompts
* @type {Object.<Boolean, null>}
*/
userIndicatesDatasetHas: {
member: null,
org: null,
group: null
},
didReceiveAttrs() { didReceiveAttrs() {
this._super(...arguments); this._super(...arguments);
// Perform validation step on the received component attributes // Perform validation step on the received component attributes
@ -78,10 +145,16 @@ export default Component.extend({
set(this, '_hasBadData', true); set(this, '_hasBadData', true);
}, },
// Map logicalTypes to options consumable by ui // Map logicalTypes to options consumable by DOM
logicalTypes: ['', ...logicalTypes].map(value => ({ idLogicalTypes: cachedLogicalTypes('id'),
// Map generic logical type to options consumable in DOM
genericLogicalTypes: cachedLogicalTypes('generic'),
// Map classifiers to options better consumed in DOM
classifiers: ['', ...classifiers.sort()].map(value => ({
value, value,
label: value ? value.replace('_', ' ').toLowerCase().capitalize() : 'Please Select' label: value ? formatAsCapitalizedStringWithSpaces(value) : '...'
})), })),
/** /**
@ -103,22 +176,57 @@ export default Component.extend({
return get(this, 'complianceDataFields').filter(({ identifierField }) => !isTrackingHeaderField(identifierField)); return get(this, 'complianceDataFields').filter(({ identifierField }) => !isTrackingHeaderField(identifierField));
}), }),
/**
* Checks that all tags/ dataset content types have a boolean value
* @type {Ember.computed}
*/
isDatasetFullyClassified: computed('datasetClassification', function() {
const datasetClassification = get(this, 'datasetClassification');
return Object.keys(datasetClassification)
.map(key => ({ value: datasetClassification[key].value }))
.every(({ value }) => [true, false].includes(value));
}),
/**
* Computed property that is dependent on all the keys in the datasetClassification map
* Returns a new map of datasetClassificationKey: String-> Object.<Boolean|undefined,String>
* @type {Ember.computed}
*/
datasetClassification: computed(`${datasetClassificationKey}.{${datasetClassifiersKeys.join(',')}}`, function() {
const sourceDatasetClassification = get(this, datasetClassificationKey) || {};
return Object.keys(datasetClassifiers).reduce((datasetClassification, classifier) => {
return Object.assign({}, datasetClassification, {
[classifier]: {
value: sourceDatasetClassification[classifier],
label: datasetClassifiers[classifier]
}
});
}, {});
}),
/** /**
* Lists all dataset fields found in the `columns` performs an intersection * Lists all dataset fields found in the `columns` performs an intersection
* of fields with the currently persisted and/or updated * of fields with the currently persisted and/or updated
* privacyCompliancePolicy.compliancePurgeEntities. * privacyCompliancePolicy.complianceEntities.
* The returned list is a map of fields with current or default privacy properties * The returned list is a map of fields with current or default privacy properties
*/ */
complianceDataFields: computed( complianceDataFields: computed(
`${complianceListKey}.@each.identifierType`, `${policyComplianceEntitiesKey}.@each.identifierType`,
`${complianceListKey}.[]`, `${policyComplianceEntitiesKey}.[]`,
policyFieldClassificationKey,
'schemaFieldNamesMappedToDataTypes', 'schemaFieldNamesMappedToDataTypes',
function() { function() {
const sourceEntities = getWithDefault(this, complianceListKey, []); /**
const complianceFieldNames = sourceEntities.mapBy('identifierField'); * Retrieves an attribute on the `policyComplianceEntitiesKey` where the identifierField is the same as the
* provided field name
* @param attribute
* @param fieldName
* @return {null}
*/
const getAttributeOnField = (attribute, fieldName) => { const getAttributeOnField = (attribute, fieldName) => {
const sourceField = getWithDefault(this, complianceListKey, []).find( const sourceField = getWithDefault(this, policyComplianceEntitiesKey, []).find(
({ identifierField }) => identifierField === fieldName ({ identifierField }) => identifierField === fieldName
); );
return sourceField ? sourceField[attribute] : null; return sourceField ? sourceField[attribute] : null;
@ -135,111 +243,54 @@ export default Component.extend({
attributes.map(attr => getAttributeOnField(attr, fieldName)); attributes.map(attr => getAttributeOnField(attr, fieldName));
// Set default or if already in policy, retrieve current values from // Set default or if already in policy, retrieve current values from
// privacyCompliancePolicy.compliancePurgeEntities // privacyCompliancePolicy.complianceEntities
return getWithDefault( return getWithDefault(
this, this,
'schemaFieldNamesMappedToDataTypes', 'schemaFieldNamesMappedToDataTypes',
[] []
).map(({ fieldName: identifierField, dataType }) => { ).map(({ fieldName: identifierField, dataType }) => {
const hasPrivacyData = complianceFieldNames.includes(identifierField);
const [identifierType, isSubject, logicalType] = getAttributesOnField( const [identifierType, isSubject, logicalType] = getAttributesOnField(
['identifierType', 'isSubject', 'logicalType'], ['identifierType', 'isSubject', 'logicalType'],
identifierField identifierField
); );
// Runtime converts the identifierType to subjectMember if the isSubject flag is true
const computedIdentifierType = identifierType === fieldIdentifierTypes.member.value && isSubject
? fieldIdentifierTypes.subjectMember.value
: identifierType;
const idLogicalTypes = get(this, 'idLogicalTypes');
// Filtered list of id logical types that end with urn, or have no value
const urnFieldFormats = idLogicalTypes.filter(
type => String(type.value).match(endsWithUrnRegex) || !type.value
);
// Get the current classification list
const fieldClassification = get(this, policyFieldClassificationKey);
// The field formats applicable to the current identifierType
let fieldFormats = fieldIdentifierTypeIds.includes(identifierType)
? idLogicalTypes
: get(this, 'genericLogicalTypes');
fieldFormats = identifierType === fieldIdentifierTypes.generic.value ? urnFieldFormats : fieldFormats;
return { return {
dataType, dataType,
identifierField, identifierField,
identifierType, fieldFormats,
isSubject, identifierType: computedIdentifierType,
logicalType, classification: fieldClassification[identifierField],
hasPrivacyData // Same object reference for equality comparision
logicalType: fieldFormats.findBy('value', logicalType)
}; };
}); });
} }
), ),
// Compliance entities filtered for each identifierType
memberComplianceEntities: complianceEntitiesMatchingType('member'),
orgComplianceEntities: complianceEntitiesMatchingType('organization'),
groupComplianceEntities: complianceEntitiesMatchingType('group'),
/** /**
* Changes the logicalType on a field. * Checks that each entity in sourceEntities has a valid logicalType
* Ensures that the logicalType / format is applicable to the specified field * ie. logical Type exists is the idLogicalTypes or genericLogicalTypes
* @param {String} fieldName the fieldName identifying the field to be updated * @param {Ember.Array} sourceEntities complianceEntities
* @param {String} format logicalType or format te field is in
* @return {String| void}
*/
changeFieldLogicalType(fieldName, format) {
const sourceField = get(this, complianceListKey).findBy('identifierField', fieldName);
if (sourceField && logicalTypes.includes(format)) {
return set(sourceField, 'logicalType', String(format).toUpperCase());
}
},
/**
* Adds or removes a field onto the
* privacyCompliancePolicy.compliancePurgeEntities list.
* @param {Object} props initial props for the field to be added
* @prop {String} props.identifierField
* @prop {String} props.dataType
* @param {String} identifierType the type of the field to toggle
* @param {('add'|'remove')} toggle operation to perform, can either be
* add or remove
* @return {Ember.Array|*}
*/
toggleFieldOnComplianceList(props, identifierType, toggle) {
const { identifierField, logicalType } = props;
const sourceEntities = get(this, complianceListKey);
if (!['add', 'remove'].includes(toggle)) {
throw new Error(`Unsupported toggle operation ${toggle}`);
}
return {
add() {
// Ensure that we don't currently have this field present on the
// privacyCompliancePolicy.compliancePurgeEntities list
if (!sourceEntities.findBy('identifierField', identifierField)) {
const addPurgeEntity = {
identifierType,
identifierField,
logicalType
};
return sourceEntities.setObjects([addPurgeEntity, ...sourceEntities]);
}
},
remove() {
// Remove the identifierType since we are removing it from the
// privacyCompliancePolicy.compliancePurgeEntities in case it
// is added back during the session
set(props, 'identifierType', null);
return sourceEntities.setObjects(sourceEntities.filter(item => item.identifierField !== identifierField));
}
}[toggle]();
},
/**
* Checks that each privacyCompliancePolicy.compliancePurgeEntities has
* a valid identifierType
* @param {Ember.Array} sourceEntities compliancePurgeEntities
* @return {Boolean} has or does not
*/
ensureTypeContainsFormat: sourceEntities =>
sourceEntities.every(entity => ['member', 'organization', 'group'].includes(get(entity, 'identifierType'))),
/**
* Checks that each privacyCompliancePolicy.compliancePurgeEntities has
* a valid logicalType
* @param {Ember.Array}sourceEntities compliancePurgeEntities
* @return {Boolean|*} Contains or does not * @return {Boolean|*} Contains or does not
*/ */
ensureTypeContainsLogicalType: sourceEntities => { ensureIdTypesHaveLogicalType: sourceEntities => {
const logicalTypesInUppercase = logicalTypes.map(type => type.toUpperCase()); const logicalTypesInUppercase = [...idLogicalTypes, ...genericLogicalTypes].map(type => type.toUpperCase());
return sourceEntities.every(entity => logicalTypesInUppercase.includes(get(entity, 'logicalType'))); return sourceEntities.every(entity => logicalTypesInUppercase.includes(get(entity, 'logicalType')));
}, },
@ -259,14 +310,13 @@ export default Component.extend({
* TODO: DSS-6672 Extract to notifications service * TODO: DSS-6672 Extract to notifications service
* Helper method to update user when an async server update to the * Helper method to update user when an async server update to the
* security specification is handled. * security specification is handled.
* @param {XMLHttpRequest|Promise|jqXHR|*} request the server request * @param {Promise|*} request the server request
* @param {String} [successMessage] optional _message for successful response * @param {String} [successMessage] optional _message for successful response
* @param { Boolean} [isSaving = false] optional flag indicating when the user intends to persist / save
*/ */
whenRequestCompletes(request, { successMessage } = {}) { whenRequestCompletes(request, { successMessage, isSaving = false } = {}) {
Promise.resolve(request) Promise.resolve(request)
.then(({ status = 'error' }) => { .then(({ status = 'error' }) => {
// The server api currently responds with an object containing
// a status when complete
return status === 'ok' return status === 'ok'
? setProperties(this, { ? setProperties(this, {
_message: successMessage || successUpdating, _message: successMessage || successUpdating,
@ -278,7 +328,7 @@ export default Component.extend({
let _message = `${failedUpdating} \n ${err}`; let _message = `${failedUpdating} \n ${err}`;
let _alertType = 'danger'; let _alertType = 'danger';
if (get(this, 'isNewPrivacyCompliancePolicy')) { if (get(this, 'isNewComplianceInfo') && !isSaving) {
_message = 'This dataset does not have any previously saved fields with a identifying information.'; _message = 'This dataset does not have any previously saved fields with a identifying information.';
_alertType = 'info'; _alertType = 'info';
} }
@ -290,91 +340,133 @@ export default Component.extend({
}); });
}, },
/**
* Sets the default classification for the given identifier field
* @param {String} identifierField
* @param {String} logicalType
*/
setDefaultClassification({ identifierField }, { value: logicalType = '' } = {}) {
const defaultTypeClassification = defaultFieldDataTypeClassification[logicalType] || null;
this.actions.onFieldClassificationChange.call(this, { identifierField }, { value: defaultTypeClassification });
},
actions: { actions: {
/** /**
* * When a user updates the identifierFieldType in the DOM, update the backing store
* @param {String} identifierField id for the field to update * @param identifierField
* @param {String} logicalType updated format to apply to the field * @param logicalType
* @return {*|String|void} logicalType or void * @param identifierType
*/ */
onFieldFormatChange({ identifierField }, { value: logicalType }) { onFieldIdentifierTypeChange({ identifierField, logicalType }, { value: identifierType }) {
// TODO:DSS-6719 refactor into mixin const complianceList = get(this, policyComplianceEntitiesKey);
this.clearMessages(); // A reference to the current field in the compliance list if it exists
return this.changeFieldLogicalType(identifierField, logicalType); const currentFieldInComplianceList = complianceList.findBy('identifierField', identifierField);
const subjectId = fieldIdentifierTypes.subjectMember.value;
const updatedEntity = Object.assign({}, currentFieldInComplianceList, {
identifierField,
identifierType: subjectId === identifierType ? fieldIdentifierTypes.member.value : identifierType,
isSubject: subjectId === identifierType ? true : null,
logicalType: !currentFieldInComplianceList ? logicalType : void 0
});
let transientComplianceList = complianceList;
if (currentFieldInComplianceList) {
transientComplianceList = complianceList.filter(
({ identifierField: fieldName }) => fieldName !== identifierField
);
}
complianceList.setObjects([updatedEntity, ...transientComplianceList]);
this.setDefaultClassification({ identifierField });
}, },
/** /**
* Toggles a field on / off the compliance list * Updates the logical type for the given identifierField
* @param {String} identifierType the type of the field to be toggled on * @param {Object} field
* the privacyCompliancePolicy.compliancePurgeEntities list * @prop {String} field.identifierField
* @param {Object|Ember.Object} props containing the props to be added * @param {Event} e the DOM change event
* @prop {Boolean} props.hasPrivacyData checked or not checked
* @return {*} * @return {*}
*/ */
onFieldPrivacyChange(identifierType, props) { onFieldLogicalTypeChange(field, e) {
// If checked, add, otherwise remove const { identifierField } = field;
const { hasPrivacyData } = props; const { value: logicalType } = e || {};
const toggle = !hasPrivacyData ? 'add' : 'remove'; let sourceIdentifierField = get(this, policyComplianceEntitiesKey).findBy('identifierField', identifierField);
// TODO:DSS-6719 refactor into mixin // If the identifierField does not current exist, invoke onFieldIdentifierChange to add it on the compliance list
this.clearMessages(); if (!sourceIdentifierField) {
this.actions.onFieldIdentifierTypeChange.call(
return this.toggleFieldOnComplianceList(props, identifierType, toggle); this,
{
identifierField,
logicalType
}, },
{ value: fieldIdentifierTypes.none.value }
/** );
* Toggles the isSubject property of a member identifiable field
* @param {Object} props the props on the member field to update
* @prop {Boolean} isSubject flag indicating this field as a subject owner
* when true
* @prop {String} identifierField unique field to update isSubject property
*/
onMemberFieldSubjectChange(props) {
const { isSubject, identifierField: name } = props;
// TODO:DSS-6719 refactor into mixin
this.clearMessages();
// Ensure that a flag isSubject is present on the props
if (props && 'isSubject' in props) {
const sourceField = get(this, complianceListKey).find(({ identifierField }) => identifierField === name);
set(sourceField, 'isSubject', !isSubject);
} }
set(sourceIdentifierField, 'logicalType', logicalType);
return this.setDefaultClassification({ identifierField }, { value: logicalType });
}, },
/** /**
* Updates the state flags that transition the prompts from one to the next * Updates the filed classification
* @param {String} sectionName name of the section that was changed * @param {String} identifierField the identifier field to update the classification for
* @param {Boolean} isPrivacyIdentifiable flag indicating that a section has * @param {String} classification
* or does not have privacy identifier * @return {*}
*/ */
didChangePrivacyIdentifiable(sectionName, isPrivacyIdentifiable) { onFieldClassificationChange({ identifierField }, { value: classification = null }) {
const section = { const fieldClassification = get(this, policyFieldClassificationKey);
'has-group': 'group',
'has-org': 'org',
'has-member': 'member'
}[sectionName];
return set(this, `userIndicatesDatasetHas.${section}`, isPrivacyIdentifiable); // TODO:DSS-6719 refactor into mixin
this.clearMessages();
if (!classification && identifierField in fieldClassification) {
const updatedFieldClassification = Object.assign({}, fieldClassification);
delete updatedFieldClassification[identifierField];
set(this, policyFieldClassificationKey, updatedFieldClassification);
return;
}
set(fieldClassification, identifierField, classification);
// Ember.computedProperties don't seem to have a direct way of depending on the shape of an object changing at
// runtime, notifyPropertyChange forces the computed property recompute the object reference
return notifyPropertyChange.call(this, policyFieldClassificationKey);
},
/**
* Updates the source object representing the current datasetClassification map
* @param {String} classifier the property on the datasetClassification to update
* @param {Boolean} value flag indicating if this dataset contains member data for the specified classifier
*/
onChangeDatasetClassification(classifier, value) {
let sourceDatasetClassification = getWithDefault(this, datasetClassificationKey, {});
// For datasets initially without a datasetClassification, the default value is null
if (sourceDatasetClassification === null) {
sourceDatasetClassification = set(this, datasetClassificationKey, {});
}
return set(sourceDatasetClassification, classifier, value);
}, },
/** /**
* If all validity checks are passed, invoke onSave action on controller * If all validity checks are passed, invoke onSave action on controller
*/ */
saveCompliance() { saveCompliance() {
const complianceList = get(this, complianceListKey); const complianceList = get(this, policyComplianceEntitiesKey);
const allEntitiesHaveValidFormat = this.ensureTypeContainsFormat(complianceList); const idFieldsHaveValidLogicalType = this.ensureIdTypesHaveLogicalType(
const allEntitiesHaveValidLogicalType = this.ensureTypeContainsLogicalType(complianceList); complianceList.filter(({ identifierType }) => fieldIdentifierTypeIds.includes(identifierType))
);
if (idFieldsHaveValidLogicalType) {
return this.whenRequestCompletes(get(this, 'onSave')(), { isSaving: true });
}
if (allEntitiesHaveValidFormat && allEntitiesHaveValidLogicalType) {
return this.whenRequestCompletes(this.get('onSave')());
} else {
setProperties(this, { setProperties(this, {
_message: missingTypes, _message: missingTypes,
_alertType: 'danger' _alertType: 'danger'
}); });
}
}, },
// Rolls back changes made to the compliance spec to current // Rolls back changes made to the compliance spec to current

View File

@ -1,7 +1,6 @@
import Ember from 'ember'; import Ember from 'ember';
import { import {
createPrivacyCompliancePolicy, createInitialComplianceInfo
createSecuritySpecification
} from 'wherehows-web/utils/datasets/functions'; } from 'wherehows-web/utils/datasets/functions';
import { makeUrnBreadcrumbs } from 'wherehows-web/utils/entities'; import { makeUrnBreadcrumbs } from 'wherehows-web/utils/entities';
@ -30,16 +29,13 @@ const getDatasetOwnersUrl = id => `${datasetUrl(id)}/owners`;
const getDatasetInstanceUrl = id => `${datasetUrl(id)}/instances`; const getDatasetInstanceUrl = id => `${datasetUrl(id)}/instances`;
const getDatasetVersionUrl = (id, dbId) => const getDatasetVersionUrl = (id, dbId) =>
`${datasetUrl(id)}/versions/db/${dbId}`; `${datasetUrl(id)}/versions/db/${dbId}`;
const getDatasetSecurityUrl = id => `${datasetUrl(id)}/security`; const getDatasetPrivacyUrl = id => `${datasetUrl(id)}/privacy`;
const getDatasetComplianceUrl = id => `${datasetUrl(id)}/compliance`;
let getDatasetColumn; let getDatasetColumn;
export default Route.extend({ export default Route.extend({
//TODO: DSS-6632 Correct server-side if status:error and record not found but response is 200OK //TODO: DSS-6632 Correct server-side if status:error and record not found but response is 200OK
setupController(controller, model) { setupController(controller, model) {
currentTab = 'Datasets';
window.updateActiveTab();
let source = ''; let source = '';
var id = 0; var id = 0;
var urn = ''; var urn = '';
@ -49,42 +45,12 @@ export default Route.extend({
* @type {{privacyCompliancePolicy: ((id)), securitySpecification: ((id)), datasetSchemaFieldNamesAndTypes: ((id))}} * @type {{privacyCompliancePolicy: ((id)), securitySpecification: ((id)), datasetSchemaFieldNamesAndTypes: ((id))}}
*/ */
const fetchThenSetOnController = { const fetchThenSetOnController = {
privacyCompliancePolicy(id, controller) { async complianceInfo(id, controller) {
Promise.resolve(getJSON(getDatasetComplianceUrl(id))) const response = await Promise.resolve(getJSON(getDatasetPrivacyUrl(id)));
.then(response => { const { msg, status, complianceInfo = createInitialComplianceInfo(id) } = response;
const { const isNewComplianceInfo = status === 'failed' && String(msg).includes('actual 0');
msg,
status,
privacyCompliancePolicy = createPrivacyCompliancePolicy()
} = response;
const isNewPrivacyCompliancePolicy = status === 'failed' &&
String(msg).includes('actual 0');
setProperties(controller, { setProperties(controller, { complianceInfo, isNewComplianceInfo });
privacyCompliancePolicy,
isNewPrivacyCompliancePolicy
});
});
return this;
},
securitySpecification(id, controller) {
Promise.resolve(getJSON(getDatasetSecurityUrl(id)))
.then(response => {
const {
msg,
status,
securitySpecification = createSecuritySpecification(id)
} = response;
const isNewSecuritySpecification = status === 'failed' &&
String(msg).includes('actual 0');
setProperties(controller, {
securitySpecification,
isNewSecuritySpecification
});
});
return this; return this;
}, },

View File

@ -63,6 +63,7 @@
border-width: 1px 0 0; border-width: 1px 0 0;
background-color: #f8f8f8; background-color: #f8f8f8;
z-index: z(dropdown); z-index: z(dropdown);
max-height: 50px;
} }
/** /**

View File

@ -0,0 +1,6 @@
.post-action-notification {
margin-bottom: 0;
display: inline-block;
border-radius: 2px;
padding: 5px;
}

View File

@ -1 +1,2 @@
@import "notification-dot"; @import "notification-dot";
@import "action-notifications";

View File

@ -30,13 +30,13 @@
Start Over Start Over
</button> </button>
</div> </div>
</section>
{{#if _message}} {{#if _message}}
<div class="alert alert-{{_alertType}} post-action-notification" role="alert"> <div class="alert alert-{{_alertType}} post-action-notification" role="alert">
{{_message}} {{_message}}
</div> </div>
{{/if}} {{/if}}
</section>
{{/if}} {{/if}}
{{#if isNewPrivacyPolicy}} {{#if isNewPrivacyPolicy}}
@ -157,8 +157,7 @@
{{tooltip-on-component {{tooltip-on-component
event="click" event="click"
hideOn="click" hideOn="click"
text="A field can contain one or multiple member IDs that do not technically \"own\" the entire record, text=helpText.subjectOwner
e.g. the list of recipients of a message. The field should be marked as a \"Subject Owner\" in this case."
}} }}
</span> </span>
</sup> </sup>
@ -172,8 +171,7 @@
{{tooltip-on-component {{tooltip-on-component
event="click" event="click"
hideOn="click" hideOn="click"
text="For each of the fields below, please classify them based on the data handling table found at go/dht. text=helpText.classification
The default classification shoud suffice in most cases."
}} }}
</span> </span>
</sup> </sup>

View File

@ -7,7 +7,7 @@ const createInitialComplianceInfo = datasetId => ({
datasetId, datasetId,
// default to first item in compliance types list // default to first item in compliance types list
complianceType: 'AUTO_PURGE', complianceType: 'AUTO_PURGE',
compliancePurgeEntities: [], complianceEntities: [],
fieldClassification: {}, fieldClassification: {},
datasetClassification: {}, datasetClassification: {},
geographicAffinity: { affinity: '' }, geographicAffinity: { affinity: '' },