mirror of
https://github.com/datahub-project/datahub.git
synced 2025-11-02 11:49:23 +00:00
Merge branch 'master' of https://github.com/linkedin/WhereHows
This commit is contained in:
commit
7d6958dcb1
@ -66,6 +66,10 @@ public class Application extends Controller {
|
||||
private static final Boolean HTTPS_REDIRECT = Play.application().configuration().getBoolean("https.redirect", false);
|
||||
private static final Boolean WHZ_SHOW_LINEAGE =
|
||||
Play.application().configuration().getBoolean("linkedin.show.dataset.lineage", false);
|
||||
private static final Boolean WHZ_SHOW_DS_HEALTH =
|
||||
Play.application().configuration().getBoolean("linkedin.show.dataset.health", false);
|
||||
private static final String WHZ_SUGGESTION_CONFIDENCE_THRESHOLD =
|
||||
Play.application().configuration().getString("linkedin.suggestion.confidence.threshold", "50");
|
||||
|
||||
private static final String WHZ_WIKI_LINKS__GDRP_PII =
|
||||
Play.application().configuration().getString("linkedin.links.wiki.gdprPii", "");
|
||||
@ -199,6 +203,8 @@ public class Application extends Controller {
|
||||
config.put("appVersion", APP_VERSION);
|
||||
config.put("isInternal", IS_INTERNAL);
|
||||
config.put("shouldShowDatasetLineage", WHZ_SHOW_LINEAGE);
|
||||
config.put("shouldShowDatasetHealth", WHZ_SHOW_DS_HEALTH);
|
||||
config.put("suggestionConfidenceThreshold", Integer.parseInt(WHZ_SUGGESTION_CONFIDENCE_THRESHOLD));
|
||||
config.set("wikiLinks", wikiLinks());
|
||||
config.set("JitAclAccessWhitelist", Json.toJson(StringUtils.split(JIT_ACL_WHITELIST, ',')));
|
||||
config.set("tracking", trackingInfo());
|
||||
|
||||
@ -67,6 +67,13 @@ export default class DatasetComplianceFieldTag extends Component {
|
||||
*/
|
||||
parentHasSingleTag: boolean;
|
||||
|
||||
/**
|
||||
* Confidence percentage number used to filter high quality suggestions versus lower quality
|
||||
* @type {number}
|
||||
* @memberof DatasetComplianceFieldTag
|
||||
*/
|
||||
suggestionConfidenceThreshold: number;
|
||||
|
||||
/**
|
||||
* Stores the value of error result if the valuePattern is invalid
|
||||
* @type {string}
|
||||
@ -204,8 +211,11 @@ export default class DatasetComplianceFieldTag extends Component {
|
||||
this: DatasetComplianceFieldTag
|
||||
): boolean {
|
||||
const tagWithoutSuggestion = <IComplianceChangeSet>omit<IComplianceChangeSet>(get(this, 'tag'), ['suggestion']);
|
||||
const suggestionConfidenceThreshold = get(this, 'suggestionConfidenceThreshold');
|
||||
|
||||
return tagNeedsReview(get(this, 'complianceDataTypes'))(tagWithoutSuggestion);
|
||||
return tagNeedsReview(get(this, 'complianceDataTypes'), { checkSuggestions: true, suggestionConfidenceThreshold })(
|
||||
tagWithoutSuggestion
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@ -65,9 +65,17 @@ export default class DatasetComplianceRollupRow extends Component.extend({
|
||||
/**
|
||||
* Reference to the compliance data types
|
||||
* @type {Array<IComplianceDataType>}
|
||||
* @memberof DatasetComplianceRollupRow
|
||||
*/
|
||||
complianceDataTypes: Array<IComplianceDataType>;
|
||||
|
||||
/**
|
||||
* Confidence percentage number used to filter high quality suggestions versus lower quality
|
||||
* @type {number}
|
||||
* @memberof DatasetComplianceRollupRow
|
||||
*/
|
||||
suggestionConfidenceThreshold: number;
|
||||
|
||||
/**
|
||||
* Flag indicating the field has a readonly attribute
|
||||
* @type ComputedProperty<boolean>
|
||||
@ -83,9 +91,16 @@ export default class DatasetComplianceRollupRow extends Component.extend({
|
||||
isReviewRequested = computed(
|
||||
`fieldChangeSet.@each.{${changeSetReviewableAttributeTriggers}}`,
|
||||
'complianceDataTypes',
|
||||
'suggestionConfidenceThreshold',
|
||||
function(this: DatasetComplianceRollupRow): boolean {
|
||||
const tags = get(this, 'fieldChangeSet');
|
||||
const { length } = fieldTagsRequiringReview(get(this, 'complianceDataTypes'))(get(this, 'identifierField'))(tags);
|
||||
const { fieldChangeSet: tags, suggestionConfidenceThreshold } = getProperties(this, [
|
||||
'fieldChangeSet',
|
||||
'suggestionConfidenceThreshold'
|
||||
]);
|
||||
const { length } = fieldTagsRequiringReview(get(this, 'complianceDataTypes'), {
|
||||
checkSuggestions: true,
|
||||
suggestionConfidenceThreshold
|
||||
})(get(this, 'identifierField'))(tags);
|
||||
|
||||
return !!length || tagsHaveNoneAndNotNoneType(tags);
|
||||
}
|
||||
@ -172,10 +187,12 @@ export default class DatasetComplianceRollupRow extends Component.extend({
|
||||
* @type {(ComputedProperty<{ identifierType: ComplianceFieldIdValue; logicalType: string; confidence: number } | void>)}
|
||||
* @memberof DatasetComplianceRollupRow
|
||||
*/
|
||||
suggestion = computed('fieldProps.suggestion', 'suggestionAuthority', function(
|
||||
suggestion = computed('fieldProps.suggestion', 'suggestionAuthority', 'suggestionConfidenceThreshold', function(
|
||||
this: DatasetComplianceRollupRow
|
||||
): ISuggestedFieldTypeValues | void {
|
||||
return getTagSuggestions(getWithDefault(this, 'fieldProps', <IComplianceChangeSet>{}));
|
||||
const fieldProps = getWithDefault(this, 'fieldProps', <IComplianceChangeSet>{});
|
||||
|
||||
return getTagSuggestions({ suggestionConfidenceThreshold: get(this, 'suggestionConfidenceThreshold') })(fieldProps);
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@ -35,7 +35,8 @@ import {
|
||||
singleTagsInChangeSet,
|
||||
tagsForIdentifierField,
|
||||
overrideTagReadonly,
|
||||
editableTags
|
||||
editableTags,
|
||||
lowQualitySuggestionConfidenceThreshold
|
||||
} from 'wherehows-web/constants';
|
||||
import { getTagsSuggestions } from 'wherehows-web/utils/datasets/compliance-suggestions';
|
||||
import { arrayMap, compact, isListUnique, iterateArrayAsync } from 'wherehows-web/utils/array';
|
||||
@ -70,12 +71,12 @@ import { notificationDialogActionFactory } from 'wherehows-web/utils/notificatio
|
||||
import validateMetadataObject, {
|
||||
complianceEntitiesTaxonomy
|
||||
} from 'wherehows-web/utils/datasets/compliance/metadata-schema';
|
||||
import { typeOf } from '@ember/utils';
|
||||
|
||||
const {
|
||||
complianceDataException,
|
||||
complianceFieldNotUnique,
|
||||
missingTypes,
|
||||
helpText,
|
||||
missingPurgePolicy,
|
||||
missingDatasetSecurityClassification
|
||||
} = compliancePolicyStrings;
|
||||
@ -104,7 +105,6 @@ export default class DatasetCompliance extends Component {
|
||||
filterBy: string;
|
||||
sortDirection: string;
|
||||
searchTerm: string;
|
||||
helpText = helpText;
|
||||
_hasBadData: boolean;
|
||||
platform: IDatasetView['platform'];
|
||||
isCompliancePolicyAvailable: boolean = false;
|
||||
@ -112,7 +112,7 @@ export default class DatasetCompliance extends Component {
|
||||
complianceInfo: undefined | IComplianceInfo;
|
||||
|
||||
/**
|
||||
* Lists the compliance entities that are entered via the advanced edititing interface
|
||||
* Lists the compliance entities that are entered via the advanced editing interface
|
||||
* @type {Pick<IComplianceInfo, 'complianceEntities'>}
|
||||
* @memberof DatasetCompliance
|
||||
*/
|
||||
@ -135,9 +135,17 @@ export default class DatasetCompliance extends Component {
|
||||
/**
|
||||
* Flag indicating the current compliance policy edit-view mode
|
||||
* @type {boolean}
|
||||
* @memberof DatasetCompliance
|
||||
*/
|
||||
showGuidedComplianceEditMode: boolean = true;
|
||||
|
||||
/**
|
||||
* Confidence percentage number used to filter high quality suggestions versus lower quality
|
||||
* @type {number}
|
||||
* @memberof DatasetCompliance
|
||||
*/
|
||||
suggestionConfidenceThreshold: number;
|
||||
|
||||
/**
|
||||
* Formatted JSON string representing the compliance entities for this dataset
|
||||
* @type {ComputedProperty<string>}
|
||||
@ -146,7 +154,6 @@ export default class DatasetCompliance extends Component {
|
||||
this: DatasetCompliance
|
||||
): string {
|
||||
const entityAttrs = ['identifierField', 'identifierType', 'logicalType', 'nonOwner', 'valuePattern', 'readonly'];
|
||||
//@ts-ignore property access path using dot notation limitation
|
||||
const entityMap: ISchemaFieldsToPolicy = get(this, 'columnIdFieldsToCurrentPrivacyPolicy');
|
||||
const entitiesWithModifiableKeys = arrayMap((tag: IComplianceEntityWithMetadata) => pick(tag, entityAttrs))(
|
||||
(<Array<IComplianceEntityWithMetadata>>[]).concat(...Object.values(entityMap))
|
||||
@ -334,6 +341,8 @@ export default class DatasetCompliance extends Component {
|
||||
this.searchTerm || set(this, 'searchTerm', '');
|
||||
this.schemaFieldNamesMappedToDataTypes || (this.schemaFieldNamesMappedToDataTypes = []);
|
||||
this.complianceDataTypes || (this.complianceDataTypes = []);
|
||||
typeOf(this.suggestionConfidenceThreshold) === 'number' ||
|
||||
set(this, 'suggestionConfidenceThreshold', lowQualitySuggestionConfidenceThreshold);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -739,16 +748,26 @@ export default class DatasetCompliance extends Component {
|
||||
'columnIdFieldsToCurrentPrivacyPolicy',
|
||||
'complianceDataTypes',
|
||||
'identifierFieldToSuggestion',
|
||||
'suggestionConfidenceThreshold',
|
||||
function(this: DatasetCompliance): Array<IComplianceChangeSet> {
|
||||
// schemaFieldNamesMappedToDataTypes is a dependency for CP columnIdFieldsToCurrentPrivacyPolicy, so no need to dep on that directly
|
||||
const changeSet = mergeComplianceEntitiesWithSuggestions(
|
||||
get(this, 'columnIdFieldsToCurrentPrivacyPolicy'),
|
||||
get(this, 'identifierFieldToSuggestion')
|
||||
);
|
||||
const suggestionThreshold = get(this, 'suggestionConfidenceThreshold');
|
||||
|
||||
// pass current changeSet state to parent handlers
|
||||
run(() => next(this, 'notifyHandlerOfSuggestions', changeSet));
|
||||
run(() => next(this, 'notifyHandlerOfFieldsRequiringReview', get(this, 'complianceDataTypes'), changeSet));
|
||||
run(() => next(this, 'notifyHandlerOfSuggestions', suggestionThreshold, changeSet));
|
||||
run(() =>
|
||||
next(
|
||||
this,
|
||||
'notifyHandlerOfFieldsRequiringReview',
|
||||
suggestionThreshold,
|
||||
get(this, 'complianceDataTypes'),
|
||||
changeSet
|
||||
)
|
||||
);
|
||||
|
||||
return changeSet;
|
||||
}
|
||||
@ -764,14 +783,16 @@ export default class DatasetCompliance extends Component {
|
||||
'fieldReviewOption',
|
||||
'compliancePolicyChangeSet',
|
||||
'complianceDataTypes',
|
||||
'suggestionConfidenceThreshold',
|
||||
function(this: DatasetCompliance): Array<IComplianceChangeSet> {
|
||||
const { compliancePolicyChangeSet: changeSet, complianceDataTypes } = getProperties(this, [
|
||||
'compliancePolicyChangeSet',
|
||||
'complianceDataTypes'
|
||||
]);
|
||||
const {
|
||||
compliancePolicyChangeSet: changeSet,
|
||||
complianceDataTypes,
|
||||
suggestionConfidenceThreshold
|
||||
} = getProperties(this, ['compliancePolicyChangeSet', 'complianceDataTypes', 'suggestionConfidenceThreshold']);
|
||||
|
||||
return get(this, 'fieldReviewOption') === 'showReview'
|
||||
? tagsRequiringReview(complianceDataTypes)(changeSet)
|
||||
? tagsRequiringReview(complianceDataTypes, { checkSuggestions: true, suggestionConfidenceThreshold })(changeSet)
|
||||
: changeSet;
|
||||
}
|
||||
);
|
||||
@ -788,9 +809,10 @@ export default class DatasetCompliance extends Component {
|
||||
changeSetReviewWithoutSuggestionCheck = computed('changeSetReview', function(
|
||||
this: DatasetCompliance
|
||||
): Array<IComplianceChangeSet> {
|
||||
return tagsRequiringReview(get(this, 'complianceDataTypes'), { checkSuggestions: false })(
|
||||
get(this, 'changeSetReview')
|
||||
);
|
||||
return tagsRequiringReview(get(this, 'complianceDataTypes'), {
|
||||
checkSuggestions: false,
|
||||
suggestionConfidenceThreshold: 0 // irrelevant value set to 0 since checkSuggestions flag is false above
|
||||
})(get(this, 'changeSetReview'));
|
||||
});
|
||||
|
||||
/**
|
||||
@ -801,8 +823,17 @@ export default class DatasetCompliance extends Component {
|
||||
changeSetReview = computed(
|
||||
`compliancePolicyChangeSet.@each.{${changeSetReviewableAttributeTriggers}}`,
|
||||
'complianceDataTypes',
|
||||
'suggestionConfidenceThreshold',
|
||||
function(this: DatasetCompliance): Array<IComplianceChangeSet> {
|
||||
return tagsRequiringReview(get(this, 'complianceDataTypes'))(get(this, 'compliancePolicyChangeSet'));
|
||||
const { suggestionConfidenceThreshold, compliancePolicyChangeSet } = getProperties(this, [
|
||||
'suggestionConfidenceThreshold',
|
||||
'compliancePolicyChangeSet'
|
||||
]);
|
||||
|
||||
return tagsRequiringReview(get(this, 'complianceDataTypes'), {
|
||||
checkSuggestions: true,
|
||||
suggestionConfidenceThreshold
|
||||
})(compliancePolicyChangeSet);
|
||||
}
|
||||
);
|
||||
|
||||
@ -866,19 +897,25 @@ export default class DatasetCompliance extends Component {
|
||||
|
||||
/**
|
||||
* Invokes external action with flag indicating that at least 1 suggestion exists for a field in the changeSet
|
||||
* @param {number} suggestionConfidenceThreshold confidence threshold for filtering out higher quality suggestions
|
||||
* @param {Array<IComplianceChangeSet>} changeSet
|
||||
*/
|
||||
notifyHandlerOfSuggestions = (changeSet: Array<IComplianceChangeSet>): void => {
|
||||
const hasChangeSetSuggestions = !!compact(getTagsSuggestions(changeSet)).length;
|
||||
notifyHandlerOfSuggestions = (
|
||||
suggestionConfidenceThreshold: number,
|
||||
changeSet: Array<IComplianceChangeSet>
|
||||
): void => {
|
||||
const hasChangeSetSuggestions = !!compact(getTagsSuggestions({ suggestionConfidenceThreshold })(changeSet)).length;
|
||||
this.notifyOnChangeSetSuggestions(hasChangeSetSuggestions);
|
||||
};
|
||||
|
||||
/**
|
||||
* Invokes external action with flag indicating that a field in the tags requires user review
|
||||
* @param {number} suggestionConfidenceThreshold confidence threshold for filtering out higher quality suggestions
|
||||
* @param {Array<IComplianceDataType>} complianceDataTypes
|
||||
* @param {Array<IComplianceChangeSet>} tags
|
||||
*/
|
||||
notifyHandlerOfFieldsRequiringReview = (
|
||||
suggestionConfidenceThreshold: number,
|
||||
complianceDataTypes: Array<IComplianceDataType>,
|
||||
tags: Array<IComplianceChangeSet>
|
||||
): void => {
|
||||
@ -886,7 +923,10 @@ export default class DatasetCompliance extends Component {
|
||||
assert('expected complianceDataTypes to be of type `array`', Array.isArray(complianceDataTypes));
|
||||
assert('expected tags to be of type `array`', Array.isArray(tags));
|
||||
|
||||
const hasChangeSetDrift = !!tagsRequiringReview(complianceDataTypes)(tags).length;
|
||||
const hasChangeSetDrift = !!tagsRequiringReview(complianceDataTypes, {
|
||||
checkSuggestions: true,
|
||||
suggestionConfidenceThreshold
|
||||
})(tags).length;
|
||||
|
||||
this.notifyOnChangeSetRequiresReview(hasChangeSetDrift);
|
||||
};
|
||||
|
||||
@ -21,12 +21,20 @@ import {
|
||||
import { columnDataTypesAndFieldNames } from 'wherehows-web/utils/api/datasets/columns';
|
||||
import { readDatasetSchemaByUrn } from 'wherehows-web/utils/api/datasets/schema';
|
||||
import { readComplianceDataTypes } from 'wherehows-web/utils/api/list/compliance-datatypes';
|
||||
import { compliancePolicyStrings, removeReadonlyAttr, editableTags, SuggestionIntent } from 'wherehows-web/constants';
|
||||
import {
|
||||
compliancePolicyStrings,
|
||||
removeReadonlyAttr,
|
||||
editableTags,
|
||||
SuggestionIntent,
|
||||
lowQualitySuggestionConfidenceThreshold
|
||||
} from 'wherehows-web/constants';
|
||||
import { iterateArrayAsync } from 'wherehows-web/utils/array';
|
||||
import validateMetadataObject, {
|
||||
complianceEntitiesTaxonomy
|
||||
} from 'wherehows-web/utils/datasets/compliance/metadata-schema';
|
||||
import { notificationDialogActionFactory } from 'wherehows-web/utils/notifications/notifications';
|
||||
import Configurator from 'wherehows-web/services/configurator';
|
||||
import { typeOf } from '@ember/utils';
|
||||
|
||||
/**
|
||||
* Type alias for the response when container data items are batched
|
||||
@ -49,6 +57,7 @@ type BatchContainerDataResult = Pick<
|
||||
| 'complianceSuggestion'
|
||||
| 'schemaFieldNamesMappedToDataTypes'
|
||||
| 'schemaless'
|
||||
| 'suggestionConfidenceThreshold'
|
||||
>;
|
||||
|
||||
const { successUpdating, failedUpdating, successUploading, invalidPolicyData } = compliancePolicyStrings;
|
||||
@ -128,6 +137,13 @@ export default class DatasetComplianceContainer extends Component {
|
||||
*/
|
||||
datasetName: string = '';
|
||||
|
||||
/**
|
||||
* Confidence percentage number used to filter high quality suggestions versus lower quality
|
||||
* @type {number}
|
||||
* @memberof DatasetComplianceContainer
|
||||
*/
|
||||
suggestionConfidenceThreshold: number = lowQualitySuggestionConfidenceThreshold;
|
||||
|
||||
/**
|
||||
* The urn identifier for the dataset
|
||||
* @type {string}
|
||||
@ -177,11 +193,16 @@ export default class DatasetComplianceContainer extends Component {
|
||||
]);
|
||||
const schemaFieldNamesMappedToDataTypes = await iterateArrayAsync(columnDataTypesAndFieldNames)(columns);
|
||||
const { containingPersonalData, fromUpstream } = complianceInfo;
|
||||
let suggestionConfidenceThreshold = Configurator.getConfig('suggestionConfidenceThreshold');
|
||||
|
||||
// convert to fractional percentage if valid number is present
|
||||
typeOf(suggestionConfidenceThreshold) === 'number' &&
|
||||
(suggestionConfidenceThreshold = suggestionConfidenceThreshold / 100);
|
||||
this.notifyPiiStatus(!!containingPersonalData);
|
||||
this.onCompliancePolicyStateChange.call(this, { isNewComplianceInfo, fromUpstream: !!fromUpstream });
|
||||
|
||||
return setProperties(this, {
|
||||
suggestionConfidenceThreshold,
|
||||
isNewComplianceInfo,
|
||||
complianceInfo,
|
||||
complianceDataTypes,
|
||||
|
||||
@ -0,0 +1,32 @@
|
||||
import Component from '@ember/component';
|
||||
import { get } from '@ember/object';
|
||||
import { task, TaskInstance } from 'ember-concurrency';
|
||||
|
||||
/**
|
||||
* This is the container component for the dataset health tab. It should contain the health bar graphs and a table
|
||||
* depicting the detailed health scores. Aside from fetching the data, it also handles click interactions between
|
||||
* the graphs and the table in terms of filtering and displaying of data
|
||||
*/
|
||||
export default class DatasetHealthContainer extends Component {
|
||||
/**
|
||||
* The urn identifier for the dataset
|
||||
* @type {string}
|
||||
*/
|
||||
urn: string;
|
||||
|
||||
didInsertElement() {
|
||||
get(this, 'getContainerDataTask').perform();
|
||||
}
|
||||
|
||||
didUpdateAttrs() {
|
||||
get(this, 'getContainerDataTask').perform();
|
||||
}
|
||||
|
||||
/**
|
||||
* An async parent task to group all data tasks for this container component
|
||||
* @type {Task<TaskInstance<Promise<any>>, (a?: any) => TaskInstance<TaskInstance<Promise<any>>>>}
|
||||
*/
|
||||
getContainerDataTask = task(function*(this: DatasetHealthContainer): IterableIterator<TaskInstance<Promise<any>>> {
|
||||
// Do something in the future
|
||||
});
|
||||
}
|
||||
@ -40,11 +40,6 @@ const compliancePolicyStrings = {
|
||||
failedUpdating: 'An error occurred while saving.',
|
||||
successUploading: 'Metadata successfully updated! Please confirm and complete subsequent metadata information.',
|
||||
invalidPolicyData: 'Received policy in an unexpected format! Please check the provided attributes and try again.',
|
||||
helpText: {
|
||||
classification:
|
||||
'This security classification is from go/dht and should be good enough in most cases. ' +
|
||||
'You can optionally override it if required by house security.'
|
||||
},
|
||||
missingPurgePolicy: 'Please specify a Compliance Purge Policy',
|
||||
missingDatasetSecurityClassification: 'Please specify a security classification for this dataset.'
|
||||
};
|
||||
@ -183,10 +178,18 @@ const suggestedIdentifierTypesInList = (suggestion: ISuggestedFieldTypeValues |
|
||||
* @param {SchemaFieldToSuggestedValue} suggestion
|
||||
* @param {SuggestionIntent} suggestionAuthority
|
||||
* @param {ComplianceFieldIdValue | NonIdLogicalType | null} identifierType
|
||||
* @param {number} suggestionConfidenceThreshold confidence threshold for filtering out higher quality suggestions
|
||||
* @return {boolean}
|
||||
*/
|
||||
const tagSuggestionNeedsReview = ({ suggestion, suggestionAuthority, identifierType }: IComplianceChangeSet): boolean =>
|
||||
suggestion && suggestion.identifierType !== identifierType && isHighConfidenceSuggestion(suggestion)
|
||||
const tagSuggestionNeedsReview = ({
|
||||
suggestion,
|
||||
suggestionAuthority,
|
||||
identifierType,
|
||||
suggestionConfidenceThreshold
|
||||
}: IComplianceChangeSet & { suggestionConfidenceThreshold: number }): boolean =>
|
||||
suggestion &&
|
||||
suggestion.identifierType !== identifierType &&
|
||||
isHighConfidenceSuggestion(suggestion, suggestionConfidenceThreshold)
|
||||
? !suggestionAuthority
|
||||
: false;
|
||||
|
||||
@ -223,14 +226,14 @@ const tagValuePatternNeedsReview = ({ valuePattern }: IComplianceChangeSet): boo
|
||||
* checking steps
|
||||
* @return {(tag: IComplianceChangeSet) => boolean}
|
||||
*/
|
||||
const tagNeedsReview = (complianceDataTypes: Array<IComplianceDataType>, options?: IComplianceTagReviewOptions) =>
|
||||
const tagNeedsReview = (complianceDataTypes: Array<IComplianceDataType>, options: IComplianceTagReviewOptions) =>
|
||||
/**
|
||||
* Checks if a compliance tag needs to be reviewed against a set of rules
|
||||
* @param {IComplianceChangeSet} tag
|
||||
* @return {boolean}
|
||||
*/
|
||||
(tag: IComplianceChangeSet): boolean => {
|
||||
const { checkSuggestions } = options || { checkSuggestions: true };
|
||||
const { checkSuggestions, suggestionConfidenceThreshold } = options;
|
||||
const { isDirty, privacyPolicyExists, identifierType, logicalType } = tag;
|
||||
let isReviewRequired = false;
|
||||
|
||||
@ -241,7 +244,7 @@ const tagNeedsReview = (complianceDataTypes: Array<IComplianceDataType>, options
|
||||
|
||||
// Check that a hi confidence suggestion exists and the identifierType does not match the change set item
|
||||
if (checkSuggestions) {
|
||||
isReviewRequired = isReviewRequired || tagSuggestionNeedsReview(tag);
|
||||
isReviewRequired = isReviewRequired || tagSuggestionNeedsReview({ ...tag, suggestionConfidenceThreshold });
|
||||
}
|
||||
|
||||
// Ensure that tag has a logical type and nonOwner flag is set when tag is of id type
|
||||
@ -391,17 +394,20 @@ const singleTagsInChangeSet = (
|
||||
* @param {IComplianceTagReviewOptions} options
|
||||
* @return {(array: Array<IComplianceChangeSet>) => Array<IComplianceChangeSet>}
|
||||
*/
|
||||
const tagsRequiringReview = (complianceDataTypes: Array<IComplianceDataType>, options?: IComplianceTagReviewOptions) =>
|
||||
const tagsRequiringReview = (complianceDataTypes: Array<IComplianceDataType>, options: IComplianceTagReviewOptions) =>
|
||||
arrayFilter<IComplianceChangeSet>(tagNeedsReview(complianceDataTypes, options));
|
||||
|
||||
/**
|
||||
* Lists the tags for a specific identifier field that need to be reviewed
|
||||
* @param {Array<IComplianceDataType>} complianceDataTypes
|
||||
* @param {IComplianceTagReviewOptions} options
|
||||
* @return {(identifierField: string) => (tags: Array<IComplianceChangeSet>) => Array<IComplianceChangeSet>}
|
||||
*/
|
||||
const fieldTagsRequiringReview = (complianceDataTypes: Array<IComplianceDataType>) => (identifierField: string) => (
|
||||
tags: Array<IComplianceChangeSet>
|
||||
) => tagsRequiringReview(complianceDataTypes)(tagsForIdentifierField(identifierField)(tags));
|
||||
const fieldTagsRequiringReview = (
|
||||
complianceDataTypes: Array<IComplianceDataType>,
|
||||
options: IComplianceTagReviewOptions
|
||||
) => (identifierField: string) => (tags: Array<IComplianceChangeSet>) =>
|
||||
tagsRequiringReview(complianceDataTypes, options)(tagsForIdentifierField(identifierField)(tags));
|
||||
|
||||
/**
|
||||
* Extracts a suggestion for a field from a suggestion map and merges a compliance entity with the suggestion
|
||||
|
||||
@ -87,6 +87,14 @@ export default class DatasetController extends Controller {
|
||||
*/
|
||||
shouldShowDatasetLineage: boolean;
|
||||
|
||||
/**
|
||||
* Flags the health feature for datasets, which is currently in the development stage so we should not
|
||||
* have it appear in production
|
||||
* @type {boolean}
|
||||
* @memberof DatasetController
|
||||
*/
|
||||
shouldShowDatasetHealth: boolean;
|
||||
|
||||
/**
|
||||
* Flag indicating if the dataset contains personally identifiable information
|
||||
* @type {boolean}
|
||||
|
||||
@ -95,7 +95,8 @@ export default class DatasetRoute extends Route {
|
||||
setProperties(controller, {
|
||||
isInternal: !!getConfig('isInternal'),
|
||||
jitAclAccessWhitelist: getConfig('JitAclAccessWhitelist') || [],
|
||||
shouldShowDatasetLineage: getConfig('shouldShowDatasetLineage')
|
||||
shouldShowDatasetLineage: getConfig('shouldShowDatasetLineage'),
|
||||
shouldShowDatasetHealth: getConfig('shouldShowDatasetHealth')
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -25,6 +25,7 @@
|
||||
platform=platform
|
||||
complianceInfo=complianceInfo
|
||||
complianceSuggestion=complianceSuggestion
|
||||
suggestionConfidenceThreshold=suggestionConfidenceThreshold
|
||||
isNewComplianceInfo=isNewComplianceInfo
|
||||
schemaFieldNamesMappedToDataTypes=schemaFieldNamesMappedToDataTypes
|
||||
complianceDataTypes=complianceDataTypes
|
||||
|
||||
@ -0,0 +1 @@
|
||||
Coming Soon!
|
||||
@ -136,6 +136,7 @@
|
||||
field=field
|
||||
isNewComplianceInfo=isNewComplianceInfo
|
||||
complianceDataTypes=complianceDataTypes
|
||||
suggestionConfidenceThreshold=suggestionConfidenceThreshold
|
||||
onFieldDblClick=(action "onFieldDblClick")
|
||||
onFieldTagAdded=(action "onFieldTagAdded")
|
||||
onFieldTagRemoved=(action "onFieldTagRemoved")
|
||||
@ -216,6 +217,7 @@
|
||||
sourceTag=tag
|
||||
parentHasSingleTag=row.hasSingleTag
|
||||
tagDidChange=(action "tagPropertiesUpdated")
|
||||
suggestionConfidenceThreshold=suggestionConfidenceThreshold
|
||||
complianceFieldIdDropdownOptions=complianceFieldIdDropdownOptions
|
||||
complianceDataTypes=complianceDataTypes as |tagRowComponent|
|
||||
}}
|
||||
|
||||
@ -105,6 +105,12 @@
|
||||
{{/tablist.tab}}
|
||||
{{/if}}
|
||||
|
||||
{{#if shouldShowDatasetHealth}}
|
||||
{{#tablist.tab tabIds.Health on-select=(action "tabSelectionChanged")}}
|
||||
Health
|
||||
{{/tablist.tab}}
|
||||
{{/if}}
|
||||
|
||||
{{/tabs.tablist}}
|
||||
</div>
|
||||
</div>
|
||||
@ -152,5 +158,11 @@
|
||||
{{datasets/dataset-relationships urn=encodedUrn}}
|
||||
{{/tabs.tabpanel}}
|
||||
{{/if}}
|
||||
|
||||
{{#if shouldShowDatasetHealth}}
|
||||
{{#tabs.tabpanel tabIds.Health}}
|
||||
{{datasets/containers/dataset-health urn=encodedUrn}}
|
||||
{{/tabs.tabpanel}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/ivy-tabs}}
|
||||
|
||||
@ -9,6 +9,9 @@ interface IAppConfig {
|
||||
isInternal: boolean | void;
|
||||
JitAclAccessWhitelist: Array<DatasetPlatform> | void;
|
||||
shouldShowDatasetLineage: boolean;
|
||||
shouldShowDatasetHealth: boolean;
|
||||
// confidence threshold for filtering out higher quality suggestions
|
||||
suggestionConfidenceThreshold: number;
|
||||
tracking: {
|
||||
isEnabled: boolean;
|
||||
trackers: {
|
||||
|
||||
@ -23,7 +23,10 @@ interface IDatasetComplianceActions {
|
||||
* @interface IComplianceTagReviewOptions
|
||||
*/
|
||||
interface IComplianceTagReviewOptions {
|
||||
// flag determines if suggested values are considered in tag(IComplianceChangeSet) review check
|
||||
checkSuggestions: boolean;
|
||||
// confidence threshold for filtering out higher quality suggestions
|
||||
suggestionConfidenceThreshold: number;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { identity } from 'wherehows-web/utils/helpers/functions';
|
||||
import { identity, not } from 'wherehows-web/utils/helpers/functions';
|
||||
|
||||
/**
|
||||
* Composable function that will in turn consume an item from a list an emit a result of equal or same type
|
||||
@ -22,37 +22,53 @@ const take = <T>(n: number = 0) => (list: Array<T>): Array<T> => Array.prototype
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param {(param: T) => U} predicate maps a single type T to type U
|
||||
* @return {(array: Array<T>) => Array<U>}
|
||||
*/
|
||||
const arrayMap = <T, U>(mappingFunction: (param: T) => U): ((array: Array<T>) => Array<U>) => (array = []) =>
|
||||
array.map(mappingFunction);
|
||||
const arrayMap = <T, U>(predicate: (param: T) => U): ((array: Array<T>) => Array<U>) => (array = []) =>
|
||||
array.map(predicate);
|
||||
|
||||
/**
|
||||
* Partitions an array into a tuple containing elements that meet the predicate in the zeroth index,
|
||||
* and excluded elements in the next
|
||||
* `iterate-first data-last` function
|
||||
* @template T type of source element list
|
||||
* @template U subtype of T in first partition
|
||||
* @param {(param: T) => param is U} predicate is a type guard function
|
||||
* @returns {((array: Array<T>) => [Array<U>, Array<Exclude<T, U>>])}
|
||||
*/
|
||||
const arrayPartition = <T, U extends T>(
|
||||
predicate: (param: T) => param is U
|
||||
): ((array: Array<T>) => [Array<U>, Array<Exclude<T, U>>]) => (array = []) => [
|
||||
array.filter(predicate),
|
||||
array.filter<Exclude<T, U>>((v: T): v is Exclude<T, U> => not(predicate)(v))
|
||||
];
|
||||
|
||||
/**
|
||||
* Convenience utility takes a type-safe filter function, and returns a list filtering function
|
||||
* @param {(param: T) => boolean} filtrationFunction
|
||||
* @param {(param: T) => boolean} predicate
|
||||
* @return {(array: Array<T>) => Array<T>}
|
||||
*/
|
||||
const arrayFilter = <T>(filtrationFunction: (param: T) => boolean): ((array: Array<T>) => Array<T>) => (array = []) =>
|
||||
array.filter(filtrationFunction);
|
||||
const arrayFilter = <T>(predicate: (param: T) => boolean): ((array: Array<T>) => Array<T>) => (array = []) =>
|
||||
array.filter(predicate);
|
||||
|
||||
/**
|
||||
* Type safe utility `iterate-first data-last` function for array every
|
||||
* @template T
|
||||
* @param {(param: T) => boolean} filter
|
||||
* @param {(param: T) => boolean} predicate
|
||||
* @returns {((array: Array<T>) => boolean)}
|
||||
*/
|
||||
const arrayEvery = <T>(filter: (param: T) => boolean): ((array: Array<T>) => boolean) => (array = []) =>
|
||||
array.every(filter);
|
||||
const arrayEvery = <T>(predicate: (param: T) => boolean): ((array: Array<T>) => boolean) => (array = []) =>
|
||||
array.every(predicate);
|
||||
|
||||
/**
|
||||
* Type safe utility `iterate-first data-last` function for array some
|
||||
* @template T
|
||||
* @param {(param: T) => boolean} filter
|
||||
* @param {(param: T) => boolean} predicate
|
||||
* @return {(array: Array<T>) => boolean}
|
||||
*/
|
||||
const arraySome = <T>(filter: (param: T) => boolean): ((array: Array<T>) => boolean) => (array = []) =>
|
||||
array.some(filter);
|
||||
const arraySome = <T>(predicate: (param: T) => boolean): ((array: Array<T>) => boolean) => (array = []) =>
|
||||
array.some(predicate);
|
||||
|
||||
/**
|
||||
* Composable reducer abstraction, curries a reducing iteratee and returns a reducing function that takes a list
|
||||
@ -228,9 +244,10 @@ export { Many, Iteratee };
|
||||
export {
|
||||
take,
|
||||
arrayMap,
|
||||
arrayPipe,
|
||||
arrayFilter,
|
||||
arrayReduce,
|
||||
arrayPipe,
|
||||
arrayPartition,
|
||||
isListUnique,
|
||||
compact,
|
||||
arrayEvery,
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { lowQualitySuggestionConfidenceThreshold } from 'wherehows-web/constants';
|
||||
import { arrayMap } from 'wherehows-web/utils/array';
|
||||
import { IComplianceChangeSet, ISuggestedFieldTypeValues } from 'wherehows-web/typings/app/dataset-compliance';
|
||||
|
||||
@ -6,23 +5,28 @@ import { IComplianceChangeSet, ISuggestedFieldTypeValues } from 'wherehows-web/t
|
||||
* Takes a list of suggestions with confidence values, and if the confidence is greater than
|
||||
* a low confidence threshold
|
||||
* @param {number} confidenceLevel percentage indicating how confidence the system is in the suggested value
|
||||
* @param {number} suggestionConfidenceThreshold threshold number to consider as a valid suggestion
|
||||
* @return {boolean}
|
||||
*/
|
||||
const isHighConfidenceSuggestion = ({ confidenceLevel = 0 }: { confidenceLevel: number }): boolean =>
|
||||
confidenceLevel > lowQualitySuggestionConfidenceThreshold;
|
||||
const isHighConfidenceSuggestion = (
|
||||
{ confidenceLevel = 0 }: { confidenceLevel: number },
|
||||
suggestionConfidenceThreshold: number = 0
|
||||
): boolean => confidenceLevel > suggestionConfidenceThreshold;
|
||||
|
||||
/**
|
||||
* Extracts the tag suggestion from an IComplianceChangeSet tag.
|
||||
* If a suggestionAuthority property exists on the tag, then the user has already either accepted or ignored
|
||||
* the suggestion for this tag. It's value should not be taken into account on re-renders,
|
||||
* in place, this substitutes an empty suggestion
|
||||
* @param {IComplianceChangeSet} tag
|
||||
* @return {{identifierType: IComplianceChangeSet.identifierType, logicalType: IComplianceChangeSet.logicalType, confidence: number} | void}
|
||||
* @param {number} suggestionConfidenceThreshold confidence threshold for filtering out higher quality suggestions
|
||||
* @return {(tag?: IComplianceChangeSet) => (ISuggestedFieldTypeValues | void)}
|
||||
*/
|
||||
const getTagSuggestions = (tag: IComplianceChangeSet = <IComplianceChangeSet>{}): ISuggestedFieldTypeValues | void => {
|
||||
const getTagSuggestions = ({ suggestionConfidenceThreshold }: { suggestionConfidenceThreshold: number }) => (
|
||||
tag: IComplianceChangeSet = <IComplianceChangeSet>{}
|
||||
): ISuggestedFieldTypeValues | void => {
|
||||
const { suggestion } = tag;
|
||||
|
||||
if (suggestion && isHighConfidenceSuggestion(suggestion)) {
|
||||
if (suggestion && isHighConfidenceSuggestion(suggestion, suggestionConfidenceThreshold)) {
|
||||
const { identifierType, logicalType, confidenceLevel: confidence } = suggestion;
|
||||
return { identifierType, logicalType, confidence: +(confidence * 100).toFixed(2) };
|
||||
}
|
||||
@ -30,8 +34,10 @@ const getTagSuggestions = (tag: IComplianceChangeSet = <IComplianceChangeSet>{})
|
||||
|
||||
/**
|
||||
* Gets the suggestions for a list of IComplianceChangeSet fields
|
||||
* @type {(array: Array<IComplianceChangeSet>) => Array<{identifierType: ComplianceFieldIdValue | NonIdLogicalType | null; logicalType: IdLogicalType | null; confidence: number} | void>}
|
||||
* @param {number} suggestionConfidenceThreshold
|
||||
* @return {(array: Array<IComplianceChangeSet>) => Array<ISuggestedFieldTypeValues | void>}
|
||||
*/
|
||||
const getTagsSuggestions = arrayMap(getTagSuggestions);
|
||||
const getTagsSuggestions = ({ suggestionConfidenceThreshold }: { suggestionConfidenceThreshold: number }) =>
|
||||
arrayMap(getTagSuggestions({ suggestionConfidenceThreshold }));
|
||||
|
||||
export { isHighConfidenceSuggestion, getTagSuggestions, getTagsSuggestions };
|
||||
|
||||
@ -27,7 +27,9 @@ interface IPartialOrIdentityTypeFn<T> {
|
||||
* @param {Array<K>} [droppedKeys=[]] the list of attributes on T to be dropped
|
||||
* @returns {IPartialOrIdentityTypeFn<T>}
|
||||
*/
|
||||
const fleece = <T, K extends keyof T>(droppedKeys: Array<K> = []): IPartialOrIdentityTypeFn<T> => o => {
|
||||
const fleece = <T, K extends keyof T = keyof T>(droppedKeys: Array<K> = []): IPartialOrIdentityTypeFn<T> => (
|
||||
o: T
|
||||
): Partial<T> | T => {
|
||||
const partialResult = Object.assign({}, o);
|
||||
|
||||
return droppedKeys.reduce((partial, key) => {
|
||||
|
||||
@ -21,6 +21,15 @@ module.exports = function(defaults) {
|
||||
includePolyfill: true
|
||||
},
|
||||
|
||||
emberHighCharts: {
|
||||
includedHighCharts: true,
|
||||
// Note: Since we only need highcharts, excluding the other available modules in the addon
|
||||
includeHighStock: false,
|
||||
includeHighMaps: false,
|
||||
includeHighChartsMore: false,
|
||||
includeHighCharts3D: false
|
||||
},
|
||||
|
||||
storeConfigInMeta: false,
|
||||
|
||||
SRI: {
|
||||
|
||||
@ -66,6 +66,7 @@
|
||||
"ember-export-application-global": "^2.0.0",
|
||||
"ember-fetch": "^3.4.4",
|
||||
"ember-font-awesome": "^4.0.0-rc.2",
|
||||
"ember-highcharts": "^1.0.0",
|
||||
"ember-inflector": "^2.2.0",
|
||||
"ember-load-initializers": "^1.0.0",
|
||||
"ember-math-helpers": "^2.4.0",
|
||||
@ -87,6 +88,7 @@
|
||||
"eslint-plugin-prettier": "^2.5.0",
|
||||
"eyeglass": "^1.3.0",
|
||||
"eyeglass-restyle": "^1.1.0",
|
||||
"highcharts": "^6.1.1",
|
||||
"husky": "^0.14.3",
|
||||
"ivy-tabs": "^3.1.0",
|
||||
"lint-staged": "^7.1.0",
|
||||
|
||||
@ -0,0 +1,13 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { render } from '@ember/test-helpers';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
|
||||
module('Integration | Component | datasets/containers/dataset-health', function(hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
// TODO: More meaningful tests as we continue with development
|
||||
test('it renders', async function(assert) {
|
||||
await render(hbs`{{datasets/containers/dataset-health}}`);
|
||||
assert.ok(this.element, 'Renders without errors');
|
||||
});
|
||||
});
|
||||
@ -8,7 +8,8 @@ import {
|
||||
PurgePolicy,
|
||||
initialComplianceObjectFactory,
|
||||
isRecentSuggestion,
|
||||
tagNeedsReview
|
||||
tagNeedsReview,
|
||||
lowQualitySuggestionConfidenceThreshold
|
||||
} from 'wherehows-web/constants';
|
||||
import complianceDataTypes from 'wherehows-web/mirage/fixtures/compliance-data-types';
|
||||
import { mockTimeStamps } from 'wherehows-web/tests/helpers/datasets/compliance-policy/recent-suggestions-constants';
|
||||
@ -17,6 +18,11 @@ import { hdfsUrn } from 'wherehows-web/mirage/fixtures/urn';
|
||||
|
||||
module('Unit | Constants | dataset compliance');
|
||||
|
||||
const complianceTagReviewOptions = {
|
||||
checkSuggestions: false,
|
||||
suggestionConfidenceThreshold: lowQualitySuggestionConfidenceThreshold
|
||||
};
|
||||
|
||||
test('initialComplianceObjectFactory', function(assert) {
|
||||
assert.expect(2);
|
||||
const mockUrn = hdfsUrn;
|
||||
@ -55,14 +61,20 @@ test('isRecentSuggestion correctly determines if a suggestion is recent or not',
|
||||
test('tagNeedsReview exists', function(assert) {
|
||||
assert.ok(typeof tagNeedsReview === 'function', 'tagNeedsReview is a function');
|
||||
|
||||
assert.ok(typeof tagNeedsReview([])({}) === 'boolean', 'tagNeedsReview returns a boolean');
|
||||
assert.ok(
|
||||
typeof tagNeedsReview([], complianceTagReviewOptions)({}) === 'boolean',
|
||||
'tagNeedsReview returns a boolean'
|
||||
);
|
||||
});
|
||||
|
||||
test('tagNeedsReview correctly determines if a fieldChangeSet requires review', function(assert) {
|
||||
assert.expect(mockFieldChangeSets.length);
|
||||
|
||||
mockFieldChangeSets.forEach(changeSet =>
|
||||
assert.ok(tagNeedsReview(complianceDataTypes)(changeSet) === changeSet.__requiresReview__, changeSet.__msg__)
|
||||
assert.ok(
|
||||
tagNeedsReview(complianceDataTypes, complianceTagReviewOptions)(changeSet) === changeSet.__requiresReview__,
|
||||
changeSet.__msg__
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@ -8,21 +8,30 @@ test('isHighConfidenceSuggestion correctly determines the confidence of a sugges
|
||||
let result = isHighConfidenceSuggestion({});
|
||||
assert.notOk(result, 'should be false if no arguments are supplied');
|
||||
|
||||
result = isHighConfidenceSuggestion({ confidenceLevel: lowQualitySuggestionConfidenceThreshold + 1 });
|
||||
result = isHighConfidenceSuggestion(
|
||||
{ confidenceLevel: lowQualitySuggestionConfidenceThreshold + 1 },
|
||||
lowQualitySuggestionConfidenceThreshold
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
result,
|
||||
`should be true if the confidence value is greater than ${lowQualitySuggestionConfidenceThreshold}`
|
||||
);
|
||||
|
||||
result = isHighConfidenceSuggestion({ confidenceLevel: lowQualitySuggestionConfidenceThreshold - 1 });
|
||||
result = isHighConfidenceSuggestion(
|
||||
{ confidenceLevel: lowQualitySuggestionConfidenceThreshold - 1 },
|
||||
lowQualitySuggestionConfidenceThreshold
|
||||
);
|
||||
|
||||
assert.notOk(
|
||||
result,
|
||||
`should be false if the confidence value is less than ${lowQualitySuggestionConfidenceThreshold}`
|
||||
);
|
||||
|
||||
result = isHighConfidenceSuggestion({ confidenceLevel: lowQualitySuggestionConfidenceThreshold });
|
||||
result = isHighConfidenceSuggestion(
|
||||
{ confidenceLevel: lowQualitySuggestionConfidenceThreshold },
|
||||
lowQualitySuggestionConfidenceThreshold
|
||||
);
|
||||
|
||||
assert.notOk(
|
||||
result,
|
||||
@ -42,15 +51,17 @@ test('getTagSuggestions correctly extracts suggestions from a compliance field',
|
||||
suggestionAuthority: SuggestionIntent.accept
|
||||
};
|
||||
|
||||
let result = getTagSuggestions({});
|
||||
let result = getTagSuggestions({ suggestionConfidenceThreshold: lowQualitySuggestionConfidenceThreshold })({});
|
||||
|
||||
assert.ok(typeof result === 'undefined', 'expected undefined return when the argument is an empty object');
|
||||
|
||||
result = getTagSuggestions();
|
||||
result = getTagSuggestions({ suggestionConfidenceThreshold: lowQualitySuggestionConfidenceThreshold })();
|
||||
|
||||
assert.ok(typeof result === 'undefined', 'expected undefined return when no argument is supplied');
|
||||
|
||||
result = getTagSuggestions({ suggestion: changeSetField.suggestion });
|
||||
result = getTagSuggestions({ suggestionConfidenceThreshold: lowQualitySuggestionConfidenceThreshold })({
|
||||
suggestion: changeSetField.suggestion
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
result,
|
||||
|
||||
@ -1382,6 +1382,10 @@ bootstrap-sass@^3.0.0:
|
||||
version "3.3.7"
|
||||
resolved "https://registry.yarnpkg.com/bootstrap-sass/-/bootstrap-sass-3.3.7.tgz#6596c7ab40f6637393323ab0bc80d064fc630498"
|
||||
|
||||
bootstrap@3.3.7:
|
||||
version "3.3.7"
|
||||
resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-3.3.7.tgz#5a389394549f23330875a3b150656574f8a9eb71"
|
||||
|
||||
bower-config@^1.3.0:
|
||||
version "1.4.1"
|
||||
resolved "https://registry.yarnpkg.com/bower-config/-/bower-config-1.4.1.tgz#85fd9df367c2b8dbbd0caa4c5f2bad40cd84c2cc"
|
||||
@ -1742,7 +1746,7 @@ broccoli-lint-eslint@^4.2.1:
|
||||
lodash.defaultsdeep "^4.6.0"
|
||||
md5-hex "^2.0.0"
|
||||
|
||||
broccoli-merge-trees@^1.0.0, broccoli-merge-trees@^1.1.0, broccoli-merge-trees@^1.1.1, broccoli-merge-trees@^1.1.4:
|
||||
broccoli-merge-trees@^1.0.0, broccoli-merge-trees@^1.1.0, broccoli-merge-trees@^1.1.1, broccoli-merge-trees@^1.1.4, broccoli-merge-trees@^1.2.0:
|
||||
version "1.2.4"
|
||||
resolved "https://registry.yarnpkg.com/broccoli-merge-trees/-/broccoli-merge-trees-1.2.4.tgz#a001519bb5067f06589d91afa2942445a2d0fdb5"
|
||||
dependencies:
|
||||
@ -3516,6 +3520,16 @@ ember-hash-helper-polyfill@^0.1.1:
|
||||
ember-cli-babel "^5.1.7"
|
||||
ember-cli-version-checker "^1.2.0"
|
||||
|
||||
ember-highcharts@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ember-highcharts/-/ember-highcharts-1.0.0.tgz#d412af4d1f2f55e1cae0174c353852fd98b5bad9"
|
||||
dependencies:
|
||||
bootstrap "3.3.7"
|
||||
broccoli-funnel "^2.0.1"
|
||||
broccoli-merge-trees "^1.2.0"
|
||||
ember-cli-babel "^6.6.0"
|
||||
ember-cli-htmlbars "^2.0.1"
|
||||
|
||||
ember-ignore-children-helper@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/ember-ignore-children-helper/-/ember-ignore-children-helper-1.0.1.tgz#f7c4aa17afb9c5685e1d4dcdb61c7b138ca7cdc3"
|
||||
@ -5170,6 +5184,10 @@ heimdalljs@^0.2.0, heimdalljs@^0.2.1, heimdalljs@^0.2.3, heimdalljs@^0.2.5:
|
||||
dependencies:
|
||||
rsvp "~3.2.1"
|
||||
|
||||
highcharts@^6.1.1:
|
||||
version "6.1.1"
|
||||
resolved "https://registry.yarnpkg.com/highcharts/-/highcharts-6.1.1.tgz#49dc34f5e963744ecd7eb87603b6cdaf8304c13a"
|
||||
|
||||
hoek@2.x.x:
|
||||
version "2.16.3"
|
||||
resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user