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",
"parserOptions": {
"ecmaVersion": 8,
"sourceType": "module"
"sourceType": "module",
"ecmaFeatures": {
"experimentalObjectRestSpread": true
}
},
"env": {
"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
} 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);
},

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
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 }) => {

View File

@ -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'
});
}
});

View File

@ -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;
}
}

View File

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

View File

@ -12,5 +12,5 @@
{{partial "main"}}
</section>
{{else}}
{{outlet}}
{{outlet "login"}}
{{/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}}
</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}}

View File

@ -17,7 +17,6 @@
sortDirection=(readonly sortDirection)
sortDidChange=(action "sortDidChange"))
body=(component tableBodyComponent
tableRowComponent=tableRowComponent
)
tableRowComponent=tableRowComponent)
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="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)}}
&larr; Prev
{{/link-to}}
{{else}}
{{#link-to 'datasets.page' previousPage}}
&larr; 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 &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 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)}}
&larr; Prev
{{/link-to}}
{{else}}
{{#link-to 'datasets.page' previousPage}}
&larr; 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 &rarr;
{{/link-to}}
{{else}}
{{#link-to 'datasets.page' nextPage}}
Next &rarr;
{{/link-to}}
{{/if}}
</li>
{{/unless}}
</ul>
</div>
<div class="col-xs-12">
{{outlet}}
</div>
</div>
</div>
{{/unless}}
{{outlet}}
</div>

View File

@ -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",

View File

@ -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]));
};

View File

@ -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"
]

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);
})();