mirror of
https://github.com/datahub-project/datahub.git
synced 2025-12-10 17:46:02 +00:00
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:
commit
f25b378740
@ -2,7 +2,10 @@ module.exports = {
|
||||
"extends": "eslint:recommended",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 8,
|
||||
"sourceType": "module"
|
||||
"sourceType": "module",
|
||||
"ecmaFeatures": {
|
||||
"experimentalObjectRestSpread": true
|
||||
}
|
||||
},
|
||||
"env": {
|
||||
"browser": true,
|
||||
|
||||
@ -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
|
||||
}
|
||||
153
wherehows-web/app/components/dataset-compliance-row.js
Normal file
153
wherehows-web/app/components/dataset-compliance-row.js
Normal 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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -10,6 +10,7 @@ import {
|
||||
compliancePolicyStrings
|
||||
} from 'wherehows-web/constants';
|
||||
import { isPolicyExpectedShape } from 'wherehows-web/utils/datasets/functions';
|
||||
import scrollMonitor from 'scrollmonitor';
|
||||
|
||||
const {
|
||||
assert,
|
||||
@ -20,7 +21,6 @@ const {
|
||||
setProperties,
|
||||
getProperties,
|
||||
getWithDefault,
|
||||
isEmpty,
|
||||
String: { htmlSafe }
|
||||
} = 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
|
||||
* @type {any[]|Array.<String>}
|
||||
*/
|
||||
const fieldIdentifierTypeIds = Object.keys(fieldIdentifierTypes)
|
||||
export const fieldIdentifierTypeIds = Object.keys(fieldIdentifierTypes)
|
||||
.map(fieldIdentifierType => fieldIdentifierTypes[fieldIdentifierType])
|
||||
.filter(({ isId }) => isId)
|
||||
.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({
|
||||
sortColumnWithName: 'identifierField',
|
||||
filterBy: 'identifierField',
|
||||
@ -156,6 +162,75 @@ export default Component.extend({
|
||||
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
|
||||
@ -173,10 +248,10 @@ export default Component.extend({
|
||||
},
|
||||
|
||||
// Map logicalTypes to options consumable by DOM
|
||||
idLogicalTypes: cachedLogicalTypes('id'),
|
||||
idLogicalTypes: logicalTypesForIds,
|
||||
|
||||
// Map generic logical type to options consumable in DOM
|
||||
genericLogicalTypes: cachedLogicalTypes('generic'),
|
||||
genericLogicalTypes: logicalTypesForGeneric,
|
||||
|
||||
// Map classifiers to options better consumed in DOM
|
||||
classifiers: ['', ...classifiers.sort()].map(value => ({
|
||||
@ -196,17 +271,17 @@ export default Component.extend({
|
||||
* tracking header.
|
||||
* Used to indicate to viewer that these fields are hidden.
|
||||
*/
|
||||
containsHiddenTrackingFields: computed('truncatedSchemaFields.length', function() {
|
||||
// If their is a diff in complianceDataFields and truncatedSchemaFields,
|
||||
containsHiddenTrackingFields: computed('truncatedColumnFields.length', function() {
|
||||
// If their is a diff in schemaFieldNamesMappedToDataTypes and truncatedColumnFields,
|
||||
// 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`
|
||||
* tracking headers
|
||||
*/
|
||||
truncatedSchemaFields: computed('schemaFieldNamesMappedToDataTypes', function() {
|
||||
truncatedColumnFields: computed('schemaFieldNamesMappedToDataTypes', function() {
|
||||
return getWithDefault(this, 'schemaFieldNamesMappedToDataTypes', []).filter(
|
||||
({ fieldName }) => !isTrackingHeaderField(fieldName)
|
||||
);
|
||||
@ -270,107 +345,76 @@ export default Component.extend({
|
||||
* privacyCompliancePolicy.complianceEntities.
|
||||
* The returned list is a map of fields with current or default privacy properties
|
||||
*/
|
||||
complianceDataFields: computed(
|
||||
`${policyComplianceEntitiesKey}.@each.identifierType`,
|
||||
`${policyComplianceEntitiesKey}.[]`,
|
||||
policyFieldClassificationKey,
|
||||
'truncatedSchemaFields',
|
||||
function() {
|
||||
/**
|
||||
* 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 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;
|
||||
mergeComplianceEntitiesAndColumnFields(columnIdFieldsToCurrentPrivacyPolicy = {}, truncatedColumnFields = []) {
|
||||
return truncatedColumnFields.map(({ fieldName: identifierField, dataType }) => {
|
||||
const { [identifierField]: { identifierType, isSubject, logicalType } } = columnIdFieldsToCurrentPrivacyPolicy;
|
||||
const { [identifierField]: classification } = get(this, policyFieldClassificationKey) || {};
|
||||
return {
|
||||
identifierField,
|
||||
dataType,
|
||||
identifierType,
|
||||
isSubject,
|
||||
logicalType,
|
||||
classification
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Get value for a list of attributes
|
||||
* @param {Array} attributes list of attribute keys to pull from
|
||||
* sourceField
|
||||
* @param {String} fieldName name of the field to lookup
|
||||
* @return {Array} list of attribute values
|
||||
*/
|
||||
const getAttributesOnField = (attributes = [], fieldName) =>
|
||||
attributes.map(attr => getAttributeOnField(attr, fieldName));
|
||||
/**
|
||||
*
|
||||
* @param {Array} columnFieldNames
|
||||
* @return {*|{}|any}
|
||||
*/
|
||||
mapColumnIdFieldsToCurrentPrivacyPolicy(columnFieldNames) {
|
||||
const complianceEntities = get(this, policyComplianceEntitiesKey) || [];
|
||||
const getKeysOnField = (keys = [], fieldName, source = []) => {
|
||||
const sourceField = source.find(({ identifierField }) => identifierField === fieldName) || {};
|
||||
let ret = {};
|
||||
|
||||
// Set default or if already in policy, retrieve current values from
|
||||
// privacyCompliancePolicy.complianceEntities
|
||||
return getWithDefault(this, 'truncatedSchemaFields', []).map(({ fieldName: identifierField, dataType }) => {
|
||||
const [identifierType, isSubject, logicalType] = getAttributesOnField(
|
||||
['identifierType', 'isSubject', 'logicalType'],
|
||||
identifierField
|
||||
);
|
||||
/**
|
||||
* 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;
|
||||
for (const [key, value] of Object.entries(sourceField)) {
|
||||
if (keys.includes(key)) {
|
||||
ret = { ...ret, [key]: value };
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
|
||||
return {
|
||||
dataType,
|
||||
identifierField,
|
||||
fieldFormats,
|
||||
// Boolean flag indicating that the list of field formats is unchanging
|
||||
isFieldFormatDisabled: isMixed || isCustom,
|
||||
identifierType: computedIdentifierType,
|
||||
// Check specific use case for urn only field format / logicalType
|
||||
classification:
|
||||
fieldClassification[identifierField] ||
|
||||
(isMixed && defaultFieldDataTypeClassification[urnFieldFormat.value]),
|
||||
// Same object reference for equality comparision
|
||||
logicalType: logicalTypeObject
|
||||
};
|
||||
});
|
||||
return columnFieldNames.reduce((acc, identifierField) => {
|
||||
const currentPrivacyAttrs = getKeysOnField(
|
||||
['identifierType', 'isSubject', 'logicalType'],
|
||||
identifierField,
|
||||
complianceEntities
|
||||
);
|
||||
|
||||
return { ...acc, ...{ [identifierField]: currentPrivacyAttrs } };
|
||||
}, {});
|
||||
},
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @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
|
||||
const complianceEntities = get(this, policyComplianceEntitiesKey);
|
||||
// All candidate fields that can be on policy, excluding tracking type fields
|
||||
const datasetFields = get(this, 'complianceDataFields');
|
||||
const fieldsCurrentlyInComplianceList = complianceEntities.mapBy('identifierField');
|
||||
const datasetFields = get(
|
||||
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`
|
||||
const unformattedFields = datasetFields.filter(
|
||||
({ identifierType, logicalType }) =>
|
||||
!logicalType && (fieldIdentifierTypes.none.value === identifierType || !identifierType)
|
||||
const { formatted, unformatted } = datasetFields.reduce(
|
||||
({ formatted, unformatted }, field) => {
|
||||
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 unformattedComplianceEntities = [];
|
||||
|
||||
// If there are unformatted fields, require confirmation from user
|
||||
if (!isEmpty(unformattedFields)) {
|
||||
// Ensure that the unformatted fields to be added to the entities are not already present
|
||||
// in the previous compliance entities list
|
||||
const unformattedComplianceEntities = unformattedFields
|
||||
.filter(({ identifierField }) => !fieldsCurrentlyInComplianceList.includes(identifierField))
|
||||
.map(({ identifierField }) => ({
|
||||
identifierField,
|
||||
identifierType: fieldIdentifierTypes.none.value,
|
||||
isSubject: null,
|
||||
logicalType: void 0
|
||||
}));
|
||||
if (unformatted.length) {
|
||||
unformattedComplianceEntities = unformatted.map(({ identifierField }) => ({
|
||||
identifierField,
|
||||
identifierType: fieldIdentifierTypes.none.value,
|
||||
isSubject: null,
|
||||
logicalType: void 0
|
||||
}));
|
||||
|
||||
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` +
|
||||
`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;
|
||||
},
|
||||
@ -585,35 +640,20 @@ export default Component.extend({
|
||||
* @param {String} 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
|
||||
const currentFieldInComplianceList = complianceList.findBy('identifierField', identifierField);
|
||||
const currentFieldInComplianceList = currentComplianceEntities.findBy('identifierField', identifierField);
|
||||
const subjectIdString = fieldIdentifierTypes.subjectMember.value;
|
||||
// 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
|
||||
const unwrappedIdentifierType = subjectIdString === identifierType
|
||||
? fieldIdentifierTypes.member.value
|
||||
: identifierType;
|
||||
const updatedEntity = Object.assign({}, currentFieldInComplianceList, {
|
||||
identifierField,
|
||||
setProperties(currentFieldInComplianceList, {
|
||||
identifierType: unwrappedIdentifierType,
|
||||
isSubject: subjectIdString === identifierType ? true : null,
|
||||
// If the field is currently not in the complianceList,
|
||||
// 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
|
||||
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,
|
||||
// although the classification is based on the logicalType,
|
||||
// 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
|
||||
* @return {*}
|
||||
*/
|
||||
onFieldLogicalTypeChange(field, e) {
|
||||
onFieldLogicalTypeChange(field, { value: logicalType } = {}) {
|
||||
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 (!sourceIdentifierField) {
|
||||
if (!field) {
|
||||
this.actions.onFieldIdentifierTypeChange.call(
|
||||
this,
|
||||
{
|
||||
identifierField,
|
||||
logicalType
|
||||
},
|
||||
{ identifierField, logicalType },
|
||||
{ value: fieldIdentifierTypes.none.value }
|
||||
);
|
||||
} else {
|
||||
set(sourceIdentifierField, 'logicalType', logicalType);
|
||||
set(field, 'logicalType', logicalType);
|
||||
}
|
||||
|
||||
return this.setDefaultClassification({ identifierField }, { value: logicalType });
|
||||
@ -656,6 +691,10 @@ export default Component.extend({
|
||||
* @return {*}
|
||||
*/
|
||||
onFieldClassificationChange({ identifierField }, { value: classification = null }) {
|
||||
const currentFieldInComplianceList = get(this, 'mergedComplianceEntitiesAndColumnFields').findBy(
|
||||
'identifierField',
|
||||
identifierField
|
||||
);
|
||||
let fieldClassification = get(this, policyFieldClassificationKey);
|
||||
let updatedFieldClassification = {};
|
||||
// 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 });
|
||||
}
|
||||
|
||||
// Apply the updated classification value to the current instance of the field in working copy
|
||||
set(currentFieldInComplianceList, 'classification', classification);
|
||||
set(this, policyFieldClassificationKey, updatedFieldClassification);
|
||||
},
|
||||
|
||||
|
||||
@ -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
|
||||
setupController(controller, model) {
|
||||
let source = '';
|
||||
var id = 0;
|
||||
var urn = '';
|
||||
let id = 0;
|
||||
let urn = '';
|
||||
|
||||
/**
|
||||
* 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 = {
|
||||
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 }) {
|
||||
Promise.resolve(getJSON(datasetUrl(id))).then(({ dataset: { schema } = { schema: undefined } } = {}) => {
|
||||
/**
|
||||
@ -132,6 +121,73 @@ export default Route.extend({
|
||||
})
|
||||
)
|
||||
.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)))
|
||||
@ -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') {
|
||||
Promise.resolve(getJSON(getDatasetPropertiesUrl(id)))
|
||||
.then(({ status, properties }) => {
|
||||
|
||||
@ -1,10 +1,6 @@
|
||||
import Ember from 'ember';
|
||||
|
||||
const {
|
||||
Route,
|
||||
get,
|
||||
inject: { service }
|
||||
} = Ember;
|
||||
const { Route, get, inject: { service } } = Ember;
|
||||
|
||||
export default Route.extend({
|
||||
session: service(),
|
||||
@ -17,5 +13,16 @@ export default Route.extend({
|
||||
if (get(this, 'session.isAuthenticated')) {
|
||||
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'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
*/
|
||||
body {
|
||||
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 {
|
||||
@ -12,4 +12,4 @@ h1, h2, h3, h4, h5, h6 {
|
||||
|
||||
th {
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,3 +16,10 @@
|
||||
.dataset-field-value {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
/// Visual state for compliance elements
|
||||
.compliance-row {
|
||||
&--off-screen {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,5 +12,5 @@
|
||||
{{partial "main"}}
|
||||
</section>
|
||||
{{else}}
|
||||
{{outlet}}
|
||||
{{outlet "login"}}
|
||||
{{/if}}
|
||||
|
||||
@ -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')
|
||||
)}}
|
||||
@ -166,54 +166,56 @@
|
||||
{{/if}}
|
||||
</section>
|
||||
|
||||
{{#dataset-table
|
||||
class="nacho-table--stripped"
|
||||
fields=complianceDataFields
|
||||
sortColumnWithName=sortColumnWithName
|
||||
filterBy=filterBy
|
||||
sortDirection=sortDirection
|
||||
searchTerm=searchTerm as |table|
|
||||
}}
|
||||
<caption>
|
||||
<input
|
||||
type="text"
|
||||
title="Search fields"
|
||||
placeholder="Search 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 columnName="dataType"}}Data Type{{/head.column}}
|
||||
{{#head.column class="nacho-table-cell-wrapped"}}
|
||||
Member, Organization, or Group, ID's
|
||||
<a
|
||||
target="_blank"
|
||||
href="http://go/metadata_acquisition#ProjectOverview-compliance">
|
||||
<sup>
|
||||
More Info
|
||||
{{#if mergedComplianceEntitiesAndColumnFields.length}}
|
||||
{{#dataset-table
|
||||
class="nacho-table--stripped dataset-compliance-fields"
|
||||
fields=mergedComplianceEntitiesAndColumnFields
|
||||
sortColumnWithName=sortColumnWithName
|
||||
filterBy=filterBy
|
||||
sortDirection=sortDirection
|
||||
tableRowComponent='dataset-compliance-row'
|
||||
searchTerm=searchTerm as |table|
|
||||
}}
|
||||
<caption>
|
||||
<input
|
||||
type="text"
|
||||
title="Search fields"
|
||||
placeholder="Search 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 columnName="dataType"}}Data Type{{/head.column}}
|
||||
{{#head.column class="nacho-table-cell-wrapped"}}
|
||||
Member, Organization, or Group, ID's
|
||||
<a
|
||||
target="_blank"
|
||||
href="http://go/metadata_acquisition#ProjectOverview-compliance">
|
||||
<sup>
|
||||
More Info
|
||||
|
||||
<span class="glyphicon glyphicon-question-sign"
|
||||
title="More information on various IDs"></span>
|
||||
</sup>
|
||||
</a>
|
||||
{{/head.column}}
|
||||
{{#head.column}}
|
||||
Field Format?
|
||||
<a
|
||||
target="_blank"
|
||||
href="http://go/gdpr-taxonomy#MetadataTaxonomyforDataSets-DatasetLevelTags">
|
||||
<sup>
|
||||
More Info
|
||||
<span class="glyphicon glyphicon-question-sign"
|
||||
title="More information on various IDs"></span>
|
||||
</sup>
|
||||
</a>
|
||||
{{/head.column}}
|
||||
{{#head.column}}
|
||||
Field Format?
|
||||
<a
|
||||
target="_blank"
|
||||
href="http://go/gdpr-taxonomy#MetadataTaxonomyforDataSets-DatasetLevelTags">
|
||||
<sup>
|
||||
More Info
|
||||
|
||||
<span class="glyphicon glyphicon-question-sign"
|
||||
title="More information on Field Format"></span>
|
||||
</sup>
|
||||
</a>
|
||||
{{/head.column}}
|
||||
{{#head.column}}
|
||||
Security Classification
|
||||
<sup>
|
||||
<span class="glyphicon glyphicon-question-sign"
|
||||
title="More information on Field Format"></span>
|
||||
</sup>
|
||||
</a>
|
||||
{{/head.column}}
|
||||
{{#head.column}}
|
||||
Security Classification
|
||||
<sup>
|
||||
<span
|
||||
class="glyphicon glyphicon-question-sign"
|
||||
title={{helpText.classification}}>
|
||||
@ -223,51 +225,62 @@
|
||||
text=helpText.classification
|
||||
}}
|
||||
</span>
|
||||
</sup>
|
||||
{{/head.column}}
|
||||
{{/table.head}}
|
||||
{{#table.body as |body|}}
|
||||
{{#each (sort-by table.sortBy table.data) as |field|}}
|
||||
{{#body.row as |row|}}
|
||||
{{#row.cell}}
|
||||
{{field.identifierField}}
|
||||
{{/row.cell}}
|
||||
{{#row.cell}}
|
||||
{{field.dataType}}
|
||||
{{/row.cell}}
|
||||
{{#row.cell}}
|
||||
{{ember-selector
|
||||
disabled=(not isEditing)
|
||||
values=fieldIdentifierOptions
|
||||
selected=field.identifierType
|
||||
selectionDidChange=(action "onFieldIdentifierTypeChange" field)
|
||||
}}
|
||||
{{/row.cell}}
|
||||
{{#row.cell}}
|
||||
{{#power-select
|
||||
options=field.fieldFormats
|
||||
selected=field.logicalType
|
||||
disabled=(or field.isFieldFormatDisabled (not isEditing))
|
||||
placeholder="Select Format"
|
||||
allowClear=true
|
||||
searchField="label"
|
||||
triggerClass="ember-power-select-trigger-search"
|
||||
onchange=(action "onFieldLogicalTypeChange" field) as |fieldFormat|}}
|
||||
{{fieldFormat.label}}
|
||||
{{/power-select}}
|
||||
{{/row.cell}}
|
||||
{{#row.cell}}
|
||||
{{ember-selector
|
||||
class="nacho-select--hidden-state"
|
||||
values=classifiers
|
||||
selected=field.classification
|
||||
disabled=(or (not isEditing) (not field.logicalType))
|
||||
selectionDidChange=(action "onFieldClassificationChange" field)
|
||||
}}
|
||||
{{/row.cell}}
|
||||
{{/body.row}}
|
||||
{{/each}}
|
||||
{{/table.body}}
|
||||
{{/dataset-table}}
|
||||
</sup>
|
||||
{{/head.column}}
|
||||
{{/table.head}}
|
||||
{{#table.body as |body|}}
|
||||
{{#each (sort-by table.sortBy table.data) as |field|}}
|
||||
{{#body.row
|
||||
field=field
|
||||
onFieldLogicalTypeChange=(action 'onFieldLogicalTypeChange')
|
||||
onFieldClassificationChange=(action 'onFieldClassificationChange')
|
||||
onFieldIdentifierTypeChange=(action 'onFieldIdentifierTypeChange')as |row|}}
|
||||
|
||||
{{#row.cell}}
|
||||
{{row.identifierField}}
|
||||
{{/row.cell}}
|
||||
|
||||
{{#row.cell}}
|
||||
{{row.dataType}}
|
||||
{{/row.cell}}
|
||||
|
||||
{{#row.cell}}
|
||||
{{ember-selector
|
||||
disabled=(not isEditing)
|
||||
values=fieldIdentifierOptions
|
||||
selected=(readonly row.identifierType)
|
||||
selectionDidChange=(action row.onFieldIdentifierTypeChange)
|
||||
}}
|
||||
{{/row.cell}}
|
||||
|
||||
{{#row.cell}}
|
||||
{{#power-select
|
||||
options=row.fieldFormats
|
||||
selected=row.logicalType
|
||||
disabled=(or row.isFieldFormatDisabled (not isEditing))
|
||||
placeholder="Select Format"
|
||||
allowClear=true
|
||||
searchField="label"
|
||||
triggerClass="ember-power-select-trigger-search"
|
||||
onchange=(action row.onFieldLogicalTypeChange) as |fieldFormat|}}
|
||||
{{fieldFormat.label}}
|
||||
{{/power-select}}
|
||||
{{/row.cell}}
|
||||
|
||||
{{#row.cell}}
|
||||
{{ember-selector
|
||||
class="nacho-select--hidden-state"
|
||||
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>
|
||||
|
||||
{{yield}}
|
||||
|
||||
@ -17,7 +17,6 @@
|
||||
sortDirection=(readonly sortDirection)
|
||||
sortDidChange=(action "sortDidChange"))
|
||||
body=(component tableBodyComponent
|
||||
tableRowComponent=tableRowComponent
|
||||
)
|
||||
tableRowComponent=tableRowComponent)
|
||||
foot=(component tableFooterComponent)
|
||||
)}}
|
||||
|
||||
@ -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}}
|
||||
@ -1,117 +1,41 @@
|
||||
<div id="pagedDatasets">
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
{{#unless detailview}}
|
||||
<div class="well well-sm">
|
||||
<div class="row">
|
||||
<div class="col-xs-11">
|
||||
<ul class="breadcrumbs">
|
||||
{{#each breadcrumbs as |crumb|}}
|
||||
<li>
|
||||
{{#link-to 'datasets.page' crumb.urn title=crumb.title}}
|
||||
{{crumb.title}}
|
||||
{{/link-to}}
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="search-pagination">
|
||||
<ul class="pager">
|
||||
{{#unless first}}
|
||||
<li class="previous">
|
||||
{{#if urn}}
|
||||
{{#link-to 'datasets.name.subpage' currentName previousPage (query-params urn=urn)}}
|
||||
← Prev
|
||||
{{/link-to}}
|
||||
{{else}}
|
||||
{{#link-to 'datasets.page' previousPage}}
|
||||
← Prev
|
||||
{{/link-to}}
|
||||
{{/if}}
|
||||
</li>
|
||||
{{/unless}}
|
||||
<li>
|
||||
{{ model.data.count }} datasets - page {{ model.data.page }} of {{ model.data.totalPages }}
|
||||
</li>
|
||||
{{#unless last}}
|
||||
<li class="next">
|
||||
{{#if urn}}
|
||||
{{#link-to 'datasets.name.subpage' currentName nextPage (query-params urn=urn)}}
|
||||
Next →
|
||||
{{/link-to}}
|
||||
{{else}}
|
||||
{{#link-to 'datasets.page' nextPage}}
|
||||
Next →
|
||||
{{/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 class="row">
|
||||
{{#unless detailview}}
|
||||
<div class="search-pagination">
|
||||
<ul class="pager">
|
||||
{{#unless first}}
|
||||
<li class="previous">
|
||||
{{#if urn}}
|
||||
{{#link-to 'datasets.name.subpage' currentName previousPage
|
||||
(query-params urn=urn)}}
|
||||
← Prev
|
||||
{{/link-to}}
|
||||
{{else}}
|
||||
{{#link-to 'datasets.page' previousPage}}
|
||||
← Prev
|
||||
{{/link-to}}
|
||||
{{/if}}
|
||||
</li>
|
||||
{{/unless}}
|
||||
<li>
|
||||
{{ model.data.count }} datasets - page {{ model.data.page }}
|
||||
of {{ model.data.totalPages }}
|
||||
</li>
|
||||
{{#unless last}}
|
||||
<li class="next">
|
||||
{{#if urn}}
|
||||
{{#link-to 'datasets.name.subpage' currentName nextPage
|
||||
(query-params urn=urn)}}
|
||||
Next →
|
||||
{{/link-to}}
|
||||
{{else}}
|
||||
{{#link-to 'datasets.page' nextPage}}
|
||||
Next →
|
||||
{{/link-to}}
|
||||
{{/if}}
|
||||
</li>
|
||||
{{/unless}}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-xs-12">
|
||||
{{outlet}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/unless}}
|
||||
{{outlet}}
|
||||
</div>
|
||||
|
||||
@ -16,7 +16,8 @@
|
||||
"jsondiffpatch": "^0.2.4",
|
||||
"marked": "^0.3.6",
|
||||
"toastr": "^2.1.3",
|
||||
"x-editable": "^1.5.1"
|
||||
"x-editable": "^1.5.1",
|
||||
"scrollMonitor": "^1.2.3"
|
||||
},
|
||||
"resolutions": {
|
||||
"ember-cli-shims": "0.1.3",
|
||||
|
||||
@ -17,7 +17,7 @@ module.exports = function(defaults) {
|
||||
},
|
||||
|
||||
fingerprint: {
|
||||
enabled: true
|
||||
enabled: EmberApp.env() === 'production'
|
||||
},
|
||||
|
||||
'ember-cli-bootstrap-sassy': {
|
||||
@ -25,6 +25,7 @@ module.exports = function(defaults) {
|
||||
},
|
||||
|
||||
minifyJS: {
|
||||
enabled: EmberApp.env() === 'production',
|
||||
options: {
|
||||
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-formatters.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]));
|
||||
};
|
||||
|
||||
@ -79,7 +79,7 @@
|
||||
"lint-staged": {
|
||||
"gitDir": "../",
|
||||
"linters": {
|
||||
"wherehows-web/app/**/*.js": [
|
||||
"wherehows-web/{app,tests}/**/*.js": [
|
||||
"prettier --print-width 120 --single-quote --write",
|
||||
"git add"
|
||||
]
|
||||
|
||||
@ -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');
|
||||
});
|
||||
9
wherehows-web/vendor/shims/scrollmonitor.js
vendored
Normal file
9
wherehows-web/vendor/shims/scrollmonitor.js
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
(function() {
|
||||
function vendorModule() {
|
||||
'use strict';
|
||||
|
||||
return { 'default': self['scrollMonitor'] };
|
||||
}
|
||||
|
||||
define('scrollmonitor', [], vendorModule);
|
||||
})();
|
||||
Loading…
x
Reference in New Issue
Block a user