mirror of
https://github.com/datahub-project/datahub.git
synced 2025-11-11 08:52:58 +00:00
DSS-6551 Rewrites dataset-confidential component and component template to support similar dataset-table based ui to dataset-compliance component. Enforces 1-to-1 mapping from identifierField -> classification. Changes wording from Reset to Start Over on both prompts.
This commit is contained in:
parent
c441c00dbc
commit
a39558c388
@ -1,121 +1,137 @@
|
|||||||
import Ember from 'ember';
|
import Ember from 'ember';
|
||||||
|
|
||||||
export default Ember.Component.extend({
|
const {
|
||||||
|
get,
|
||||||
|
set,
|
||||||
|
isBlank,
|
||||||
|
computed,
|
||||||
|
getWithDefault,
|
||||||
|
Component
|
||||||
|
} = Ember;
|
||||||
|
|
||||||
|
// String constant identifying the classified fields on the security spec
|
||||||
|
const sourceClassificationKey = 'securitySpecification.classification';
|
||||||
|
const classifiers = [
|
||||||
|
'confidential',
|
||||||
|
'highlyConfidential',
|
||||||
|
'limitedDistribution',
|
||||||
|
'mustBeEncrypted',
|
||||||
|
'mustBeMasked'
|
||||||
|
];
|
||||||
|
/**
|
||||||
|
* 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();
|
||||||
|
|
||||||
|
export default Component.extend({
|
||||||
|
sortColumnWithName: 'identifierField',
|
||||||
|
filterBy: 'identifierField',
|
||||||
|
sortDirection: 'asc',
|
||||||
searchTerm: '',
|
searchTerm: '',
|
||||||
retention: Ember.computed.alias('securitySpecification.retentionPolicy.retentionType'),
|
|
||||||
geographicAffinity: Ember.computed.alias('securitySpecification.geographicAffinity.affinity'),
|
// Map classifiers to options better consumed by drop down
|
||||||
recordOwnerType: Ember.computed.alias('securitySpecification.recordOwnerType'),
|
classifiers: ['', ...classifiers].map(value => ({
|
||||||
datasetSchemaFieldNames: Ember.computed('datasetSchemaFieldsAndTypes', function () {
|
value,
|
||||||
return this.get('datasetSchemaFieldsAndTypes').mapBy('name');
|
label: value ? formatAsCapitalizedStringWithSpaces(value) : 'Please Select'
|
||||||
|
})),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a lookup table of fieldNames to classification
|
||||||
|
* Also, the expectation is that the association from fieldName -> classification
|
||||||
|
* is one-to-one hence no check to ensure a fieldName gets clobbered
|
||||||
|
* in the lookup assignment
|
||||||
|
*/
|
||||||
|
fieldNameToClass: computed(`${sourceClassificationKey}`, function () {
|
||||||
|
const sourceClasses = getWithDefault(this, sourceClassificationKey, []);
|
||||||
|
// Creates a lookup table of fieldNames to classification
|
||||||
|
// Also, the expectation is that the association from fieldName -> classification
|
||||||
|
// is one-to-one hence no check to ensure a fieldName gets clobbered
|
||||||
|
// in the lookup assignment
|
||||||
|
return Object.keys(sourceClasses).reduce((lookup, classificationKey) =>
|
||||||
|
// For the provided classificationKey, iterate over it's fieldNames,
|
||||||
|
// and assign the classificationKey to the fieldName in the table
|
||||||
|
(sourceClasses[classificationKey] || []).reduce((lookup, fieldName) => {
|
||||||
|
// cKey -> 1...fieldNameList => fieldName -> cKey
|
||||||
|
lookup[fieldName] = classificationKey;
|
||||||
|
return lookup;
|
||||||
|
}, lookup),
|
||||||
|
{}
|
||||||
|
);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
matchingFields: Ember.computed('searchTerm', 'datasetSchemaFieldsAndTypes', function () {
|
/**
|
||||||
if (this.get('datasetSchemaFieldsAndTypes')) {
|
* Lists all the dataset fields found in the `columns` api, and intersects
|
||||||
const searchTerm = this.get('searchTerm');
|
* each with the currently classified field names in
|
||||||
const matches = $.ui.autocomplete.filter(this.get('datasetSchemaFieldNames'), searchTerm);
|
* securitySpecification.classification or null if not found
|
||||||
return matches.map(value => {
|
*/
|
||||||
const {type} = this.get('datasetSchemaFieldsAndTypes').filterBy('name', value).get('firstObject');
|
classificationDataFields: computed(
|
||||||
const dataType = Array.isArray(type) && type.toString().toUpperCase();
|
`${sourceClassificationKey}.${classifiers}`,
|
||||||
|
'fieldNameToClass',
|
||||||
return {
|
'schemaFieldNamesMappedToDataTypes',
|
||||||
value,
|
function () {
|
||||||
dataType
|
// Set default or if already in policy, retrieve current values from
|
||||||
};
|
// privacyCompliancePolicy.compliancePurgeEntities
|
||||||
});
|
return getWithDefault(
|
||||||
|
this, 'schemaFieldNamesMappedToDataTypes', []
|
||||||
|
).map(({fieldName, dataType}) => ({
|
||||||
|
dataType,
|
||||||
|
identifierField: fieldName,
|
||||||
|
classification: get(this, `fieldNameToClass.${fieldName}`) || null
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
}),
|
),
|
||||||
|
|
||||||
didRender() {
|
|
||||||
const $typeahead = $('#confidential-typeahead') || [];
|
|
||||||
if ($typeahead.length) {
|
|
||||||
this.enableTypeaheadOn($typeahead);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
enableTypeaheadOn(selector) {
|
|
||||||
selector.autocomplete({
|
|
||||||
minLength: 0,
|
|
||||||
source: request => {
|
|
||||||
const {term = ''} = request;
|
|
||||||
this.set('searchTerm', term);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
get recordOwnerTypes() {
|
|
||||||
return ['MEMBER', 'CUSTOMER', 'JOINT', 'INTERNAL', 'COMPANY'].map(ownerType => ({
|
|
||||||
value: ownerType,
|
|
||||||
label: ownerType.toLowerCase().capitalize()
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
|
|
||||||
get retentionTypes() {
|
|
||||||
return ['LIMITED', 'LEGAL_HOLD', 'UNLIMITED'].map(retention => ({
|
|
||||||
value: retention,
|
|
||||||
label: retention.replace('_', ' ').toLowerCase().capitalize()
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
|
|
||||||
get affinityTypes() {
|
|
||||||
return ['LIMITED', 'EXCLUDED'].map(affinity => ({
|
|
||||||
value: affinity,
|
|
||||||
label: affinity.toLowerCase().capitalize()
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
|
|
||||||
classification: Ember.computed('securitySpecification.classification', function () {
|
|
||||||
const confidentialClassification = this.get('securitySpecification.classification');
|
|
||||||
const formatAsCapitalizedStringWithSpaces = string => string.replace(/[A-Z]/g, match => ` ${match}`).capitalize();
|
|
||||||
|
|
||||||
return Object.keys(confidentialClassification).map(classifier => ({
|
|
||||||
key: classifier,
|
|
||||||
label: formatAsCapitalizedStringWithSpaces(classifier),
|
|
||||||
values: Ember.get(confidentialClassification, classifier)
|
|
||||||
}));
|
|
||||||
}),
|
|
||||||
|
|
||||||
_toggleOnClassification(classifier, key, operation) {
|
|
||||||
this.get(`securitySpecification.classification.${key}`)[`${operation}Object`](classifier);
|
|
||||||
},
|
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
addToClassification(classifier, classifierKey) {
|
/**
|
||||||
this._toggleOnClassification(classifier, classifierKey, `add`);
|
* Toggles the provided identifierField onto a classification list
|
||||||
|
* on securitySpecification.classification, identified by the provided
|
||||||
|
* classKey.
|
||||||
|
* @param {String} identifierField field on the dataset
|
||||||
|
* @param {String} classKey the name of the class to add, or potentially
|
||||||
|
* remove the identifierField from
|
||||||
|
*/
|
||||||
|
updateClassification({identifierField}, {value: classKey}) {
|
||||||
|
const currentClass = get(this, `fieldNameToClass.${identifierField}`);
|
||||||
|
// Since the association from identifierField -> classification is 1-to-1
|
||||||
|
// ensure that we do not currently have this identifierField
|
||||||
|
// in any other classification lists by checking that the lookup is void
|
||||||
|
if (!isBlank(currentClass)) {
|
||||||
|
// Get the current classification list
|
||||||
|
const currentClassification = get(
|
||||||
|
this, `${sourceClassificationKey}.${currentClass}`
|
||||||
|
);
|
||||||
|
// Remove identifierField from list
|
||||||
|
currentClassification.removeObject(identifierField);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (classKey) {
|
||||||
|
// Get the candidate list
|
||||||
|
let classification = get(this, `${sourceClassificationKey}.${classKey}`);
|
||||||
|
// In the case that the list is not pre-populated,
|
||||||
|
// the value will be the default null, array ops won't work here
|
||||||
|
// ...so make array
|
||||||
|
if (!classification) {
|
||||||
|
classification = set(this, `${sourceClassificationKey}.${classKey}`, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally perform operation
|
||||||
|
classification.addObject(identifierField);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
removeFromClassification(classifier, classifierKey) {
|
// Notify controller to propagate changes
|
||||||
this._toggleOnClassification(classifier, classifierKey, `remove`);
|
saveSecuritySpecification() {
|
||||||
},
|
get(this, 'onSave')();
|
||||||
|
|
||||||
updateRetentionType({value}) {
|
|
||||||
this.set('securitySpecification.retentionPolicy.retentionType', value);
|
|
||||||
},
|
|
||||||
|
|
||||||
updateGeographicAffinity({value}) {
|
|
||||||
this.set('securitySpecification.geographicAffinity.affinity', value);
|
|
||||||
},
|
|
||||||
|
|
||||||
updateRecordOwnerType({value}) {
|
|
||||||
this.set('securitySpecification.recordOwnerType', value);
|
|
||||||
},
|
|
||||||
|
|
||||||
saveSecuritySpecification () {
|
|
||||||
this.get('onSave')();
|
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
approveCompliance () {
|
|
||||||
//TODO: not implemented
|
|
||||||
},
|
|
||||||
|
|
||||||
disapproveCompliance () {
|
|
||||||
//TODO: not implemented
|
|
||||||
},
|
|
||||||
|
|
||||||
// Rolls back changes made to the compliance spec to current
|
// Rolls back changes made to the compliance spec to current
|
||||||
// server state
|
// server state
|
||||||
resetSecuritySpecification () {
|
resetSecuritySpecification() {
|
||||||
this.get('onReset')();
|
get(this, 'onReset')();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -7,9 +7,9 @@
|
|||||||
</button>
|
</button>
|
||||||
<button class="nacho-button nacho-button--large"
|
<button class="nacho-button nacho-button--large"
|
||||||
{{action "resetCompliance"}}>
|
{{action "resetCompliance"}}>
|
||||||
<i class="fa fa-times" title="Reset">
|
<i class="fa fa-times" title="Start Over">
|
||||||
</i>
|
</i>
|
||||||
Reset
|
Start Over
|
||||||
</button>
|
</button>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,67 @@
|
|||||||
<div id="confidential" class="tab-body">
|
<div id="confidential" class="tab-body">
|
||||||
|
<section class="action-bar">
|
||||||
|
<button class="nacho-button nacho-button--large-inverse"
|
||||||
|
{{action "saveSecuritySpecification"}}>
|
||||||
|
{{if isNewSecuritySpecification "Create Confidential Specification"
|
||||||
|
"Confirm Confidential Fields"}}
|
||||||
|
</button>
|
||||||
|
<button class="nacho-button nacho-button--large"
|
||||||
|
{{action "resetSecuritySpecification"}}>
|
||||||
|
<i class="fa fa-times" title="Start Over">
|
||||||
|
</i>
|
||||||
|
Start Over
|
||||||
|
</button>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="metadata-prompt" id="has-confidentiality">
|
<section class="metadata-prompt" id="has-confidentiality">
|
||||||
<header>
|
<header class="metadata-prompt__header">
|
||||||
<p>Which fields should be classified as Confidential or Highly Confidential (if any)?</p>
|
<p>Which fields should be classified as Confidential or Highly Confidential (if any)?</p>
|
||||||
</header>
|
</header>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{{#dataset-table fields=classificationDataFields
|
||||||
|
sortColumnWithName=sortColumnWithName
|
||||||
|
filterBy=filterBy
|
||||||
|
sortDirection=sortDirection
|
||||||
|
searchTerm=searchTerm as |table|}}
|
||||||
|
<caption>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
title="Filter fields"
|
||||||
|
placeholder="Filter fields"
|
||||||
|
value="{{table.searchTerm}}"
|
||||||
|
oninput={{action table.filterDidChange value="target.value"}}>
|
||||||
|
</caption>
|
||||||
|
{{#table.head as |head|}}
|
||||||
|
{{#head.column columnName="identifierField"}}Field{{/head.column}}
|
||||||
|
{{#head.column}}Security Classification{{/head.column}}
|
||||||
|
{{/table.head}}
|
||||||
|
{{#table.body as |body|}}
|
||||||
|
{{#each
|
||||||
|
(slice table.beginOffset table.endOffset (sort-by table.sortBy table.data)) as |field|}}
|
||||||
|
{{#body.row as |row|}}
|
||||||
|
{{#row.cell}}
|
||||||
|
{{field.identifierField}}
|
||||||
|
{{/row.cell}}
|
||||||
|
{{#row.cell}}
|
||||||
|
|
||||||
|
{{ember-selector values=classifiers
|
||||||
|
selected=field.classification
|
||||||
|
selectionDidChange=(action "updateClassification" field)}}
|
||||||
|
{{/row.cell}}
|
||||||
|
{{/body.row}}
|
||||||
|
{{/each}}
|
||||||
|
{{/table.body}}
|
||||||
|
{{#table.foot}}
|
||||||
|
{{dataset-table-pager data=table.data
|
||||||
|
page=table.page
|
||||||
|
limit=table.limit
|
||||||
|
pageLengths=table.pageLengths
|
||||||
|
beginOffset=table.beginOffset
|
||||||
|
endOffset=table.endOffset
|
||||||
|
onLimitChanged=table.onLimitChanged
|
||||||
|
onPageChanged=table.onPageChanged}}
|
||||||
|
{{/table.foot}}
|
||||||
|
{{/dataset-table}}
|
||||||
</div>
|
</div>
|
||||||
{{yield}}
|
{{yield}}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user