From 09031814d5c75c819d3bdbc9805c65726b2e89b4 Mon Sep 17 00:00:00 2001 From: Seyi Adebajo Date: Tue, 26 Sep 2017 23:07:29 -0700 Subject: [PATCH 1/2] renames array-map util module and adds functions for filter and uniqueness. adds mergeMappedColumnFieldsWithSuggestions and fieldChangeSetRequiresReview to compliance shared functions --- wherehows-web/app/utils/array-map.ts | 10 --- wherehows-web/app/utils/array.ts | 24 +++++++ .../{functions.js => compliance-policy.js} | 62 ++++++++++++++++++- 3 files changed, 85 insertions(+), 11 deletions(-) delete mode 100644 wherehows-web/app/utils/array-map.ts create mode 100644 wherehows-web/app/utils/array.ts rename wherehows-web/app/utils/datasets/{functions.js => compliance-policy.js} (61%) diff --git a/wherehows-web/app/utils/array-map.ts b/wherehows-web/app/utils/array-map.ts deleted file mode 100644 index 716aea8766..0000000000 --- a/wherehows-web/app/utils/array-map.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Convenience utility takes a type-safe mapping function, and returns a list mapping function - * @param {(param: T) => U} mappingFunction maps a single type T to type U - * @return {(array: Array) => Array} - */ -const arrayMap = (mappingFunction: (param: T) => U): ((array: Array) => Array) => { - return array => array.map(mappingFunction); -}; - -export { arrayMap }; diff --git a/wherehows-web/app/utils/array.ts b/wherehows-web/app/utils/array.ts new file mode 100644 index 0000000000..7ccf49f3a2 --- /dev/null +++ b/wherehows-web/app/utils/array.ts @@ -0,0 +1,24 @@ +/** + * Convenience utility takes a type-safe mapping function, and returns a list mapping function + * @param {(param: T) => U} mappingFunction maps a single type T to type U + * @return {(array: Array) => Array} + */ +const arrayMap = (mappingFunction: (param: T) => U): ((array: Array) => Array) => (array = []) => + array.map(mappingFunction); + +/** + * Convenience utility takes a type-safe filter function, and returns a list filtering function + * @param {(param: T) => boolean} filtrationFunction + * @return {(array: Array) => Array} + */ +const arrayFilter = (filtrationFunction: (param: T) => boolean): ((array: Array) => Array) => (array = []) => + array.filter(filtrationFunction); + +/** + * Duplicate check using every to short-circuit iteration + * @param {Array} [list = []] list to check for dupes + * @return {boolean} true is unique + */ +const isListUnique = (list: Array = []): boolean => new Set(list).size === list.length; + +export { arrayMap, arrayFilter, isListUnique }; diff --git a/wherehows-web/app/utils/datasets/functions.js b/wherehows-web/app/utils/datasets/compliance-policy.js similarity index 61% rename from wherehows-web/app/utils/datasets/functions.js rename to wherehows-web/app/utils/datasets/compliance-policy.js index dbdc8ad447..a7c389f60b 100644 --- a/wherehows-web/app/utils/datasets/functions.js +++ b/wherehows-web/app/utils/datasets/compliance-policy.js @@ -110,4 +110,64 @@ const isPolicyExpectedShape = (candidatePolicy = {}) => { return false; }; -export { createInitialComplianceInfo, isPolicyExpectedShape }; +/** + * Checks if a compliance policy changeSet field requires user attention: if a suggestion + * is available but the user has not indicated intent or a policy for the field does not currently exist remotely + * and the related field changeSet has not been modified on the client + * @param {boolean} isDirty flag indicating the field changeSet has been modified on the client + * @param {object|void} suggestion the field suggestion properties + * @param {boolean} privacyPolicyExists flag indicating that the field has a current policy upstream + * @param {string} suggestionAuthority possibly empty string indicating the user intent for the suggestion + * @return {boolean} + */ +const fieldChangeSetRequiresReview = ({ isDirty, suggestion, privacyPolicyExists, suggestionAuthority }) => { + if (suggestion) { + return !suggestionAuthority; + } + + return !privacyPolicyExists && !isDirty; +}; + +/** + * Merges the column fields with the suggestion for the field if available + * @param {object} mappedColumnFields a map of column fields to compliance entity properties + * @param {object} fieldSuggestionMap a map of field suggestion properties keyed by field name + * @return {Array} mapped column field augmented with suggestion if available + */ +const mergeMappedColumnFieldsWithSuggestions = (mappedColumnFields = {}, fieldSuggestionMap = {}) => + Object.keys(mappedColumnFields).map(fieldName => { + const { + identifierField, + dataType, + identifierType, + logicalType, + securityClassification, + privacyPolicyExists, + isDirty + } = mappedColumnFields[fieldName]; + const suggestion = fieldSuggestionMap[identifierField]; + + const field = { + identifierField, + dataType, + identifierType, + logicalType, + privacyPolicyExists, + isDirty, + classification: securityClassification + }; + + // If a suggestion exists for this field add the suggestion attribute to the field properties + if (suggestion) { + return { ...field, suggestion }; + } + + return field; + }); + +export { + createInitialComplianceInfo, + isPolicyExpectedShape, + fieldChangeSetRequiresReview, + mergeMappedColumnFieldsWithSuggestions +}; From 748540914608ac7be9e486344e22973945e5011d Mon Sep 17 00:00:00 2001 From: Seyi Adebajo Date: Wed, 27 Sep 2017 09:33:20 -0700 Subject: [PATCH 2/2] adds reviewable filter option. updates filter logic to create diff between fields on policy and column field values. refactors implementation for client working copy / change-set: reduces number of steps. modifies compliance entity table styles --- .../app/components/dataset-compliance-row.js | 46 ++--- .../app/components/dataset-compliance.js | 179 ++++++++++-------- .../app/components/disable-bubble-input.ts | 10 + .../app/constants/metadata-acquisition.ts | 58 +++++- .../_compliance-container.scss | 6 + .../dataset-compliance/_compliance-table.scss | 5 +- .../components/nacho/_nacho-select.scss | 6 +- .../notifications/_notification-dot.scss | 4 +- .../components/dataset-compliance-row.hbs | 3 +- .../components/disable-bubble-input.hbs | 1 + .../-dataset-compliance-entities.hbs | 70 ++++--- .../app/typings/untyped-js-module.d.ts | 2 +- .../app/utils/api/datasets/columns.ts | 2 +- .../app/utils/api/datasets/compliance.ts | 2 +- wherehows-web/app/utils/object.ts | 15 ++ wherehows-web/package.json | 1 - 16 files changed, 261 insertions(+), 149 deletions(-) create mode 100644 wherehows-web/app/components/disable-bubble-input.ts create mode 100644 wherehows-web/app/templates/components/disable-bubble-input.hbs create mode 100644 wherehows-web/app/utils/object.ts diff --git a/wherehows-web/app/components/dataset-compliance-row.js b/wherehows-web/app/components/dataset-compliance-row.js index 8932ec2804..da52ca2423 100644 --- a/wherehows-web/app/components/dataset-compliance-row.js +++ b/wherehows-web/app/components/dataset-compliance-row.js @@ -1,36 +1,19 @@ import Ember from 'ember'; import DatasetTableRow from 'wherehows-web/components/dataset-table-row'; import { - fieldIdentifierTypes, + fieldIdentifierTypeValues, + fieldIdentifierTypeIds, defaultFieldDataTypeClassification, isMixedId, isCustomId, hasPredefinedFieldFormat, logicalTypesForIds, - logicalTypesForGeneric + logicalTypesForGeneric, + SuggestionIntent } from 'wherehows-web/constants'; -import { fieldIdentifierTypeIds } from 'wherehows-web/components/dataset-compliance'; +import { fieldChangeSetRequiresReview } from 'wherehows-web/utils/datasets/compliance-policy'; -const { computed, get, getProperties, set } = Ember; -/** - * String indicating that the user affirms a suggestion - * @type {string} - */ -const acceptIntent = 'accept'; - -/** - * String indicating that the user ignored a suggestion - * @type {string} - */ -const ignoreIntent = 'ignore'; - -/** - * Caches a list of fieldIdentifierTypes values - * @type {Array} - */ -const fieldIdentifierTypeValues = Object.keys(fieldIdentifierTypes) - .map(fieldIdentifierType => fieldIdentifierTypes[fieldIdentifierType]) - .mapBy('value'); +const { computed, get, getProperties } = Ember; /** * Extracts the suggestions for identifierType, logicalType suggestions, and confidence from a list of predictions @@ -77,23 +60,23 @@ export default DatasetTableRow.extend({ suggestionAuthority: computed.alias('field.suggestionAuthority'), /** - * Checks that the field does not have a recently input value + * Checks that the field does not have a current policy value * @type {Ember.computed} * @return {boolean} */ - isNewField: computed('isNewComplianceInfo', 'isModified', function() { - const { isNewComplianceInfo, isModified } = getProperties(this, ['isNewComplianceInfo', 'isModified']); - return isNewComplianceInfo && !isModified; + isReviewRequested: computed('field.{isDirty,suggestion,privacyPolicyExists,suggestionAuthority}', function() { + return fieldChangeSetRequiresReview(get(this, 'field')); }), /** * Maps the suggestion response to a string resolution * @type {Ember.computed} + * @return {string|void} */ suggestionResolution: computed('field.suggestionAuthority', function() { return { - [acceptIntent]: 'Accepted', - [ignoreIntent]: 'Discarded' + [SuggestionIntent.accept]: 'Accepted', + [SuggestionIntent.ignore]: 'Discarded' }[get(this, 'field.suggestionAuthority')]; }), @@ -204,7 +187,6 @@ export default DatasetTableRow.extend({ const { onFieldIdentifierTypeChange } = this.attrs; if (typeof onFieldIdentifierTypeChange === 'function') { onFieldIdentifierTypeChange(get(this, 'field'), { value }); - set(this, 'isModified', true); } }, @@ -217,7 +199,6 @@ export default DatasetTableRow.extend({ const { onFieldLogicalTypeChange } = this.attrs; if (typeof onFieldLogicalTypeChange === 'function') { onFieldLogicalTypeChange(get(this, 'field'), { value }); - set(this, 'isModified', true); } }, @@ -229,7 +210,6 @@ export default DatasetTableRow.extend({ const { onFieldClassificationChange } = this.attrs; if (typeof onFieldClassificationChange === 'function') { onFieldClassificationChange(get(this, 'field'), { value }); - set(this, 'isModified', true); } }, @@ -242,7 +222,7 @@ export default DatasetTableRow.extend({ const { onSuggestionIntent } = this.attrs; // Accept the suggestion for either identifierType and/or logicalType - if (intent === acceptIntent) { + if (intent === SuggestionIntent.accept) { const { identifierType, logicalType } = get(this, 'prediction'); if (identifierType) { this.actions.onFieldIdentifierTypeChange.call(this, { value: identifierType }); diff --git a/wherehows-web/app/components/dataset-compliance.js b/wherehows-web/app/components/dataset-compliance.js index c3c7119cae..043ab51fe0 100644 --- a/wherehows-web/app/components/dataset-compliance.js +++ b/wherehows-web/app/components/dataset-compliance.js @@ -4,6 +4,7 @@ import { classifiers, datasetClassifiers, fieldIdentifierTypes, + fieldIdentifierTypeIds, idLogicalTypes, nonIdFieldLogicalTypes, defaultFieldDataTypeClassification, @@ -13,8 +14,14 @@ import { hasPredefinedFieldFormat, getDefaultLogicalType } from 'wherehows-web/constants'; -import { isPolicyExpectedShape } from 'wherehows-web/utils/datasets/functions'; +import { + isPolicyExpectedShape, + fieldChangeSetRequiresReview, + mergeMappedColumnFieldsWithSuggestions +} from 'wherehows-web/utils/datasets/compliance-policy'; import scrollMonitor from 'scrollmonitor'; +import { hasEnumerableKeys } from 'wherehows-web/utils/object'; +import { arrayFilter, isListUnique } from 'wherehows-web/utils/array'; const { assert, @@ -75,17 +82,17 @@ const datasetClassifiersKeys = Object.keys(datasetClassifiers); * @type {string} */ const policyComplianceEntitiesKey = 'complianceInfo.complianceEntities'; -/** - * Duplicate check using every to short-circuit iteration - * @param {Array} list = [] the list to check for dupes - * @return {Boolean} true is unique, false otherwise - */ -const listIsUnique = (list = []) => new Set(list).size === list.length; assert('`fieldIdentifierTypes` contains an object with a key `none`', typeof fieldIdentifierTypes.none === 'object'); const fieldIdentifierTypeKeysBarNone = Object.keys(fieldIdentifierTypes).filter(k => k !== 'none'); const fieldDisplayKeys = ['none', '_', ...fieldIdentifierTypeKeysBarNone]; +/** + * Returns a list of changeSet fields that requires user attention + * @type {function({}): Array<{ isDirty, suggestion, privacyPolicyExists, suggestionAuthority }>} + */ +const changeSetFieldsRequiringReview = arrayFilter(fieldChangeSetRequiresReview); + /** * A list of field identifier types mapped to label, value options for select display * @type {any[]|Array.<{value: String, label: String}>} @@ -103,15 +110,6 @@ 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.} - */ -export const fieldIdentifierTypeIds = Object.keys(fieldIdentifierTypes) - .map(fieldIdentifierType => fieldIdentifierTypes[fieldIdentifierType]) - .filter(({ isId }) => isId) - .mapBy('value'); - export default Component.extend({ sortColumnWithName: 'identifierField', filterBy: 'identifierField', @@ -142,6 +140,7 @@ export default Component.extend({ * @return {boolean} */ isReadOnly: computed.not('isEditing'), + /** * Flag indicating that the component is currently saving / attempting to save the privacy policy * @type {String} @@ -183,6 +182,25 @@ export default Component.extend({ })); }), + /** + * Returns a list of ui values and labels for review filter drop-down + * @type {Ember.computed} + */ + fieldReviewOptions: computed(function() { + return [ + { value: 'showAll', label: 'Showing all fields' }, + { + value: 'showReview', + label: 'Showing only fields to review' + } + ]; + }), + + /** + * @type {string} + */ + fieldReviewOption: 'showAll', + /** * Reference to the application notifications Service * @type {Ember.Service} @@ -272,7 +290,7 @@ export default Component.extend({ const fieldNames = getWithDefault(this, 'schemaFieldNamesMappedToDataTypes', []).mapBy('fieldName'); // identifier field names from the column api should be unique - if (listIsUnique(fieldNames.sort())) { + if (isListUnique(fieldNames.sort())) { return set(this, '_hasBadData', false); } @@ -400,48 +418,13 @@ export default Component.extend({ }, []); }), - /** - * Lists all dataset fields found in the `columns` performs an intersection - * of fields with the currently persisted and/or updated - * privacyCompliancePolicy.complianceEntities. - * The returned list is a map of fields with current or default privacy properties - */ - mergeComplianceEntitiesAndColumnFields( - columnIdFieldsToCurrentPrivacyPolicy = {}, - truncatedColumnFields = [], - identifierFieldMappedToSuggestions = {} - ) { - return truncatedColumnFields.map(({ fieldName: identifierField, dataType }) => { - const { - [identifierField]: { identifierType, logicalType, securityClassification } - } = columnIdFieldsToCurrentPrivacyPolicy; - - //Cache the mapped suggestion into a local - const suggestion = identifierFieldMappedToSuggestions[identifierField]; - let field = { - identifierField, - dataType, - identifierType, - logicalType, - classification: securityClassification - }; - - // If a suggestion exists for this field add the suggestion attribute to the field properties - if (suggestion) { - field = { ...field, suggestion }; - } - - return field; - }); - }, - /** * - * @param {Array} columnFieldNames - * @return {*|{}|any} + * @param {Array} columnFieldProps + * @param {Array} complianceEntities + * @return {object} */ - mapColumnIdFieldsToCurrentPrivacyPolicy(columnFieldNames) { - const complianceEntities = get(this, policyComplianceEntitiesKey) || []; + mapColumnIdFieldsToCurrentPrivacyPolicy(columnFieldProps, complianceEntities) { const getKeysOnField = (keys = [], fieldName, source = []) => { const sourceField = source.find(({ identifierField }) => identifierField === fieldName) || {}; let ret = {}; @@ -454,14 +437,23 @@ export default Component.extend({ return ret; }; - return columnFieldNames.reduce((acc, identifierField) => { + return columnFieldProps.reduce((acc, { identifierField, dataType }) => { const currentPrivacyAttrs = getKeysOnField( ['identifierType', 'logicalType', 'securityClassification'], identifierField, complianceEntities ); - return { ...acc, ...{ [identifierField]: currentPrivacyAttrs } }; + return { + ...acc, + [identifierField]: { + identifierField, + dataType, + ...currentPrivacyAttrs, + privacyPolicyExists: hasEnumerableKeys(currentPrivacyAttrs), + isDirty: false + } + }; }, {}); }, @@ -473,29 +465,58 @@ export default Component.extend({ 'truncatedColumnFields', `${policyComplianceEntitiesKey}.[]`, function() { - const columnFieldNames = get(this, 'truncatedColumnFields').map(({ fieldName }) => fieldName); - return this.mapColumnIdFieldsToCurrentPrivacyPolicy(columnFieldNames); + // Truncated list of Dataset field names and data types currently returned from the column endpoint + const columnFieldProps = get(this, 'truncatedColumnFields').map(({ fieldName, dataType }) => ({ + identifierField: fieldName, + dataType + })); + // Dataset fields that currently have a compliance policy + const currentComplianceEntities = get(this, policyComplianceEntitiesKey) || []; + + return this.mapColumnIdFieldsToCurrentPrivacyPolicy(columnFieldProps, currentComplianceEntities); } ), /** * Caches a reference to the generated list of merged data between the column api and the current compliance entities list - * @type {Ember.computed} + * @type {Array<{identifierType: string, logicalType: string, securityClassification: string, privacyPolicyExists: boolean, isDirty: boolean, [suggestion]: object}>} */ - mergedComplianceEntitiesAndColumnFields: computed('columnIdFieldsToCurrentPrivacyPolicy', function() { + compliancePolicyChangeSet: computed('columnIdFieldsToCurrentPrivacyPolicy', function() { // truncatedColumnFields is a dependency for cp columnIdFieldsToCurrentPrivacyPolicy, so no need to dep on that directly - return this.mergeComplianceEntitiesAndColumnFields( + return mergeMappedColumnFieldsWithSuggestions( get(this, 'columnIdFieldsToCurrentPrivacyPolicy'), - get(this, 'truncatedColumnFields'), get(this, 'identifierFieldToSuggestion') ); }), + /** + * Returns a list of changeSet fields that meets the user selected filter criteria + * @type {Ember.computed} + * @return {Array<{}>} + */ + filteredChangeSet: computed('changeSetReviewCount', 'fieldReviewOption', 'compliancePolicyChangeSet', function() { + const changeSet = get(this, 'compliancePolicyChangeSet'); + + return get(this, 'fieldReviewOption') === 'showReview' ? changeSetFieldsRequiringReview(changeSet) : changeSet; + }), + + /** + * Returns a count of changeSet fields that require user attention + * @type {Ember.computed} + * @return {Array<{}>} + */ + changeSetReviewCount: computed( + 'compliancePolicyChangeSet.@each.{isDirty,suggestion,privacyPolicyExists,suggestionAuthority}', + function() { + return changeSetFieldsRequiringReview(get(this, 'compliancePolicyChangeSet')).length; + } + ), + /** * Creates a mapping of compliance suggestions to identifierField * This improves performance in a subsequent merge op since this loop * happens only once and is cached - * @type {Ember.computed} + * @type {object} */ identifierFieldToSuggestion: computed('complianceSuggestion', function() { const identifierFieldToSuggestion = {}; @@ -603,7 +624,7 @@ export default Component.extend({ // All candidate fields that can be on policy, excluding tracking type fields const datasetFields = get( this, - 'mergedComplianceEntitiesAndColumnFields' + 'compliancePolicyChangeSet' ).map(({ identifierField, identifierType, logicalType, classification }) => ({ identifierField, identifierType, @@ -681,7 +702,7 @@ export default Component.extend({ complianceEntities.filter(({ identifierType }) => fieldIdentifierTypeIds.includes(identifierType)), [...genericLogicalTypes, ...idLogicalTypes] ); - const fieldIdentifiersAreUnique = listIsUnique(complianceEntities.mapBy('identifierField')); + const fieldIdentifiersAreUnique = isListUnique(complianceEntities.mapBy('identifierField')); const schemaFieldLengthGreaterThanComplianceEntities = this.isSchemaFieldLengthGreaterThanComplianceEntities(); if (!fieldIdentifiersAreUnique || !schemaFieldLengthGreaterThanComplianceEntities) { @@ -751,6 +772,14 @@ export default Component.extend({ return set(this, 'showAllDatasetMemberData', true); }, + /** + * Updates the fieldReviewOption with the user selected value + * @param {string} value + */ + onFieldReviewChange({ value }) { + return set(this, 'fieldReviewOption', value); + }, + /** * Handler for setting the compliance policy into edit mode and rendering */ @@ -862,17 +891,19 @@ export default Component.extend({ * @param {String} identifierType */ onFieldIdentifierTypeChange({ identifierField }, { value: identifierType }) { - const currentComplianceEntities = get(this, 'mergedComplianceEntitiesAndColumnFields'); - let logicalType; + const currentComplianceEntities = get(this, 'compliancePolicyChangeSet'); // A reference to the current field in the compliance list, it should exist even for empty complianceEntities - // since this is a reference created in the working copy: mergedComplianceEntitiesAndColumnFields + // since this is a reference created in the working copy: compliancePolicyChangeSet const currentFieldInComplianceList = currentComplianceEntities.findBy('identifierField', identifierField); + let logicalType; if (hasPredefinedFieldFormat(identifierType)) { logicalType = getDefaultLogicalType(identifierType); } + setProperties(currentFieldInComplianceList, { identifierType, - logicalType + logicalType, + isDirty: true }); // Set the defaultClassification for the identifierField, // although the classification is based on the logicalType, @@ -898,7 +929,7 @@ export default Component.extend({ { value: fieldIdentifierTypes.none.value } ); } else { - set(field, 'logicalType', logicalType); + setProperties(field, { logicalType, isDirty: true }); } return this.setDefaultClassification({ identifierField }, { value: logicalType }); @@ -911,7 +942,7 @@ export default Component.extend({ * @return {*} */ onFieldClassificationChange({ identifierField }, { value: classification = null }) { - const currentFieldInComplianceList = get(this, 'mergedComplianceEntitiesAndColumnFields').findBy( + const currentFieldInComplianceList = get(this, 'compliancePolicyChangeSet').findBy( 'identifierField', identifierField ); @@ -919,7 +950,7 @@ export default Component.extend({ this.clearMessages(); // Apply the updated classification value to the current instance of the field in working copy - set(currentFieldInComplianceList, 'classification', classification); + setProperties(currentFieldInComplianceList, { classification, isDirty: true }); }, /** diff --git a/wherehows-web/app/components/disable-bubble-input.ts b/wherehows-web/app/components/disable-bubble-input.ts new file mode 100644 index 0000000000..7da18715a1 --- /dev/null +++ b/wherehows-web/app/components/disable-bubble-input.ts @@ -0,0 +1,10 @@ +import Ember from 'ember'; + +const { TextField } = Ember; + +export default TextField.extend({ + /** + * Prevents click event bubbling + */ + click: () => false +}); diff --git a/wherehows-web/app/constants/metadata-acquisition.ts b/wherehows-web/app/constants/metadata-acquisition.ts index fce1f43e04..dd6b696f9e 100644 --- a/wherehows-web/app/constants/metadata-acquisition.ts +++ b/wherehows-web/app/constants/metadata-acquisition.ts @@ -1,4 +1,5 @@ import Ember from 'ember'; +import { arrayMap } from 'wherehows-web/utils/array'; /** * Defines the string values that are allowed for a classification @@ -9,6 +10,14 @@ enum Classification { HighlyConfidential = 'highlyConfidential' } +/** + * String indicating that the user affirms or ignored a field suggestion + */ +enum SuggestionIntent { + accept = 'accept', + ignore = 'ignore' +} + /** * Describes the index signature for the nonIdFieldLogicalTypes object */ @@ -18,6 +27,23 @@ interface INonIdLogicalTypes { displayAs: string; }; } + +/** + * Describes the properties on a field identifier object for ui rendering + */ +interface IFieldIdProps { + value: string; + isId: boolean; + displayAs: string; +} + +/** + * Describes the index signature for fieldIdentifierTypes + */ +interface IFieldIdTypes { + [prop: string]: IFieldIdProps; +} + /** * A list of id logical types * @type {Array.} @@ -154,12 +180,11 @@ const defaultFieldDataTypeClassification = Object.assign( const classifiers = Object.values(defaultFieldDataTypeClassification).filter( (classifier, index, iter) => iter.indexOf(classifier) === index ); - /** * A map of identifier types for fields on a dataset * @type {{none: {value: string, isId: boolean, displayAs: string}, member: {value: string, isId: boolean, displayAs: string}, subjectMember: {value: string, isId: boolean, displayAs: string}, group: {value: string, isId: boolean, displayAs: string}, organization: {value: string, isId: boolean, displayAs: string}, generic: {value: string, isId: boolean, displayAs: string}}} */ -const fieldIdentifierTypes = { +const fieldIdentifierTypes: IFieldIdTypes = { none: { value: 'NONE', isId: false, @@ -282,10 +307,36 @@ const logicalTypesForIds = logicalTypeValueLabel('id'); // Map generic logical type to options consumable in DOM const logicalTypesForGeneric = logicalTypeValueLabel('generic'); +/** + * Caches a list of field identifiers + * @type {Array} + */ +const fieldIdentifierTypesList: Array = arrayMap( + (fieldIdentifierType: string) => fieldIdentifierTypes[fieldIdentifierType] +)(Object.keys(fieldIdentifierTypes)); + +/** + * A list of field identifier types that are Ids i.e member ID, org ID, group ID + * @type {Array>} + */ +const fieldIdentifierTypeIds: Array> = fieldIdentifierTypesList + .filter(({ isId }) => isId) + .map(({ value }) => ({ value })); + +/** + * Caches a list of fieldIdentifierTypes values + * @type {Array>} + */ +const fieldIdentifierTypeValues: Array> = fieldIdentifierTypesList.map(({ value }) => ({ + value +})); + export { defaultFieldDataTypeClassification, classifiers, fieldIdentifierTypes, + fieldIdentifierTypeIds, + fieldIdentifierTypeValues, idLogicalTypes, customIdLogicalTypes, nonIdFieldLogicalTypes, @@ -294,5 +345,6 @@ export { hasPredefinedFieldFormat, logicalTypesForIds, logicalTypesForGeneric, - getDefaultLogicalType + getDefaultLogicalType, + SuggestionIntent }; diff --git a/wherehows-web/app/styles/components/dataset-compliance/_compliance-container.scss b/wherehows-web/app/styles/components/dataset-compliance/_compliance-container.scss index e86fa6442c..5401f9b8d2 100644 --- a/wherehows-web/app/styles/components/dataset-compliance/_compliance-container.scss +++ b/wherehows-web/app/styles/components/dataset-compliance/_compliance-container.scss @@ -35,3 +35,9 @@ visibility: hidden; } } + +.compliance-entities-meta { + padding: item-spacing(2 0 2); + color: #777777; + text-align: left; +} diff --git a/wherehows-web/app/styles/components/dataset-compliance/_compliance-table.scss b/wherehows-web/app/styles/components/dataset-compliance/_compliance-table.scss index 6dfe4768a7..e152abee78 100644 --- a/wherehows-web/app/styles/components/dataset-compliance/_compliance-table.scss +++ b/wherehows-web/app/styles/components/dataset-compliance/_compliance-table.scss @@ -1,12 +1,13 @@ -@import "../variables"; +@import '../variables'; .dataset-compliance-fields { &__has-suggestions { color: $compliance-suggestion-hint; + margin-left: item-spacing(2); } &__notification-column { - width: 5% + width: 5%; } &__classification-column { diff --git a/wherehows-web/app/styles/components/nacho/_nacho-select.scss b/wherehows-web/app/styles/components/nacho/_nacho-select.scss index 78731c759e..d9302c6efc 100644 --- a/wherehows-web/app/styles/components/nacho/_nacho-select.scss +++ b/wherehows-web/app/styles/components/nacho/_nacho-select.scss @@ -1,5 +1,5 @@ -@import "../../abstracts/variables"; -@import "../../abstracts/mixins"; +@import '../../abstracts/variables'; +@import '../../abstracts/mixins'; /** * - - {{#if (and hasRecentSuggestions (not isNewComplianceInfo))}} - - {{complianceSuggestion.complianceSuggestions.length}} fields to be reviewed - - {{/if}} - + searchTerm=searchTerm as |table| + }} {{#table.head as |head|}} {{#head.column class="dataset-compliance-fields__notification-column"}}{{/head.column}} @@ -94,6 +95,18 @@ {{/head.column}} {{/table.head}} + + + + {{disable-bubble-input + title="Search field names" + placeholder="Search field names" + value=table.searchTerm + on-input=(action table.filterDidChange value="target.value") + }} + + + {{#table.body as |body|}} {{#each (sort-by table.sortBy table.data) as |field|}} {{#body.row @@ -104,17 +117,22 @@ onFieldLogicalTypeChange=(action 'onFieldLogicalTypeChange') onFieldClassificationChange=(action 'onFieldClassificationChange') onSuggestionIntent=(action 'onFieldSuggestionIntentChange') - onFieldIdentifierTypeChange=(action 'onFieldIdentifierTypeChange')as |row|}} + onFieldIdentifierTypeChange=(action 'onFieldIdentifierTypeChange') as |row| + }} {{#row.cell}} - {{#if row.suggestion}} + {{#if (and row.suggestion (not row.suggestionResolution))}} + - {{/if}} - {{#if (and row.isNewField (not row.suggestion))}} - + {{else}} + + {{#if row.isReviewRequested}} + + {{/if}} + {{/if}} {{/row.cell}} @@ -134,8 +152,8 @@ {{auto-suggest-action action=(action row.onSuggestionAction)}} {{/if}} {{else}} - {{#if row.suggestionAuthority}} - {{capitalize row.suggestionResolution}} + {{#if row.suggestionResolution}} + {{row.suggestionResolution}} {{else}} — {{/if}} diff --git a/wherehows-web/app/typings/untyped-js-module.d.ts b/wherehows-web/app/typings/untyped-js-module.d.ts index f154265285..0c2ea85323 100644 --- a/wherehows-web/app/typings/untyped-js-module.d.ts +++ b/wherehows-web/app/typings/untyped-js-module.d.ts @@ -2,7 +2,7 @@ declare module 'ember-modal-dialog/components/modal-dialog'; declare module 'ember-simple-auth/mixins/authenticated-route-mixin'; -declare module 'wherehows-web/utils/datasets/functions'; +declare module 'wherehows-web/utils/datasets/compliance-policy'; // https://github.com/ember-cli/ember-fetch/issues/72 // TS assumes the mapping btw ES modules and CJS modules is 1:1 diff --git a/wherehows-web/app/utils/api/datasets/columns.ts b/wherehows-web/app/utils/api/datasets/columns.ts index 4cde682052..0f4785b94c 100644 --- a/wherehows-web/app/utils/api/datasets/columns.ts +++ b/wherehows-web/app/utils/api/datasets/columns.ts @@ -6,7 +6,7 @@ import { } from 'wherehows-web/typings/api/datasets/columns'; import { getJSON } from 'wherehows-web/utils/api/fetcher'; import { ApiStatus } from 'wherehows-web/utils/api'; -import { arrayMap } from 'wherehows-web/utils/array-map'; +import { arrayMap } from 'wherehows-web/utils/array'; // TODO: DSS-6122 Create and move to Error module diff --git a/wherehows-web/app/utils/api/datasets/compliance.ts b/wherehows-web/app/utils/api/datasets/compliance.ts index 9adcaef151..25bf787f62 100644 --- a/wherehows-web/app/utils/api/datasets/compliance.ts +++ b/wherehows-web/app/utils/api/datasets/compliance.ts @@ -1,5 +1,5 @@ import Ember from 'ember'; -import { createInitialComplianceInfo } from 'wherehows-web/utils/datasets/functions'; +import { createInitialComplianceInfo } from 'wherehows-web/utils/datasets/compliance-policy'; import { datasetUrlById } from 'wherehows-web/utils/api/datasets/shared'; import { ApiStatus } from 'wherehows-web/utils/api/shared'; import { IComplianceSuggestion, IComplianceSuggestionResponse } from 'wherehows-web/typings/api/datasets/compliance'; diff --git a/wherehows-web/app/utils/object.ts b/wherehows-web/app/utils/object.ts new file mode 100644 index 0000000000..c101ec892c --- /dev/null +++ b/wherehows-web/app/utils/object.ts @@ -0,0 +1,15 @@ +/** + * Checks if a type is an object + * @param {any} candidate the entity to check + */ +const isObject = (candidate: any): candidate is object => + candidate && Object.prototype.toString.call(candidate) === '[object Object]'; + +/** + * Checks that an object has it own enumerable props + * @param {Object} object the object to the be tested + * @return {boolean} true if enumerable keys are present + */ +const hasEnumerableKeys = (object: object): boolean => isObject(object) && !!Object.keys(object).length; + +export { isObject, hasEnumerableKeys }; diff --git a/wherehows-web/package.json b/wherehows-web/package.json index bdd7fa70ed..8e2585c38d 100644 --- a/wherehows-web/package.json +++ b/wherehows-web/package.json @@ -42,7 +42,6 @@ "ember-cli-typescript": "^1.0.3", "ember-cli-uglify": "^1.2.0", "ember-composable-helpers": "^2.0.0", - "ember-data": "^2.11.1", "ember-export-application-global": "^1.1.1", "ember-fetch": "^3.4.0", "ember-lodash-shim": "^2.0.2",