Merge pull request #642 from theseyi/master

reduces latency on user interaction for large field sizes on compliance tab. create separate working copy of compliance entities based on column fields.
This commit is contained in:
Seyi Adebajo 2017-08-08 14:26:33 -07:00 committed by GitHub
commit f25b378740
19 changed files with 639 additions and 884 deletions

View File

@ -2,7 +2,10 @@ module.exports = {
"extends": "eslint:recommended", "extends": "eslint:recommended",
"parserOptions": { "parserOptions": {
"ecmaVersion": 8, "ecmaVersion": 8,
"sourceType": "module" "sourceType": "module",
"ecmaFeatures": {
"experimentalObjectRestSpread": true
}
}, },
"env": { "env": {
"browser": true, "browser": true,

View File

@ -1,32 +0,0 @@
{
"predef": [
"document",
"window",
"Promise"
],
"browser": true,
"boss": true,
"curly": true,
"debug": false,
"devel": true,
"eqeqeq": true,
"evil": true,
"forin": false,
"immed": false,
"laxbreak": false,
"newcap": true,
"noarg": true,
"noempty": false,
"nonew": false,
"nomen": false,
"onevar": false,
"plusplus": false,
"regexp": false,
"undef": true,
"sub": true,
"strict": false,
"white": false,
"eqnull": true,
"esversion": 6,
"unused": true
}

View File

@ -0,0 +1,153 @@
import Ember from 'ember';
import DatasetTableRow from 'wherehows-web/components/dataset-table-row';
import { fieldIdentifierTypes, defaultFieldDataTypeClassification } from 'wherehows-web/constants';
import {
fieldIdentifierTypeIds,
logicalTypesForIds,
logicalTypesForGeneric
} from 'wherehows-web/components/dataset-compliance';
const { computed, get } = Ember;
const isMixedId = identifierType => identifierType === fieldIdentifierTypes.generic.value;
const isCustomId = identifierType => identifierType === fieldIdentifierTypes.custom.value;
export default DatasetTableRow.extend({
/**
* @type {Array} logical id types mapped to options for <select>
*/
logicalTypesForIds,
/**
* @type {Array} logical generic types mapped to options for <select>
*/
logicalTypesForGeneric,
/**
* flag indicating that the identifier type is a generic value
* @type {Ember.computed<boolean>}
*/
isMixed: computed.equal('field.identifierType', fieldIdentifierTypes.generic.value),
/**
* flag indicating that the identifier type is a custom value
* @type {Ember.computed<boolean>}
*/
isCustom: computed.equal('field.identifierType', fieldIdentifierTypes.custom.value),
/**
* aliases the identifierField on the field
* @type {Ember.computed<string>}
*/
identifierField: computed.alias('field.identifierField'),
/**
* aliases the data type for the field
* @type {Ember.computed<string>}
*/
dataType: computed.alias('field.dataType'),
/**
* Checks if the field format drop-down should be disabled based on the type of the field
* @type {Ember.computed}
*/
isFieldFormatDisabled: computed('field.identifierType', function() {
const identifierType = get(this, 'field.identifierType');
return isMixedId(identifierType) || isCustomId(identifierType);
}).readOnly(),
/**
* Returns a computed value for the field identifierType
* isSubject flag on the field is represented as MemberId(Subject Member Id)
* @type {Ember.computed<string>}
*/
identifierType: computed('field.identifierType', 'field.isSubject', function() {
const identifierType = get(this, 'field.identifierType');
return identifierType === fieldIdentifierTypes.member.value && get(this, 'field.isSubject')
? fieldIdentifierTypes.subjectMember.value
: identifierType;
}).readOnly(),
/**
* A list of field formats that are determined based on the field identifierType
* @type {Ember.computed<Array>}
*/
fieldFormats: computed('field.identifierType', function() {
const identifierType = get(this, 'field.identifierType');
const logicalTypesForIds = get(this, 'logicalTypesForIds');
const mixed = isMixedId(identifierType);
const custom = isCustomId(identifierType);
let fieldFormats = fieldIdentifierTypeIds.includes(identifierType)
? logicalTypesForIds
: get(this, 'logicalTypesForGeneric');
const urnFieldFormat = logicalTypesForIds.findBy('value', 'URN');
fieldFormats = mixed ? urnFieldFormat : fieldFormats;
fieldFormats = custom ? void 0 : fieldFormats;
return fieldFormats;
}),
/**
* The fields logical type, rendered as an Object
* @type {Ember.computed<Object>}
*/
logicalType: computed('field.logicalType', function() {
const fieldFormats = get(this, 'fieldFormats');
const logicalType = get(this, 'field.logicalType');
// Same object reference for equality comparision
return Array.isArray(fieldFormats) ? fieldFormats.findBy('value', logicalType) : fieldFormats;
}),
/**
* The field security classification
* @type {Ember.computed}
*/
classification: computed('field.classification', 'field.identifierType', function() {
const identifierType = get(this, 'field.identifierType');
const mixed = isMixedId(identifierType);
// Filtered list of id logical types that end with urn, or have no value
const urnFieldFormat = get(this, 'logicalTypesForIds').findBy('value', 'URN');
return get(this, 'field.classification') || (mixed && defaultFieldDataTypeClassification[urnFieldFormat.value]);
}),
actions: {
/**
* Handles UI changes to the field identifierType
* @param {string} value
*/
onFieldIdentifierTypeChange({ value }) {
const { onFieldIdentifierTypeChange } = this.attrs;
if (typeof onFieldIdentifierTypeChange === 'function') {
onFieldIdentifierTypeChange(get(this, 'field'), { value });
}
},
/**
* Handles the updates when the field logical type changes on this field
* @param {Event|null} e
*/
onFieldLogicalTypeChange(e) {
const { value } = e || {};
const { onFieldLogicalTypeChange } = this.attrs;
if (typeof onFieldLogicalTypeChange === 'function') {
onFieldLogicalTypeChange(get(this, 'field'), { value });
}
},
/**
* Handles UI change to field security classification
* @param {string} value
*/
onFieldClassificationChange({ value }) {
const { onFieldClassificationChange } = this.attrs;
if (typeof onFieldClassificationChange === 'function') {
onFieldClassificationChange(get(this, 'field'), { value });
}
}
}
});

View File

@ -10,6 +10,7 @@ import {
compliancePolicyStrings compliancePolicyStrings
} from 'wherehows-web/constants'; } from 'wherehows-web/constants';
import { isPolicyExpectedShape } from 'wherehows-web/utils/datasets/functions'; import { isPolicyExpectedShape } from 'wherehows-web/utils/datasets/functions';
import scrollMonitor from 'scrollmonitor';
const { const {
assert, assert,
@ -20,7 +21,6 @@ const {
setProperties, setProperties,
getProperties, getProperties,
getWithDefault, getWithDefault,
isEmpty,
String: { htmlSafe } String: { htmlSafe }
} = Ember; } = Ember;
@ -105,7 +105,7 @@ const fieldIdentifierOptions = fieldDisplayKeys.map(fieldIdentifierType => {
* A list of field identifier types that are Ids i.e member ID, org ID, group ID * A list of field identifier types that are Ids i.e member ID, org ID, group ID
* @type {any[]|Array.<String>} * @type {any[]|Array.<String>}
*/ */
const fieldIdentifierTypeIds = Object.keys(fieldIdentifierTypes) export const fieldIdentifierTypeIds = Object.keys(fieldIdentifierTypes)
.map(fieldIdentifierType => fieldIdentifierTypes[fieldIdentifierType]) .map(fieldIdentifierType => fieldIdentifierTypes[fieldIdentifierType])
.filter(({ isId }) => isId) .filter(({ isId }) => isId)
.mapBy('value'); .mapBy('value');
@ -127,6 +127,12 @@ const cachedLogicalTypes = logicalType =>
})); }));
}); });
// Map logicalTypes to options consumable by DOM
export const logicalTypesForIds = cachedLogicalTypes('id');
// Map generic logical type to options consumable in DOM
export const logicalTypesForGeneric = cachedLogicalTypes('generic');
export default Component.extend({ export default Component.extend({
sortColumnWithName: 'identifierField', sortColumnWithName: 'identifierField',
filterBy: 'identifierField', filterBy: 'identifierField',
@ -156,6 +162,75 @@ export default Component.extend({
this.validateAttrs(); 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 * Ensure that props received from on this component
* are valid, otherwise flag * are valid, otherwise flag
@ -173,10 +248,10 @@ export default Component.extend({
}, },
// Map logicalTypes to options consumable by DOM // Map logicalTypes to options consumable by DOM
idLogicalTypes: cachedLogicalTypes('id'), idLogicalTypes: logicalTypesForIds,
// Map generic logical type to options consumable in DOM // Map generic logical type to options consumable in DOM
genericLogicalTypes: cachedLogicalTypes('generic'), genericLogicalTypes: logicalTypesForGeneric,
// Map classifiers to options better consumed in DOM // Map classifiers to options better consumed in DOM
classifiers: ['', ...classifiers.sort()].map(value => ({ classifiers: ['', ...classifiers.sort()].map(value => ({
@ -196,17 +271,17 @@ export default Component.extend({
* tracking header. * tracking header.
* Used to indicate to viewer that these fields are hidden. * Used to indicate to viewer that these fields are hidden.
*/ */
containsHiddenTrackingFields: computed('truncatedSchemaFields.length', function() { containsHiddenTrackingFields: computed('truncatedColumnFields.length', function() {
// If their is a diff in complianceDataFields and truncatedSchemaFields, // If their is a diff in schemaFieldNamesMappedToDataTypes and truncatedColumnFields,
// then we have hidden tracking fields // then we have hidden tracking fields
return get(this, 'truncatedSchemaFields.length') !== get(this, 'complianceDataFields.length'); return get(this, 'truncatedColumnFields.length') !== get(this, 'schemaFieldNamesMappedToDataTypes.length');
}), }),
/** /**
* @type {Array.<Object>} Filters the mapped compliance data fields without `kafka type` * @type {Array.<Object>} Filters the mapped compliance data fields without `kafka type`
* tracking headers * tracking headers
*/ */
truncatedSchemaFields: computed('schemaFieldNamesMappedToDataTypes', function() { truncatedColumnFields: computed('schemaFieldNamesMappedToDataTypes', function() {
return getWithDefault(this, 'schemaFieldNamesMappedToDataTypes', []).filter( return getWithDefault(this, 'schemaFieldNamesMappedToDataTypes', []).filter(
({ fieldName }) => !isTrackingHeaderField(fieldName) ({ fieldName }) => !isTrackingHeaderField(fieldName)
); );
@ -270,107 +345,76 @@ export default Component.extend({
* privacyCompliancePolicy.complianceEntities. * 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( mergeComplianceEntitiesAndColumnFields(columnIdFieldsToCurrentPrivacyPolicy = {}, truncatedColumnFields = []) {
`${policyComplianceEntitiesKey}.@each.identifierType`, return truncatedColumnFields.map(({ fieldName: identifierField, dataType }) => {
`${policyComplianceEntitiesKey}.[]`, const { [identifierField]: { identifierType, isSubject, logicalType } } = columnIdFieldsToCurrentPrivacyPolicy;
policyFieldClassificationKey, const { [identifierField]: classification } = get(this, policyFieldClassificationKey) || {};
'truncatedSchemaFields', return {
function() { identifierField,
/** dataType,
* Retrieves an attribute on the `policyComplianceEntitiesKey` where the identifierField is the same as the identifierType,
* provided field name isSubject,
* @param attribute logicalType,
* @param fieldName classification
* @return {null}
*/
const getAttributeOnField = (attribute, fieldName) => {
const complianceEntities = get(this, policyComplianceEntitiesKey) || [];
// const sourceField = complianceEntities.find(({ identifierField }) => identifierField === fieldName);
let sourceField;
// For long records: >500 elements, the find operation is consistently less performant than a for-loop:
// trading elegance for efficiency here
for (let i = 0; i < complianceEntities.length; i++) {
if (complianceEntities[i]['identifierField'] === fieldName) {
sourceField = complianceEntities[i];
break;
}
}
return sourceField ? sourceField[attribute] : null;
}; };
});
},
/** /**
* Get value for a list of attributes *
* @param {Array} attributes list of attribute keys to pull from * @param {Array} columnFieldNames
* sourceField * @return {*|{}|any}
* @param {String} fieldName name of the field to lookup */
* @return {Array} list of attribute values mapColumnIdFieldsToCurrentPrivacyPolicy(columnFieldNames) {
*/ const complianceEntities = get(this, policyComplianceEntitiesKey) || [];
const getAttributesOnField = (attributes = [], fieldName) => const getKeysOnField = (keys = [], fieldName, source = []) => {
attributes.map(attr => getAttributeOnField(attr, fieldName)); const sourceField = source.find(({ identifierField }) => identifierField === fieldName) || {};
let ret = {};
// Set default or if already in policy, retrieve current values from for (const [key, value] of Object.entries(sourceField)) {
// privacyCompliancePolicy.complianceEntities if (keys.includes(key)) {
return getWithDefault(this, 'truncatedSchemaFields', []).map(({ fieldName: identifierField, dataType }) => { ret = { ...ret, [key]: value };
const [identifierType, isSubject, logicalType] = getAttributesOnField( }
['identifierType', 'isSubject', 'logicalType'], }
identifierField return ret;
); };
/**
* Flag indicating that the field has an identifierType matching a generic type
* @type {Boolean}
*/
const isMixed = identifierType === fieldIdentifierTypes.generic.value;
/**
* Flag indicating that the field has an identifierType matching a custom id type
* @type {Boolean}
*/
const isCustom = identifierType === fieldIdentifierTypes.custom.value;
// 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 urnFieldFormat = idLogicalTypes.findBy('value', 'URN');
// 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');
/**
* If field is a mixed identifier, avail only the urnFieldFormat, otherwise use the prev determined fieldFormats
* @type {any|Object}
*/
fieldFormats = isMixed ? urnFieldFormat : fieldFormats;
fieldFormats = isCustom ? void 0 : fieldFormats;
/**
* An object referencing the fieldFormat for this field
* @type {any|Object}
*/
const logicalTypeObject = Array.isArray(fieldFormats)
? fieldFormats.findBy('value', logicalType)
: fieldFormats;
return { return columnFieldNames.reduce((acc, identifierField) => {
dataType, const currentPrivacyAttrs = getKeysOnField(
identifierField, ['identifierType', 'isSubject', 'logicalType'],
fieldFormats, identifierField,
// Boolean flag indicating that the list of field formats is unchanging complianceEntities
isFieldFormatDisabled: isMixed || isCustom, );
identifierType: computedIdentifierType,
// Check specific use case for urn only field format / logicalType return { ...acc, ...{ [identifierField]: currentPrivacyAttrs } };
classification: }, {});
fieldClassification[identifierField] || },
(isMixed && defaultFieldDataTypeClassification[urnFieldFormat.value]),
// Same object reference for equality comparision /**
logicalType: logicalTypeObject * Computed prop over the current Id fields in the Privacy Policy
}; * @type {Ember.computed}
}); */
columnIdFieldsToCurrentPrivacyPolicy: computed(
'truncatedColumnFields',
`${policyComplianceEntitiesKey}.[]`,
function() {
const columnFieldNames = get(this, 'truncatedColumnFields').map(({ fieldName }) => fieldName);
return this.mapColumnIdFieldsToCurrentPrivacyPolicy(columnFieldNames);
} }
), ),
/**
* Caches a reference to the generated list of merged data between the column api and the current compliance entities list
* @type {Ember.computed}
*/
mergedComplianceEntitiesAndColumnFields: computed('columnIdFieldsToCurrentPrivacyPolicy', function() {
// truncatedColumnFields is a dependency for cp columnIdFieldsToCurrentPrivacyPolicy, so no need to dep on that directly
return this.mergeComplianceEntitiesAndColumnFields(
get(this, 'columnIdFieldsToCurrentPrivacyPolicy'),
get(this, 'truncatedColumnFields')
);
}),
/** /**
* Checks that each entity in sourceEntities has a generic * Checks that each entity in sourceEntities has a generic
* @param {Array} sourceEntities = [] the source entities to be matched against * @param {Array} sourceEntities = [] the source entities to be matched against
@ -453,38 +497,49 @@ export default Component.extend({
// Current list of compliance entities on policy // Current list of compliance entities on policy
const complianceEntities = get(this, policyComplianceEntitiesKey); const complianceEntities = get(this, policyComplianceEntitiesKey);
// All candidate fields that can be on policy, excluding tracking type fields // All candidate fields that can be on policy, excluding tracking type fields
const datasetFields = get(this, 'complianceDataFields'); const datasetFields = get(
const fieldsCurrentlyInComplianceList = complianceEntities.mapBy('identifierField'); this,
'mergedComplianceEntitiesAndColumnFields'
).map(({ identifierField, identifierType, isSubject, logicalType }) => ({
identifierField,
identifierType,
isSubject,
logicalType
}));
// Fields that do not have a logicalType, and no identifierType or identifierType is `fieldIdentifierTypes.none` // Fields that do not have a logicalType, and no identifierType or identifierType is `fieldIdentifierTypes.none`
const unformattedFields = datasetFields.filter( const { formatted, unformatted } = datasetFields.reduce(
({ identifierType, logicalType }) => ({ formatted, unformatted }, field) => {
!logicalType && (fieldIdentifierTypes.none.value === identifierType || !identifierType) const { identifierType, logicalType } = getProperties(field, ['identifierType', 'logicalType']);
if (!logicalType && (fieldIdentifierTypes.none.value === identifierType || !identifierType)) {
unformatted = [...unformatted, field];
} else {
formatted = [...formatted, field];
}
return { formatted, unformatted };
},
{ formatted: [], unformatted: [] }
); );
let isConfirmed = true; let isConfirmed = true;
let unformattedComplianceEntities = [];
// If there are unformatted fields, require confirmation from user // If there are unformatted fields, require confirmation from user
if (!isEmpty(unformattedFields)) { if (unformatted.length) {
// Ensure that the unformatted fields to be added to the entities are not already present unformattedComplianceEntities = unformatted.map(({ identifierField }) => ({
// in the previous compliance entities list identifierField,
const unformattedComplianceEntities = unformattedFields identifierType: fieldIdentifierTypes.none.value,
.filter(({ identifierField }) => !fieldsCurrentlyInComplianceList.includes(identifierField)) isSubject: null,
.map(({ identifierField }) => ({ logicalType: void 0
identifierField, }));
identifierType: fieldIdentifierTypes.none.value,
isSubject: null,
logicalType: void 0
}));
isConfirmed = confirm( isConfirmed = confirm(
`There are ${unformattedFields.length} non-ID fields that have no field format specified. ` + `There are ${unformatted.length} non-ID fields that have no field format specified. ` +
`Are you sure they don't contain any of the following PII?\n\n` + `Are you sure they don't contain any of the following PII?\n\n` +
`Name, Email, Phone, Address, Location, IP Address, Payment Info, Password, National ID, Device ID etc.` `Name, Email, Phone, Address, Location, IP Address, Payment Info, Password, National ID, Device ID etc.`
); );
// If the user confirms that this is ok, apply the unformatted fields on the current compliance list
// to be saved
isConfirmed && complianceEntities.setObjects([...complianceEntities, ...unformattedComplianceEntities]);
} }
// If the user confirms that this is ok, apply the unformatted fields on the current compliance list
// to be saved
isConfirmed && complianceEntities.setObjects([...formatted, ...unformattedComplianceEntities]);
return isConfirmed; return isConfirmed;
}, },
@ -585,35 +640,20 @@ export default Component.extend({
* @param {String} identifierType * @param {String} identifierType
*/ */
onFieldIdentifierTypeChange({ identifierField, logicalType }, { value: identifierType }) { onFieldIdentifierTypeChange({ identifierField, logicalType }, { value: identifierType }) {
const complianceList = get(this, policyComplianceEntitiesKey); const currentComplianceEntities = get(this, 'mergedComplianceEntitiesAndColumnFields');
// A reference to the current field in the compliance list if it exists // A reference to the current field in the compliance list if it exists
const currentFieldInComplianceList = complianceList.findBy('identifierField', identifierField); const currentFieldInComplianceList = currentComplianceEntities.findBy('identifierField', identifierField);
const subjectIdString = fieldIdentifierTypes.subjectMember.value; const subjectIdString = fieldIdentifierTypes.subjectMember.value;
// Some rendered identifierTypes may be masks of other underlying types, e.g. subjectId Member type is really // Some rendered identifierTypes may be masks of other underlying types, e.g. subjectId Member type is really
// a memberId with an attribute of isSubject in the affirmative // a memberId with an attribute of isSubject in the affirmative
const unwrappedIdentifierType = subjectIdString === identifierType const unwrappedIdentifierType = subjectIdString === identifierType
? fieldIdentifierTypes.member.value ? fieldIdentifierTypes.member.value
: identifierType; : identifierType;
const updatedEntity = Object.assign({}, currentFieldInComplianceList, { setProperties(currentFieldInComplianceList, {
identifierField,
identifierType: unwrappedIdentifierType, identifierType: unwrappedIdentifierType,
isSubject: subjectIdString === identifierType ? true : null, isSubject: subjectIdString === identifierType ? true : null,
// If the field is currently not in the complianceList, logicalType: void 0
// we will set the logicalType to be the provided value, otherwise, set to undefined
// since the next step removes it from the updated list
logicalType: !currentFieldInComplianceList ? logicalType : void 0
}); });
let transientComplianceList = complianceList;
// If the identifierField is in the current compliance list,
// create a filtered list excluding the identifierField before updating the list
if (currentFieldInComplianceList) {
transientComplianceList = complianceList.filter(
({ identifierField: fieldName }) => fieldName !== identifierField
);
}
complianceList.setObjects([updatedEntity, ...transientComplianceList]);
// Set the defaultClassification for the identifierField, // Set the defaultClassification for the identifierField,
// although the classification is based on the logicalType, // although the classification is based on the logicalType,
// an identifierField may only have one valid logicalType for it's given identifierType // an identifierField may only have one valid logicalType for it's given identifierType
@ -627,23 +667,18 @@ export default Component.extend({
* @param {Event} e the DOM change event * @param {Event} e the DOM change event
* @return {*} * @return {*}
*/ */
onFieldLogicalTypeChange(field, e) { onFieldLogicalTypeChange(field, { value: logicalType } = {}) {
const { identifierField } = field; const { identifierField } = field;
const { value: logicalType } = e || {};
let sourceIdentifierField = get(this, policyComplianceEntitiesKey).findBy('identifierField', identifierField);
// If the identifierField does not current exist, invoke onFieldIdentifierChange to add it on the compliance list // If the identifierField does not current exist, invoke onFieldIdentifierChange to add it on the compliance list
if (!sourceIdentifierField) { if (!field) {
this.actions.onFieldIdentifierTypeChange.call( this.actions.onFieldIdentifierTypeChange.call(
this, this,
{ { identifierField, logicalType },
identifierField,
logicalType
},
{ value: fieldIdentifierTypes.none.value } { value: fieldIdentifierTypes.none.value }
); );
} else { } else {
set(sourceIdentifierField, 'logicalType', logicalType); set(field, 'logicalType', logicalType);
} }
return this.setDefaultClassification({ identifierField }, { value: logicalType }); return this.setDefaultClassification({ identifierField }, { value: logicalType });
@ -656,6 +691,10 @@ export default Component.extend({
* @return {*} * @return {*}
*/ */
onFieldClassificationChange({ identifierField }, { value: classification = null }) { onFieldClassificationChange({ identifierField }, { value: classification = null }) {
const currentFieldInComplianceList = get(this, 'mergedComplianceEntitiesAndColumnFields').findBy(
'identifierField',
identifierField
);
let fieldClassification = get(this, policyFieldClassificationKey); let fieldClassification = get(this, policyFieldClassificationKey);
let updatedFieldClassification = {}; let updatedFieldClassification = {};
// For datasets initially without a fieldClassification, the default value is null // For datasets initially without a fieldClassification, the default value is null
@ -675,6 +714,8 @@ export default Component.extend({
updatedFieldClassification = Object.assign({}, fieldClassification, { [identifierField]: classification }); updatedFieldClassification = Object.assign({}, fieldClassification, { [identifierField]: classification });
} }
// Apply the updated classification value to the current instance of the field in working copy
set(currentFieldInComplianceList, 'classification', classification);
set(this, policyFieldClassificationKey, updatedFieldClassification); set(this, policyFieldClassificationKey, updatedFieldClassification);
}, },

View File

@ -33,24 +33,13 @@ 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) {
let source = ''; let source = '';
var id = 0; let id = 0;
var urn = ''; let urn = '';
/** /**
* Series of chain-able functions invoked to set set properties on the controller required for dataset tab sections * Series of chain-able functions invoked to set set properties on the controller required for dataset tab sections
* @type {{privacyCompliancePolicy: ((id)), securitySpecification: ((id)), datasetSchemaFieldNamesAndTypes: ((id))}}
*/ */
const fetchThenSetOnController = { const fetchThenSetOnController = {
async complianceInfo(controller, { id }) {
const response = await Promise.resolve(getJSON(getDatasetPrivacyUrl(id)));
const { msg, status, complianceInfo = createInitialComplianceInfo(id) } = response;
const isNewComplianceInfo = status === 'failed' && String(msg).includes('actual 0');
setProperties(controller, { complianceInfo, isNewComplianceInfo });
return this;
},
datasetSchemaFieldNamesAndTypes(controller, { id }) { datasetSchemaFieldNamesAndTypes(controller, { id }) {
Promise.resolve(getJSON(datasetUrl(id))).then(({ dataset: { schema } = { schema: undefined } } = {}) => { Promise.resolve(getJSON(datasetUrl(id))).then(({ dataset: { schema } = { schema: undefined } } = {}) => {
/** /**
@ -132,6 +121,73 @@ export default Route.extend({
}) })
) )
.forEach(func => func()); .forEach(func => func());
/**
* Fetches the current compliance policy for the rendered dataset
* @param {number} id the id of the dataset
* @return {Promise.<{complianceInfo: *, isNewComplianceInfo: boolean}>}
*/
const getComplianceInfo = async id => {
const response = await Promise.resolve(getJSON(getDatasetPrivacyUrl(id)));
const { msg, status, complianceInfo = createInitialComplianceInfo(id) } = response;
const isNewComplianceInfo = status === 'failed' && String(msg).includes('actual 0');
return { complianceInfo, isNewComplianceInfo };
};
/**
* Fetch the datasetColumn
* @param {number} id the id of the dataset
*/
getDatasetColumn = id =>
Promise.resolve(getJSON(getDatasetColumnUrl(id)))
.then(({ status, columns }) => {
if (status === 'ok') {
if (columns && columns.length) {
const columnsWithHTMLComments = columns.map(column => {
const { comment } = column;
if (comment) {
// TODO: DSS-6122 Refactor global function reference
column.commentHtml = window.marked(comment).htmlSafe();
}
return column;
});
controller.set('hasSchemas', true);
controller.set('schemas', columnsWithHTMLComments);
// TODO: DSS-6122 Refactor direct method invocation on controller
controller.buildJsonView();
// TODO: DSS-6122 Refactor setTimeout,
// global function reference
setTimeout(window.initializeColumnTreeGrid, 500);
}
return columns;
}
return Promise.reject(new Error('Dataset columns request failed.'));
})
.then(columns => columns.map(({ dataType, fullFieldPath }) => ({ dataType, fieldName: fullFieldPath })))
.catch(() => setProperties(controller, { hasSchemas: false, schemas: null }));
/**
* async IIFE sets the the complianceInfo and schemaFieldNamesMappedToDataTypes
* at once so observers will be buffered
* @param {number} id the dataset id
* @return {Promise.<void>}
*/
(async id => {
const [columns, privacy] = await Promise.all([getDatasetColumn(id), getComplianceInfo(id)]);
const { complianceInfo, isNewComplianceInfo } = privacy;
setProperties(controller, {
complianceInfo,
isNewComplianceInfo,
schemaFieldNamesMappedToDataTypes: columns
});
})(id);
} }
Promise.resolve(getJSON(getDatasetInstanceUrl(id))) Promise.resolve(getJSON(getDatasetInstanceUrl(id)))
@ -207,48 +263,6 @@ export default Route.extend({
} }
}); });
getDatasetColumn = id =>
Promise.resolve(getJSON(getDatasetColumnUrl(id)))
.then(({ status, columns }) => {
if (status === 'ok') {
if (columns && columns.length) {
const columnsWithHTMLComments = columns.map(column => {
const { comment } = column;
if (comment) {
// TODO: DSS-6122 Refactor global function reference
column.commentHtml = window.marked(comment).htmlSafe();
}
return column;
});
controller.set('hasSchemas', true);
controller.set('schemas', columnsWithHTMLComments);
// TODO: DSS-6122 Refactor direct method invocation on controller
controller.buildJsonView();
// TODO: DSS-6122 Refactor setTimeout,
// global function reference
setTimeout(window.initializeColumnTreeGrid, 500);
}
return columns;
}
return Promise.reject(new Error('Dataset columns request failed.'));
})
.then(columns => columns.map(({ dataType, fullFieldPath }) => ({ dataType, fieldName: fullFieldPath })))
.then(set.bind(Ember, controller, 'schemaFieldNamesMappedToDataTypes'))
.catch(() =>
setProperties(controller, {
hasSchemas: false,
schemas: null
})
);
getDatasetColumn(id);
if (source.toLowerCase() !== 'pinot') { if (source.toLowerCase() !== 'pinot') {
Promise.resolve(getJSON(getDatasetPropertiesUrl(id))) Promise.resolve(getJSON(getDatasetPropertiesUrl(id)))
.then(({ status, properties }) => { .then(({ status, properties }) => {

View File

@ -1,10 +1,6 @@
import Ember from 'ember'; import Ember from 'ember';
const { const { Route, get, inject: { service } } = Ember;
Route,
get,
inject: { service }
} = Ember;
export default Route.extend({ export default Route.extend({
session: service(), session: service(),
@ -17,5 +13,16 @@ export default Route.extend({
if (get(this, 'session.isAuthenticated')) { if (get(this, 'session.isAuthenticated')) {
this.transitionTo('index'); this.transitionTo('index');
} }
},
/**
* Overrides the default method with a custom op
* renders the default template into the login outlet
* @override
*/
renderTemplate() {
this.render({
outlet: 'login'
});
} }
}); });

View File

@ -3,7 +3,7 @@
*/ */
body { body {
color: $text-color; color: $text-color;
font: normal 100 150% / 1.4 $text-font-stack; font: normal 300 150% / 1.4 $text-font-stack;
} }
h1, h2, h3, h4, h5, h6 { h1, h2, h3, h4, h5, h6 {

View File

@ -16,3 +16,10 @@
.dataset-field-value { .dataset-field-value {
visibility: hidden; visibility: hidden;
} }
/// Visual state for compliance elements
.compliance-row {
&--off-screen {
visibility: hidden;
}
}

View File

@ -12,5 +12,5 @@
{{partial "main"}} {{partial "main"}}
</section> </section>
{{else}} {{else}}
{{outlet}} {{outlet "login"}}
{{/if}} {{/if}}

View File

@ -0,0 +1,13 @@
{{yield (hash
cell=(component 'dataset-table-cell')
isFieldFormatDisabled=isFieldFormatDisabled
identifierType=identifierType
identifierField=identifierField
dataType=dataType
fieldFormats=fieldFormats
logicalType=logicalType
classification=classification
onFieldIdentifierTypeChange=(action 'onFieldIdentifierTypeChange')
onFieldClassificationChange=(action 'onFieldClassificationChange')
onFieldLogicalTypeChange=(action 'onFieldLogicalTypeChange')
)}}

View File

@ -166,54 +166,56 @@
{{/if}} {{/if}}
</section> </section>
{{#dataset-table {{#if mergedComplianceEntitiesAndColumnFields.length}}
class="nacho-table--stripped" {{#dataset-table
fields=complianceDataFields class="nacho-table--stripped dataset-compliance-fields"
sortColumnWithName=sortColumnWithName fields=mergedComplianceEntitiesAndColumnFields
filterBy=filterBy sortColumnWithName=sortColumnWithName
sortDirection=sortDirection filterBy=filterBy
searchTerm=searchTerm as |table| sortDirection=sortDirection
}} tableRowComponent='dataset-compliance-row'
<caption> searchTerm=searchTerm as |table|
<input }}
type="text" <caption>
title="Search fields" <input
placeholder="Search fields" type="text"
value="{{table.searchTerm}}" title="Search fields"
oninput={{action table.filterDidChange value="target.value"}}> placeholder="Search fields"
</caption> value="{{table.searchTerm}}"
{{#table.head as |head|}} oninput={{action table.filterDidChange value="target.value"}}>
{{#head.column columnName="identifierField"}}Field{{/head.column}} </caption>
{{#head.column columnName="dataType"}}Data Type{{/head.column}} {{#table.head as |head|}}
{{#head.column class="nacho-table-cell-wrapped"}} {{#head.column columnName="identifierField"}}Field{{/head.column}}
Member, Organization, or Group, ID's {{#head.column columnName="dataType"}}Data Type{{/head.column}}
<a {{#head.column class="nacho-table-cell-wrapped"}}
target="_blank" Member, Organization, or Group, ID's
href="http://go/metadata_acquisition#ProjectOverview-compliance"> <a
<sup> target="_blank"
More Info href="http://go/metadata_acquisition#ProjectOverview-compliance">
<sup>
More Info
<span class="glyphicon glyphicon-question-sign" <span class="glyphicon glyphicon-question-sign"
title="More information on various IDs"></span> title="More information on various IDs"></span>
</sup> </sup>
</a> </a>
{{/head.column}} {{/head.column}}
{{#head.column}} {{#head.column}}
Field Format? Field Format?
<a <a
target="_blank" target="_blank"
href="http://go/gdpr-taxonomy#MetadataTaxonomyforDataSets-DatasetLevelTags"> href="http://go/gdpr-taxonomy#MetadataTaxonomyforDataSets-DatasetLevelTags">
<sup> <sup>
More Info More Info
<span class="glyphicon glyphicon-question-sign" <span class="glyphicon glyphicon-question-sign"
title="More information on Field Format"></span> title="More information on Field Format"></span>
</sup> </sup>
</a> </a>
{{/head.column}} {{/head.column}}
{{#head.column}} {{#head.column}}
Security Classification Security Classification
<sup> <sup>
<span <span
class="glyphicon glyphicon-question-sign" class="glyphicon glyphicon-question-sign"
title={{helpText.classification}}> title={{helpText.classification}}>
@ -223,51 +225,62 @@
text=helpText.classification text=helpText.classification
}} }}
</span> </span>
</sup> </sup>
{{/head.column}} {{/head.column}}
{{/table.head}} {{/table.head}}
{{#table.body as |body|}} {{#table.body as |body|}}
{{#each (sort-by table.sortBy table.data) as |field|}} {{#each (sort-by table.sortBy table.data) as |field|}}
{{#body.row as |row|}} {{#body.row
{{#row.cell}} field=field
{{field.identifierField}} onFieldLogicalTypeChange=(action 'onFieldLogicalTypeChange')
{{/row.cell}} onFieldClassificationChange=(action 'onFieldClassificationChange')
{{#row.cell}} onFieldIdentifierTypeChange=(action 'onFieldIdentifierTypeChange')as |row|}}
{{field.dataType}}
{{/row.cell}} {{#row.cell}}
{{#row.cell}} {{row.identifierField}}
{{ember-selector {{/row.cell}}
disabled=(not isEditing)
values=fieldIdentifierOptions {{#row.cell}}
selected=field.identifierType {{row.dataType}}
selectionDidChange=(action "onFieldIdentifierTypeChange" field) {{/row.cell}}
}}
{{/row.cell}} {{#row.cell}}
{{#row.cell}} {{ember-selector
{{#power-select disabled=(not isEditing)
options=field.fieldFormats values=fieldIdentifierOptions
selected=field.logicalType selected=(readonly row.identifierType)
disabled=(or field.isFieldFormatDisabled (not isEditing)) selectionDidChange=(action row.onFieldIdentifierTypeChange)
placeholder="Select Format" }}
allowClear=true {{/row.cell}}
searchField="label"
triggerClass="ember-power-select-trigger-search" {{#row.cell}}
onchange=(action "onFieldLogicalTypeChange" field) as |fieldFormat|}} {{#power-select
{{fieldFormat.label}} options=row.fieldFormats
{{/power-select}} selected=row.logicalType
{{/row.cell}} disabled=(or row.isFieldFormatDisabled (not isEditing))
{{#row.cell}} placeholder="Select Format"
{{ember-selector allowClear=true
class="nacho-select--hidden-state" searchField="label"
values=classifiers triggerClass="ember-power-select-trigger-search"
selected=field.classification onchange=(action row.onFieldLogicalTypeChange) as |fieldFormat|}}
disabled=(or (not isEditing) (not field.logicalType)) {{fieldFormat.label}}
selectionDidChange=(action "onFieldClassificationChange" field) {{/power-select}}
}} {{/row.cell}}
{{/row.cell}}
{{/body.row}} {{#row.cell}}
{{/each}} {{ember-selector
{{/table.body}} class="nacho-select--hidden-state"
{{/dataset-table}} values=classifiers
selected=row.classification
disabled=(or (not isEditing) (not row.logicalType))
selectionDidChange=(action row.onFieldClassificationChange)
}}
{{/row.cell}}
{{/body.row}}
{{/each}}
{{/table.body}}
{{/dataset-table}}
{{/if}}
</div> </div>
{{yield}} {{yield}}

View File

@ -17,7 +17,6 @@
sortDirection=(readonly sortDirection) sortDirection=(readonly sortDirection)
sortDidChange=(action "sortDidChange")) sortDidChange=(action "sortDidChange"))
body=(component tableBodyComponent body=(component tableBodyComponent
tableRowComponent=tableRowComponent tableRowComponent=tableRowComponent)
)
foot=(component tableFooterComponent) foot=(component tableFooterComponent)
)}} )}}

View File

@ -1,424 +0,0 @@
<div id="dataset" >
<div class="well well-sm">
<div class="row">
<div class="col-xs-11">
<ul class="breadcrumbs">
{{#each breadcrumbs as |crumb|}}
<li>
<a title="{{crumb.title}}" href="#/datasets/{{crumb.urn}}">
{{crumb.title}}
</a>
</li>
{{/each}}
</ul>
</div>
</div>
</div>
<div class="row">
<div class="col-xs-5">
<h3>{{ model.name }}</h3>
</div>
<div class="col-xs-7 text-right">
<ul class="datasetDetailsLinks">
<li>
{{#dataset-favorite dataset=model action="didFavorite"}}
{{/dataset-favorite}}
<span class="hidden-sm hidden-xs">
{{#if model.isFavorite}}
Unfavorite
{{else}}
Favorite
{{/if}}
</span>
</li>
{{#if model.hdfsBrowserLink}}
<li>
<a target="_blank" href={{model.hdfsBrowserLink}}
title="View on HDFS">
<i class="fa fa-database"></i>
<span class="hidden-sm hidden-xs">
HDFS
</span>
</a>
</li>
{{/if}}
<li>
<a target="_blank" href={{lineageUrl}}
title="View Lineage">
<i class="fa fa-sitemap"></i>
<span class="hidden-sm hidden-xs">
Lineage
</span>
</a>
</li>
{{#if model.hasSchemaHistory}}
<li>
<a target="_blank" href={{schemaHistoryUrl}}
title="View Schema History">
<i class="fa fa-history"></i>
<span class="hidden-sm hidden-xs">
Schema History
</span>
</a>
</li>
{{/if}}
<li>
{{#dataset-watch dataset=model getDatasets="getDataset"}}
{{/dataset-watch}}
<span class="hidden-xs hidden-sm">
{{#if model.isWatched}}
Unwatch
{{else}}
Watch
{{/if}}
</span>
</li>
</ul>
</div>
</div>
{{dataset-owner-list owners=owners datasetName=model.name}}
{{#if hasinstances}}
<div class="row">
<span class="col-xs-1">Instances:</span>
<div class="btn-toolbar col-xs-11" role="toolbar">
{{#each instances as |instance index|}}
<div class="btn-group" role="group">
{{#if index}}
<button type="button" data-value="{{ instance.dbCode }}" class="btn btn-default instance-btn" {{action "updateInstance" instance}}>
{{ instance.dbCode }}
</button>
{{else}}
<button type="button" data-value="{{ instance.dbCode }}" class="btn btn-primary instance-btn" {{action "updateInstance" instance}}>
{{ instance.dbCode }}
</button>
{{/if}}
</div>
{{/each}}
</div>
</div>
{{/if}}
{{#if hasversions}}
<div class="row">
<span class="col-xs-1">Versions:</span>
<div class="btn-toolbar col-xs-11" role="toolbar">
{{#each versions as |version index|}}
<div class="btn-group" role="group">
{{#if index}}
<button type="button" data-value="{{ version }}" class="btn btn-default version-btn" {{action "updateVersion" version}}>
{{ version }}
</button>
{{else}}
<button type="button" data-value="{{ version }}" class="btn btn-primary version-btn" {{action "updateVersion" version}}>
{{ version }}
</button>
{{/if}}
</div>
{{/each}}
</div>
</div>
{{/if}}
</div>
{{#if tabview}}
<ul class="nav nav-tabs nav-justified">
{{#unless isPinot}}
<li id="properties">
<a data-toggle="tab" href="#propertiestab">
Properties
</a>
</li>
{{/unless}}
<li id="properties">
<a data-toggle="tab" href="#commentstab">
Comments
</a>
</li>
<li id="schemas" class="active"><a data-toggle="tab" href="#schematab">Schema</a></li>
<li id="ownership"><a data-toggle="tab" href="#ownertab">Ownership</a></li>
{{#unless isSFDC}}
<li id="samples"><a data-toggle="tab" href="#sampletab">Sample Data</a></li>
{{/unless}}
<li id="impacts">
<a data-toggle="tab"
title="Down Stream Impact Analysis"
href="#impacttab">
Downstream
</a>
</li>
<li id="depends">
<a data-toggle="tab"
title="Relations"
href="#dependtab">
Relations
</a>
</li>
<li id="access">
<a data-toggle="tab"
title="Accessibilities"
href="#accesstab">
Availability
</a>
</li>
<li id="compliance">
<a data-toggle="tab"
title="Compliance"
href="#compliancetab">
Compliance
</a>
</li>
<li id="confidential">
<a data-toggle="tab"
title="Confidential"
href="#confidentialtab">
Confidential
</a>
</li>
</ul>
<div class="tab-content">
{{#unless isPinot}}
<div id="propertiestab" class="tab-pane">
{{#dataset-property hasProperty=hasProperty properties=properties}}
{{/dataset-property}}
</div>
{{/unless}}
<div id="commentstab" class="tab-pane">
{{#dataset-comments dataset=this}}
{{/dataset-comments}}
</div>
<div id="schematab" class="tab-pane active">
{{#dataset-schema hasSchemas=hasSchemas isTable=isTable isJSON=isJSON schemas=schemas dataset=model}}
{{/dataset-schema}}
</div>
<div id="ownertab" class="tab-pane">
{{dataset-author owners=owners
ownerTypes=ownerTypes
showMsg=showMsg
alertType=alertType
ownerMessage=ownerMessage
parentController=this}}
</div>
{{#unless isSFDC}}
<div id="sampletab" class="tab-pane">
{{#dataset-sample hasSamples=hasSamples isPinot=isPinot columns=columns samples=samples}}
{{/dataset-sample}}
</div>
{{/unless}}
<div id="impacttab" class="tab-pane">
{{#dataset-impact hasImpacts=hasImpacts impacts=impacts}}
{{/dataset-impact}}
</div>
<div id="dependtab" class="tab-pane">
{{#dataset-relations hasDepends=hasDepends depends=depends hasReferences=hasReferences references=references}}
{{/dataset-relations}}
</div>
<div id="accesstab" class="tab-pane">
{{#dataset-access hasAccess=hasAccess accessibilities=accessibilities}}
{{/dataset-access}}
</div>
<div id="compliancetab" class="tab-pane">
{{dataset-compliance privacyCompliancePolicy=privacyCompliancePolicy
isNewPrivacyCompliancePolicy=isNewPrivacyCompliancePolicy
datasetSchemaFieldsAndTypes=datasetSchemaFieldsAndTypes
onSave=(action "savePrivacyCompliancePolicy")
onReset=(action "resetPrivacyCompliancePolicy")}}
</div>
<div id="confidentialtab" class="tab-pane">
{{dataset-confidential securitySpecification=securitySpecification
isNewSecuritySpecification=isNewSecuritySpecification
datasetSchemaFieldsAndTypes=datasetSchemaFieldsAndTypes
onSave=(action "saveSecuritySpecification")
onReset=(action "resetSecuritySpecification")}}
</div>
</div>
{{else}}
<div class="panel-group" id="accordion" role="tablist" aria-multiselectable="true">
{{#unless isPinot}}
<div class="panel panel-default">
<div class="panel-heading" role="tab" id="propertiesHeading">
<h4 class="panel-title">
<a data-toggle="collapse" data-parent="#accordion"
href="#properties" aria-expanded="true" aria-controls="properties">
Properties
</a>
</h4>
</div>
<div id="properties" class="panel-collapse collapse" role="tabpanel" aria-labelledby="propertiesHeading">
<div class="panel-body">
<div class="row">
{{#dataset-property hasProperty=hasProperty properties=properties}}
{{/dataset-property}}
</div>
</div>
</div>
</div>
{{/unless}}
<div class="panel panel-default">
<div class="panel-heading" role="tab" id="commentsHeading">
<h4 class="panel-title">
<a data-toggle="collapse" data-parent="#accordion"
href="#comments" aria-expanded="true" aria-controls="comments">
Comments
</a>
</h4>
</div>
<div id="comments" class="panel-collapse collapse" role="tabpanel" aria-labelledby="commentsHeading">
<div class="panel-body">
{{#dataset-comments dataset=this}}
{{/dataset-comments}}
</div>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading" role="tab" id="schemaHeading">
<h4 class="panel-title">
<a class="collapsed" data-toggle="collapse" data-parent="#accordion"
href="#schema" aria-expanded="false" aria-controls="schema">
Schema
</a>
</h4>
</div>
<div id="schema" class="panel-collapse collapse in" role="tabpanel" aria-labelledby="schemaHeading">
<div class="panel-body">
{{#dataset-schema hasSchemas=hasSchemas isTable=isTable isJSON=isJSON schemas=schemas dataset=model}}
{{/dataset-schema}}
</div>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading" role="tab" id="ownershipHeading">
<h4 class="panel-title">
<a class="collapsed" data-toggle="collapse" data-parent="#accordion"
href="#ownership" aria-expanded="false" aria-controls="ownership">
Ownership
</a>
</h4>
</div>
<div id="ownership" class="panel-collapse collapse" role="tabpanel" aria-labelledby="ownershipHeading">
<div class="panel-body">
{{dataset-author owners=owners
ownerTypes=ownerTypes
showMsg=showMsg
alertType=alertType
ownerMessage=ownerMessage
parentController=this}}
</div>
</div>
</div>
{{#unless isSFDC}}
<div class="panel panel-default">
<div class="panel-heading" role="tab" id="sampleHeading">
<h4 class="panel-title">
<a class="collapsed" data-toggle="collapse" data-parent="#accordion"
href="#sampleData" aria-expanded="false" aria-controls="sampleData">
Sample Data
</a>
</h4>
</div>
<div id="sampleData" class="panel-collapse collapse" role="tabpanel" aria-labelledby="sampleHeading">
<div class="panel-body">
{{#dataset-sample hasSamples=hasSamples isPinot=isPinot columns=columns samples=samples}}
{{/dataset-sample}}
</div>
</div>
</div>
{{/unless}}
<div class="panel panel-default">
<div class="panel-heading" role="tab" id="impactsHeading">
<h4 class="panel-title">
<a class="collapsed" data-toggle="collapse" data-parent="#accordion"
href="#impactAnalysis" title="Down Stream Impact Analysis"
aria-expanded="false" aria-controls="sampleData">
Downstream
</a>
</h4>
</div>
<div id="impactAnalysis" class="panel-collapse collapse" role="tabpanel" aria-labelledby="impactsHeading">
<div class="panel-body">
{{#dataset-impact hasImpacts=hasImpacts impacts=impacts}}
{{/dataset-impact}}
</div>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading" role="tab" id="dependsHeading">
<h4 class="panel-title">
<a class="collapsed" data-toggle="collapse" data-parent="#accordion"
href="#dependsview" aria-expanded="false" aria-controls="sampleData">
Relations
</a>
</h4>
</div>
<div id="dependsview" class="panel-collapse collapse" role="tabpanel" aria-labelledby="impactsHeading">
<div class="panel-body">
{{#dataset-relations hasDepends=hasDepends depends=depends hasReferences=hasReferences references=references}}
{{/dataset-relations}}
</div>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading" role="tab" id="accessHeading">
<h4 class="panel-title">
<a class="collapsed" data-toggle="collapse" data-parent="#accordion"
href="#accessview" aria-expanded="false" aria-controls="accessData">
Availability
</a>
</h4>
</div>
<div id="accessview" class="panel-collapse collapse" role="tabpanel" aria-labelledby="accessHeading">
<div class="panel-body">
{{#dataset-access hasAccess=hasAccess accessibilities=accessibilities}}
{{/dataset-access}}
</div>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading" role="tab" id="complianceHeading">
<h4 class="panel-title">
<a class="collapsed" data-toggle="collapse" data-parent="#accordion"
href="#accessview" aria-expanded="false" aria-controls="accessData">
Compliance
</a>
</h4>
</div>
<div id="accessview" class="panel-collapse collapse" role="tabpanel" aria-labelledby="complianceHeading">
<div class="panel-body">
{{dataset-compliance privacyCompliancePolicy=privacyCompliancePolicy
isNewPrivacyCompliancePolicy=isNewPrivacyCompliancePolicy
datasetSchemaFieldsAndTypes=datasetSchemaFieldsAndTypes
onSave=(action "savePrivacyCompliancePolicy")
onReset=(action "resetPrivacyCompliancePolicy")}}
</div>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading" role="tab" id="confidentialHeading">
<h4 class="panel-title">
<a class="collapsed" data-toggle="collapse" data-parent="#accordion"
href="#accessview" aria-expanded="false" aria-controls="accessData">
Confidential
</a>
</h4>
</div>
<div id="accessview" class="panel-collapse collapse" role="tabpanel"
aria-labelledby="confidentialHeading">
<div class="panel-body">
{{dataset-confidential securitySpecification=securitySpecification
isNewSecuritySpecification=isNewSecuritySpecification
datasetSchemaFieldsAndTypes=datasetSchemaFieldsAndTypes
onSave=(action "saveSecuritySpecification")
onReset=(action "resetSecuritySpecification")}}
</div>
</div>
</div>
</div>
{{/if}}

View File

@ -1,117 +1,41 @@
<div id="pagedDatasets"> <div class="row">
<div class="row"> {{#unless detailview}}
<div class="col-xs-12"> <div class="search-pagination">
{{#unless detailview}} <ul class="pager">
<div class="well well-sm"> {{#unless first}}
<div class="row"> <li class="previous">
<div class="col-xs-11"> {{#if urn}}
<ul class="breadcrumbs"> {{#link-to 'datasets.name.subpage' currentName previousPage
{{#each breadcrumbs as |crumb|}} (query-params urn=urn)}}
<li> &larr; Prev
{{#link-to 'datasets.page' crumb.urn title=crumb.title}} {{/link-to}}
{{crumb.title}} {{else}}
{{/link-to}} {{#link-to 'datasets.page' previousPage}}
</li> &larr; Prev
{{/each}} {{/link-to}}
</ul> {{/if}}
</div> </li>
</div> {{/unless}}
</div> <li>
<div class="search-pagination"> {{ model.data.count }} datasets - page {{ model.data.page }}
<ul class="pager"> of {{ model.data.totalPages }}
{{#unless first}} </li>
<li class="previous"> {{#unless last}}
{{#if urn}} <li class="next">
{{#link-to 'datasets.name.subpage' currentName previousPage (query-params urn=urn)}} {{#if urn}}
&larr; Prev {{#link-to 'datasets.name.subpage' currentName nextPage
{{/link-to}} (query-params urn=urn)}}
{{else}} Next &rarr;
{{#link-to 'datasets.page' previousPage}} {{/link-to}}
&larr; Prev {{else}}
{{/link-to}} {{#link-to 'datasets.page' nextPage}}
{{/if}} Next &rarr;
</li> {{/link-to}}
{{/unless}} {{/if}}
<li> </li>
{{ model.data.count }} datasets - page {{ model.data.page }} of {{ model.data.totalPages }} {{/unless}}
</li> </ul>
{{#unless last}}
<li class="next">
{{#if urn}}
{{#link-to 'datasets.name.subpage' currentName nextPage (query-params urn=urn)}}
Next &rarr;
{{/link-to}}
{{else}}
{{#link-to 'datasets.page' nextPage}}
Next &rarr;
{{/link-to}}
{{/if}}
</li>
{{/unless}}
</ul>
</div>
{{/unless}}
{{#unless detailview}}
<table class="table table-bordered search-results" style="word-break: break-all;">
<tbody>
{{#each model.data.datasets as |dataset|}}
<tr>
<td>
<div class="row">
<div class="col-md-8">
<div class="col-xs-12">
{{#link-to 'datasets.dataset' dataset}}
{{ dataset.name }}
{{/link-to}}
</div>
{{#if dataset.owners}}
<div class="col-xs-12">
<span>owner:</span>
{{#each dataset.owners as |owner|}}
<p style="display:inline" title={{owner.name}}>{{ owner.userName }}</p>
{{/each}}
</div>
{{/if}}
{{#if dataset.formatedModified}}
<div class="col-xs-12">
<span>last modified:</span>
{{ dataset.formatedModified }}
</div>
{{/if}}
</div>
<div class="col-md-4 text-right">
<ul class="datasetTableLinks">
<li class="text-center no-link">
<span class="source">
{{ dataset.source }}
</span>
</li>
<li class="text-center" title="ownership">
{{dataset-owner dataset=dataset action="owned"}}
</li>
<li class="text-center" title="favorite">
{{dataset-favorite dataset=dataset action="didFavorite"}}
</li>
<li class="text-center" title="watch">
{{dataset-watch dataset=dataset getDatasets='getDatasets'}}
</li>
<li class="text-center">
<a href="/lineage/dataset/{{dataset.id}}" title="lineage for {{dataset.name}}">
<i class="fa fa-sitemap"></i>
</a>
</li>
</ul>
</div>
</div>
</td>
</tr>
{{/each}}
</tbody>
</table>
{{/unless}}
</div> </div>
<div class="col-xs-12"> {{/unless}}
{{outlet}} {{outlet}}
</div>
</div>
</div> </div>

View File

@ -16,7 +16,8 @@
"jsondiffpatch": "^0.2.4", "jsondiffpatch": "^0.2.4",
"marked": "^0.3.6", "marked": "^0.3.6",
"toastr": "^2.1.3", "toastr": "^2.1.3",
"x-editable": "^1.5.1" "x-editable": "^1.5.1",
"scrollMonitor": "^1.2.3"
}, },
"resolutions": { "resolutions": {
"ember-cli-shims": "0.1.3", "ember-cli-shims": "0.1.3",

View File

@ -17,7 +17,7 @@ module.exports = function(defaults) {
}, },
fingerprint: { fingerprint: {
enabled: true enabled: EmberApp.env() === 'production'
}, },
'ember-cli-bootstrap-sassy': { 'ember-cli-bootstrap-sassy': {
@ -25,6 +25,7 @@ module.exports = function(defaults) {
}, },
minifyJS: { minifyJS: {
enabled: EmberApp.env() === 'production',
options: { options: {
exclude: ['**/vendor.js', 'legacy-app/**'] exclude: ['**/vendor.js', 'legacy-app/**']
} }
@ -130,6 +131,8 @@ module.exports = function(defaults) {
app.import('bower_components/jsondiffpatch/public/build/jsondiffpatch.min.js'); app.import('bower_components/jsondiffpatch/public/build/jsondiffpatch.min.js');
app.import('bower_components/jsondiffpatch/public/build/jsondiffpatch-formatters.min.js'); app.import('bower_components/jsondiffpatch/public/build/jsondiffpatch-formatters.min.js');
app.import('bower_components/x-editable/dist/bootstrap3-editable/js/bootstrap-editable.min.js'); app.import('bower_components/x-editable/dist/bootstrap3-editable/js/bootstrap-editable.min.js');
app.import('bower_components/scrollMonitor/scrollMonitor.js');
app.import('vendor/shims/scrollmonitor.js');
return app.toTree(new MergeTrees([faFontTree, bsFontTree, treegridImgTree])); return app.toTree(new MergeTrees([faFontTree, bsFontTree, treegridImgTree]));
}; };

View File

@ -79,7 +79,7 @@
"lint-staged": { "lint-staged": {
"gitDir": "../", "gitDir": "../",
"linters": { "linters": {
"wherehows-web/app/**/*.js": [ "wherehows-web/{app,tests}/**/*.js": [
"prettier --print-width 120 --single-quote --write", "prettier --print-width 120 --single-quote --write",
"git add" "git add"
] ]

View File

@ -0,0 +1,24 @@
import { moduleForComponent, test } from 'ember-qunit';
import hbs from 'htmlbars-inline-precompile';
moduleForComponent('dataset-compliance-row', 'Integration | Component | dataset compliance row', {
integration: true
});
test('it renders', function(assert) {
// Set any properties with this.set('myProperty', 'value');
// Handle any actions with this.on('myAction', function(val) { ... });
this.render(hbs`{{dataset-compliance-row}}`);
assert.equal(this.$().text().trim(), '');
// Template block usage:
this.render(hbs`
{{#dataset-compliance-row}}
template block text
{{/dataset-compliance-row}}
`);
assert.equal(this.$().text().trim(), 'template block text');
});

View File

@ -0,0 +1,9 @@
(function() {
function vendorModule() {
'use strict';
return { 'default': self['scrollMonitor'] };
}
define('scrollmonitor', [], vendorModule);
})();