import Ember from 'ember';
import isTrackingHeaderField from 'wherehows-web/utils/validators/tracking-headers';
import {
classifiers,
datasetClassifiers,
fieldIdentifierTypes,
fieldIdentifierTypeIds,
idLogicalTypes,
nonIdFieldLogicalTypes,
defaultFieldDataTypeClassification,
compliancePolicyStrings,
logicalTypesForIds,
logicalTypesForGeneric,
hasPredefinedFieldFormat,
getDefaultLogicalType,
lastSeenSuggestionInterval
} from 'wherehows-web/constants';
import {
isPolicyExpectedShape,
fieldChangeSetRequiresReview,
mergeMappedColumnFieldsWithSuggestions
} from 'wherehows-web/utils/datasets/compliance-policy';
import scrollMonitor from 'scrollmonitor';
import { hasEnumerableKeys } from 'wherehows-web/utils/object';
import { arrayFilter, isListUnique } from 'wherehows-web/utils/array';
const {
assert,
Component,
computed,
set,
get,
setProperties,
getProperties,
getWithDefault,
String: { htmlSafe },
inject: { service }
} = Ember;
const {
complianceDataException,
missingTypes,
successUpdating,
failedUpdating,
helpText,
successUploading,
invalidPolicyData
} = compliancePolicyStrings;
const hiddenTrackingFieldsMsg = htmlSafe(
'
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.
" +
'
For example: header.memberId, requestHeader. ' +
'Hopefully, this saves you some scrolling!
'
);
/**
* 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();
/**
* List of non Id field data type classifications
* @type {Array}
*/
const genericLogicalTypes = Object.keys(nonIdFieldLogicalTypes).sort();
/**
* 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);
/**
* A reference to the compliance policy entities on the complianceInfo map
* @type {string}
*/
const policyComplianceEntitiesKey = 'complianceInfo.complianceEntities';
assert('`fieldIdentifierTypes` contains an object with a key `none`', typeof fieldIdentifierTypes.none === 'object');
const fieldIdentifierTypeKeysBarNone = Object.keys(fieldIdentifierTypes).filter(k => k !== 'none');
const fieldDisplayKeys = ['none', '_', ...fieldIdentifierTypeKeysBarNone];
/**
* Returns a list of changeSet fields that requires user attention
* @type {function({}): Array<{ isDirty, suggestion, privacyPolicyExists, suggestionAuthority }>}
*/
const changeSetFieldsRequiringReview = arrayFilter(fieldChangeSetRequiresReview);
/**
* A list of field identifier types mapped to label, value options for select display
* @type {any[]|Array.<{value: String, label: String}>}
*/
const fieldIdentifierOptions = fieldDisplayKeys.map(fieldIdentifierType => {
const divider = '──────────';
const { value = fieldIdentifierType, displayAs: label = divider } = fieldIdentifierTypes[fieldIdentifierType] || {};
// Adds a divider for a value of _
// Visually this separates ID from none fieldIdentifierTypes
return {
value,
label,
isDisabled: fieldIdentifierType === '_'
};
});
export default Component.extend({
sortColumnWithName: 'identifierField',
filterBy: 'identifierField',
sortDirection: 'asc',
searchTerm: '',
helpText,
fieldIdentifierOptions,
hiddenTrackingFields: hiddenTrackingFieldsMsg,
classNames: ['compliance-container'],
classNameBindings: ['isEditing:compliance-container--edit-mode'],
/**
* Flag indicating that the component is in edit mode
* @type {String}
*/
isEditing: computed('isNewComplianceInfo', 'isEditingDatasetClassification', 'isEditingCompliancePolicy', function() {
const { isNewComplianceInfo, isEditingDatasetClassification, isEditingCompliancePolicy } = getProperties(
this,
'isNewComplianceInfo',
'isEditingDatasetClassification',
'isEditingCompliancePolicy'
);
return isNewComplianceInfo || isEditingDatasetClassification || isEditingCompliancePolicy;
}),
/**
* Convenience flag indicating the policy is not currently being edited
* @type {Ember.computed}
* @return {boolean}
*/
isReadOnly: computed.not('isEditing'),
/**
* Flag indicating that the component is currently saving / attempting to save the privacy policy
* @type {String}
*/
isSaving: false,
/**
* Determines if the the compliance policy update form should be shown
* @type {Ember.computed}
* @return {boolean}
*/
isShowingComplianceEditMode: computed('isNewComplianceInfo', 'isEditingCompliancePolicy', function() {
const { isNewComplianceInfo, isEditingCompliancePolicy, isEditingDatasetClassification } = getProperties(
this,
'isNewComplianceInfo',
'isEditingCompliancePolicy',
'isEditingDatasetClassification'
);
return (isNewComplianceInfo || isEditingCompliancePolicy) && !isEditingDatasetClassification;
}),
/**
* Proxy to the check if the dataset classification form is being edited and should be shown
* @type {Ember.computed}
* @return {boolean}
*/
isShowingDatasetClassificationEditMode: computed.bool('isEditingDatasetClassification'),
datasetComplianceSteps: computed('isEditingCompliancePolicy', 'isEditingDatasetClassification', function() {
const { isEditingCompliancePolicy, isEditingDatasetClassification } = getProperties(
this,
'isEditingCompliancePolicy',
'isEditingDatasetClassification'
);
return [isEditingCompliancePolicy, isEditingDatasetClassification].map((_step, index) => ({
done: !index ? !!isEditingDatasetClassification : false
}));
}),
/**
* Returns a list of ui values and labels for review filter drop-down
* @type {Ember.computed}
*/
fieldReviewOptions: computed(function() {
return [
{ value: 'showAll', label: 'Showing all fields' },
{
value: 'showReview',
label: 'Showing only fields to review'
}
];
}),
/**
* Toggles the field review option based on if the dataset
* already has a compliance policy created for fields
* @type {string}
*/
fieldReviewOption: computed('isNewComplianceInfo', function() {
return get(this, 'isNewComplianceInfo') ? 'showAll' : 'showReview';
}),
/**
* Reference to the application notifications Service
* @type {Ember.Service}
*/
notifications: service(),
didReceiveAttrs() {
this._super(...Array.from(arguments));
// Perform validation step on the received component attributes
this.validateAttrs();
},
/**
* @override
*/
didRender() {
this._super(...arguments);
// Hides DOM elements that are not currently visible in the UI and unhides them once the user scrolls the
// elements into view
this.enableDomCloaking();
},
/**
* A `lite` / intermediary step to occlusion culling, this helps to improve the rendering of
* elements that are currently rendered in the viewport by hiding that aren't.
* Setting them to visibility hidden doesn't remove them from the document flow, but the browser
* doesn't have to deal with layout for the affected elements since they are off-screen
*/
enableDomCloaking() {
const [dom] = this.$('.dataset-compliance-fields');
const triggerCount = 100;
if (dom) {
const rows = dom.querySelectorAll('tbody tr');
// if we already have watchers for elements, or if the elements previously cached are no longer valid,
// e.g. those elements were destroyed when new data was received, pagination etc
if (rows.length > triggerCount && (!this.complianceWatchers || !this.complianceWatchers.has(rows[0]))) {
/**
* If an item is not in the viewport add a class to occlude it
*/
const cloaker = function() {
if (!this.isInViewport) {
return this.watchItem.classList.add('compliance-row--off-screen');
}
this.watchItem.classList.remove('compliance-row--off-screen');
};
this.watchers = [];
// Retain a weak reference to DOM nodes
this.complianceWatchers = new WeakMap(
[...rows].map(row => {
const watcher = scrollMonitor.create(row);
watcher['stateChange'](cloaker);
cloaker.call(watcher);
this.watchers = [...this.watchers, watcher];
return [watcher.watchItem, watcher];
})
);
}
}
},
/**
* Cleans up the artifacts from the dom cloaking operation, drops references held by scroll monitor
*/
disableDomCloaking() {
if (!this.watchers || !Array.isArray(this.watchers)) {
return;
}
this.watchers.forEach(watcher => watcher.destroy());
},
/**
* @override
*/
willDestroyElement() {
this.disableDomCloaking();
},
/**
* Ensure that props received from on this component
* are valid, otherwise flag
*/
validateAttrs() {
const fieldNames = getWithDefault(this, 'schemaFieldNamesMappedToDataTypes', []).mapBy('fieldName');
// identifier field names from the column api should be unique
if (isListUnique(fieldNames.sort())) {
return set(this, '_hasBadData', false);
}
// Flag this component's data as problematic
set(this, '_hasBadData', true);
},
// Map logicalTypes to options consumable by DOM
idLogicalTypes: logicalTypesForIds,
// Map generic logical type to options consumable in DOM
genericLogicalTypes: logicalTypesForGeneric,
// Map classifiers to options better consumed in DOM
classifiers: ['', ...classifiers.sort()].map(value => ({
value,
label: value ? formatAsCapitalizedStringWithSpaces(value) : '...'
})),
/**
* Caches the policy's modification time in milliseconds
*/
policyModificationTimeInEpoch: computed('complianceInfo', function() {
return getWithDefault(this, 'complianceInfo.modifiedTime', 0);
}),
/**
* Checks that suggested values postdate the last save date or that suggestions exist
* @type {boolean}
*/
hasRecentSuggestions: computed('policyModificationTimeInEpoch', 'complianceSuggestion', function() {
const { policyModificationTimeInEpoch, complianceSuggestion = {} } = getProperties(this, [
'policyModificationTimeInEpoch',
'complianceSuggestion'
]);
const { lastModified: suggestionsLastModified, complianceSuggestions = [] } = complianceSuggestion;
// If modification dates exist, check that the suggestions are considered 'unseen' since the last time the policy was saved
// and we have at least 1 suggestion, otherwise check that the count of suggestions is at least 1
if (policyModificationTimeInEpoch && suggestionsLastModified) {
const suggestionIsUnseen = suggestionsLastModified - policyModificationTimeInEpoch >= lastSeenSuggestionInterval;
return complianceSuggestions.length && suggestionIsUnseen;
}
return !!complianceSuggestions.length;
}),
/**
* @type {Boolean} cached boolean flag indicating that fields do contain a `kafka type`
* tracking header.
* Used to indicate to viewer that these fields are hidden.
*/
containsHiddenTrackingFields: computed('truncatedColumnFields.length', function() {
// If their is a diff in schemaFieldNamesMappedToDataTypes and truncatedColumnFields,
// then we have hidden tracking fields
return get(this, 'truncatedColumnFields.length') !== get(this, 'schemaFieldNamesMappedToDataTypes.length');
}),
/**
* @type {Array.