diff --git a/wherehows-web/app/components/dataset-compliance.js b/wherehows-web/app/components/dataset-compliance.js index fe71330b16..1026e18ad2 100644 --- a/wherehows-web/app/components/dataset-compliance.js +++ b/wherehows-web/app/components/dataset-compliance.js @@ -1,7 +1,7 @@ import Ember from 'ember'; import isTrackingHeaderField from 'wherehows-web/utils/validators/tracking-headers'; import { - classifiers, + securityClassificationDropdownOptions, DatasetClassifiers, fieldIdentifierTypes, fieldIdentifierOptions, @@ -14,7 +14,7 @@ import { logicalTypesForGeneric, hasPredefinedFieldFormat, getDefaultLogicalType, - complianceSteps, + getComplianceSteps, hiddenTrackingFields, isExempt } from 'wherehows-web/constants'; @@ -53,13 +53,6 @@ const { invalidPolicyData } = compliancePolicyStrings; -/** - * Takes a string, returns a formatted string. Niche , single use case - * for now, so no need to make into a helper - * @param {String} string - */ -const formatAsCapitalizedStringWithSpaces = string => string.replace(/[A-Z]/g, match => ` ${match}`).capitalize(); - /** * List of non Id field data type classifications * @type {Array} @@ -109,13 +102,20 @@ export default Component.extend({ * @type {number} */ editStepIndex: initialStepIndex, + /** * Converts the hash of complianceSteps to a list of steps - * @type {Array<{}>} + * @type {ComputedProperty>} */ - editSteps: Object.keys(complianceSteps) - .sort() - .map(key => complianceSteps[key]), + editSteps: computed('schemaless', function() { + const hasSchema = !getWithDefault(this, 'schemaless', false); + const steps = getComplianceSteps(hasSchema); + + // Ensure correct step ordering + return Object.keys(steps) + .sort() + .map(key => steps[key]); + }), /** * Handles the transition between steps in the compliance edit wizard @@ -136,9 +136,9 @@ export default Component.extend({ let lastIndex = initialStepIndex; return function() { - const currentIndex = get(this, 'editStepIndex'); + const { editStepIndex: currentIndex, editSteps } = getProperties(this, ['editStepIndex', 'editSteps']); // the current step in the edit sequence - const editStep = this.editSteps[currentIndex] || {}; + const editStep = editSteps[currentIndex] || {}; const { name } = editStep; if (name) { @@ -156,6 +156,7 @@ export default Component.extend({ if (typeof nextAction === 'function') { return (previousAction = nextAction); } + // otherwise clear the previous action previousAction = noop; }) .catch(() => { @@ -228,9 +229,13 @@ export default Component.extend({ didReceiveAttrs() { this._super(...Array.from(arguments)); - this.resetEdit(); // Perform validation step on the received component attributes this.validateAttrs(); + + // Set the current step to first edit step if compliance policy is new / doesn't exist + if (get(this, 'isNewComplianceInfo')) { + this.updateStep(0); + } }, /** @@ -302,13 +307,6 @@ export default Component.extend({ this.disableDomCloaking(); }, - /** - * Resets the editable state of the component, dependent on `isNewComplianceInfo` flag - */ - resetEdit() { - return get(this, 'isNewComplianceInfo') ? this.updateStep(0) : this.updateStep(initialStepIndex); - }, - /** * Ensure that props received from on this component * are valid, otherwise flag @@ -331,11 +329,8 @@ export default Component.extend({ // Map generic logical type to options consumable in DOM genericLogicalTypes: logicalTypesForGeneric, - // Map classifiers to options better consumed in DOM - classifiers: ['', ...classifiers.sort()].map(value => ({ - value, - label: value ? formatAsCapitalizedStringWithSpaces(value) : '...' - })), + // Map of classifiers options for drop down + classifiers: securityClassificationDropdownOptions, /** * @type {Boolean} cached boolean flag indicating that fields do contain a `kafka type` @@ -1049,6 +1044,25 @@ export default Component.extend({ return set(this, 'complianceInfo.complianceType', purgePolicy); }, + /** + * Updates the policy flag indicating that this dataset contains personal data + * @param {boolean} containsPersonalData + * @returns boolean + */ + onDatasetLevelPolicyChange(containsPersonalData) { + // directly mutate the attribute on the complianceInfo object + return set(this, 'complianceInfo.containingPersonalData', containsPersonalData); + }, + + /** + * Updates the confidentiality flag on the dataset compliance + * @param {null | Classification} [securityClassification=null] + * @returns null | Classification + */ + onDatasetSecurityClassificationChange(securityClassification = null) { + return set(this, 'complianceInfo.confidentiality', securityClassification); + }, + /** * If all validity checks are passed, invoke onSave action on controller */ diff --git a/wherehows-web/app/components/datasets/schemaless-tagging.ts b/wherehows-web/app/components/datasets/schemaless-tagging.ts new file mode 100644 index 0000000000..c1d70ef330 --- /dev/null +++ b/wherehows-web/app/components/datasets/schemaless-tagging.ts @@ -0,0 +1,74 @@ +import Component from '@ember/component'; +import { get } from '@ember/object'; +import { + Classification, + ISecurityClassificationOption, + securityClassificationDropdownOptions +} from 'wherehows-web/constants'; + +type NullOrClassification = null | Classification; + +export default class SchemalessTagging extends Component { + classNames = ['schemaless-tagging']; + + /** + * Interface for parent supplied onPersonalDataChange action + * @memberof SchemalessTagging + */ + onPersonalDataChange: (containsPersonalData: boolean) => boolean; + + /** + * Interface for parent supplied onClassificationChange action + * @memberof SchemalessTagging + */ + onClassificationChange: (securityClassification: NullOrClassification) => NullOrClassification; + + /** + * Flag indicating that the dataset contains personally identifiable data + * @type {boolean} + * @memberof SchemalessTagging + */ + containsPersonalData: boolean; + + /** + * List of drop down options for classifying the dataset + * @type {Array} + * @memberof SchemalessTagging + */ + classifiers: Array = securityClassificationDropdownOptions; + + /** + * Flag indicating if this component should be in edit mode or readonly + * @type {boolean} + * @memberof SchemalessTagging + */ + isEditable: boolean; + + /** + * The current dataset classification value + * @type {(null | Classification)} + * @memberof SchemalessTagging + */ + classification: null | Classification; + + actions = { + /** + * Invokes the closure action onPersonaDataChange when the flag is toggled + * @param {boolean} containsPersonalDataTag flag indicating that the dataset contains personal data + * @returns boolean + */ + onPersonalDataToggle(this: SchemalessTagging, containsPersonalDataTag: boolean) { + return get(this, 'onPersonalDataChange')(containsPersonalDataTag); + }, + + /** + * Updates the dataset security classification via the closure action onClassificationChange + * @param {ISecurityClassificationOption} { value } security Classification value for the dataset + * @returns null | Classification + */ + onSecurityClassificationChange(this: SchemalessTagging, { value }: ISecurityClassificationOption) { + const securityClassification = value || null; + return get(this, 'onClassificationChange')(securityClassification); + } + }; +} diff --git a/wherehows-web/app/constants/dataset-compliance.ts b/wherehows-web/app/constants/dataset-compliance.ts index 9b90f70bb8..6fe3d7c5b8 100644 --- a/wherehows-web/app/constants/dataset-compliance.ts +++ b/wherehows-web/app/constants/dataset-compliance.ts @@ -81,4 +81,22 @@ const complianceSteps = { } }; -export { compliancePolicyStrings, fieldIdentifierOptions, complianceSteps, hiddenTrackingFields }; +/** + * Takes a map of dataset options and constructs the relevant compliance edit wizard steps to build the wizard flow + * @param {{ hasSchema: boolean }} [{ hasSchema }={ hasSchema: true }] flag indicating if the dataset is schema-less + * @returns {([x: number]: {name: string})} + */ +const getComplianceSteps = ( + { hasSchema }: { hasSchema: boolean } = { hasSchema: true } +): { [x: number]: { name: string } } => { + // Step to tag dataset with PII data, this is at the dataset level for schema-less datasets + const piiTaggingStep = { 0: { name: 'editDatasetLevelCompliancePolicy' } }; + + if (!hasSchema) { + return { ...complianceSteps, ...piiTaggingStep }; + } + + return complianceSteps; +}; + +export { compliancePolicyStrings, fieldIdentifierOptions, complianceSteps, hiddenTrackingFields, getComplianceSteps }; diff --git a/wherehows-web/app/constants/metadata-acquisition.ts b/wherehows-web/app/constants/metadata-acquisition.ts index f2677aa62f..0e3770aa18 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 { capitalize } from '@ember/string'; import { Classification, nonIdFieldLogicalTypes, @@ -11,6 +12,16 @@ import { FieldIdValues } from 'wherehows-web/constants/datasets/compliance'; +/** + * Defines the interface for an each security classification dropdown option + * @export + * @interface ISecurityClassificationOption + */ +export interface ISecurityClassificationOption { + value: '' | Classification; + label: string; +} + /** * Length of time between suggestion modification time and last modified time for the compliance policy * If a policy has been updated within the range of this window then it is considered as stale / or @@ -52,7 +63,7 @@ const nonIdFieldDataTypeClassification: { [K: string]: Classification } = generi /** * A merge of id and non id field type security classifications - * @type {[K: string] : Classification} + * @type {([k: string]: Classification)} */ const defaultFieldDataTypeClassification = { ...idFieldDataTypeClassification, ...nonIdFieldDataTypeClassification }; @@ -64,6 +75,26 @@ const classifiers = Object.values(defaultFieldDataTypeClassification).filter( (classifier, index, iter) => iter.indexOf(classifier) === index ); +/** + * Takes a string, returns a formatted string. Niche , single use case + * for now, so no need to make into a helper + * @param {string} string + */ +const formatAsCapitalizedStringWithSpaces = (string: string) => + capitalize(string.replace(/[A-Z]/g, match => ` ${match}`)); + +/** + * A derived list of security classification options from classifiers list, including an empty string option and value + * @type {Array} + */ +const securityClassificationDropdownOptions: Array = [ + '', + ...classifiers.sort() +].map((value: '' | Classification) => ({ + value, + label: value ? formatAsCapitalizedStringWithSpaces(value) : '...' +})); + /** * Checks if the identifierType is a mixed Id * @param {string} identifierType @@ -101,7 +132,7 @@ const getDefaultLogicalType = (identifierType: string): string | void => { /** * Returns a list of logicalType mappings for displaying its value and a label by logicalType * @param {('id' | 'generic')} logicalType - * @returns {Array<{value: NonIdLogicalType | IdLogicalType; label: string;}>} + * @returns {(Array<{ value: NonIdLogicalType | IdLogicalType; label: string }>)} */ const logicalTypeValueLabel = (logicalType: 'id' | 'generic') => { const logicalTypes: Array = { @@ -129,13 +160,13 @@ const logicalTypeValueLabel = (logicalType: 'id' | 'generic') => { /** * Map logicalTypes to options consumable by DOM - * @returns {Array<{value: IdLogicalType; label: string;}>} + * @returns {(Array<{value: IdLogicalType; label: string;}>)} */ const logicalTypesForIds = logicalTypeValueLabel('id'); /** * Map generic logical type to options consumable in DOM - * @returns {Array<{value: NonIdLogicalType; label: string;}>} + * @returns {(Array<{value: NonIdLogicalType; label: string;}>)} */ const logicalTypesForGeneric = logicalTypeValueLabel('generic'); @@ -155,7 +186,8 @@ const fieldIdentifierTypeValues: Array = Object.values(FieldIdVal export { defaultFieldDataTypeClassification, - classifiers, + securityClassificationDropdownOptions, + formatAsCapitalizedStringWithSpaces, fieldIdentifierTypeIds, fieldIdentifierTypeValues, isMixedId, diff --git a/wherehows-web/app/routes/datasets/dataset.js b/wherehows-web/app/routes/datasets/dataset.js index 2ce5e4a891..c206884a3e 100644 --- a/wherehows-web/app/routes/datasets/dataset.js +++ b/wherehows-web/app/routes/datasets/dataset.js @@ -108,7 +108,7 @@ export default Route.extend({ let properties; const [ - columns, + { schemaless, columns }, compliance, complianceSuggestion, datasetComments, @@ -140,6 +140,7 @@ export default Route.extend({ isNewComplianceInfo, complianceSuggestion, datasetComments, + schemaless, schemas, isInternal, datasetView, diff --git a/wherehows-web/app/styles/base/_helpers.scss b/wherehows-web/app/styles/base/_helpers.scss index 6adad32587..e04b8bf56b 100644 --- a/wherehows-web/app/styles/base/_helpers.scss +++ b/wherehows-web/app/styles/base/_helpers.scss @@ -52,3 +52,8 @@ .hidden { display: none; } + +/// Adds a visual indicator for text that have an associated help text +.define-text { + border-bottom: 1px dashed #999; +} diff --git a/wherehows-web/app/styles/components/_all.scss b/wherehows-web/app/styles/components/_all.scss index 71e44f60e7..40b2e79dab 100644 --- a/wherehows-web/app/styles/components/_all.scss +++ b/wherehows-web/app/styles/components/_all.scss @@ -15,6 +15,8 @@ @import 'dataset-property/all'; @import 'user-lookup/all'; @import 'pendulum-ellipsis-animation/all'; +@import 'toggle-switch/all'; +@import 'schemaless-tagging/all'; @import 'nacho/nacho-button'; @import 'nacho/nacho-global-search'; diff --git a/wherehows-web/app/styles/components/dataset-compliance/_compliance-prompts.scss b/wherehows-web/app/styles/components/dataset-compliance/_compliance-prompts.scss index 3e878f4830..2d19a6ddc2 100644 --- a/wherehows-web/app/styles/components/dataset-compliance/_compliance-prompts.scss +++ b/wherehows-web/app/styles/components/dataset-compliance/_compliance-prompts.scss @@ -2,7 +2,7 @@ * Prompt for privacy compliance */ .metadata-prompt { - margin: 30px 0 20px; + margin: 40px 0 10px; /** * Overrides default styles diff --git a/wherehows-web/app/styles/components/schemaless-tagging/_all.scss b/wherehows-web/app/styles/components/schemaless-tagging/_all.scss new file mode 100644 index 0000000000..504fdd856f --- /dev/null +++ b/wherehows-web/app/styles/components/schemaless-tagging/_all.scss @@ -0,0 +1 @@ +@import 'schemaless-tagging'; diff --git a/wherehows-web/app/styles/components/schemaless-tagging/_schemaless-tagging.scss b/wherehows-web/app/styles/components/schemaless-tagging/_schemaless-tagging.scss new file mode 100644 index 0000000000..a2effb8be2 --- /dev/null +++ b/wherehows-web/app/styles/components/schemaless-tagging/_schemaless-tagging.scss @@ -0,0 +1,15 @@ +.schemaless-tagging { + &__tag { + display: flex; + align-items: center; + justify-content: space-between; + } + + &__prompt { + line-height: 1.5; + } + + &__input { + margin-right: 40%; + } +} diff --git a/wherehows-web/app/styles/components/toggle-switch/_all.scss b/wherehows-web/app/styles/components/toggle-switch/_all.scss new file mode 100644 index 0000000000..ccf860bde6 --- /dev/null +++ b/wherehows-web/app/styles/components/toggle-switch/_all.scss @@ -0,0 +1 @@ +@import 'toggle-switch'; diff --git a/wherehows-web/app/styles/components/toggle-switch/_toggle-switch.scss b/wherehows-web/app/styles/components/toggle-switch/_toggle-switch.scss new file mode 100644 index 0000000000..8f4e8208dd --- /dev/null +++ b/wherehows-web/app/styles/components/toggle-switch/_toggle-switch.scss @@ -0,0 +1,58 @@ +.toggle-switch { + display: none; + + + .toggle-button { + outline: 0; + display: block; + width: 4em; + height: 2em; + position: relative; + cursor: pointer; + user-select: none; + margin: 0; + + &::selection { + background: none; + } + + &::after, + &::before { + position: relative; + display: block; + content: ''; + width: 50%; + height: 100%; + } + + &::after { + left: 0; + } + + &::before { + display: none; + } + } + + &:checked + .toggle-button::after { + left: 50%; + } + + &--light { + + .toggle-button { + background: #f0f0f0; + border-radius: 2em; + padding: 2px; + transition: all 0.4s ease; + + &::after { + border-radius: 50%; + background: set-color(white, base); + transition: all 0.2s ease; + } + } + + &:checked + .toggle-button { + background: set-color(green, green5); + } + } +} diff --git a/wherehows-web/app/templates/components/dataset-compliance.hbs b/wherehows-web/app/templates/components/dataset-compliance.hbs index 7c716f78fc..68b591e2d9 100644 --- a/wherehows-web/app/templates/components/dataset-compliance.hbs +++ b/wherehows-web/app/templates/components/dataset-compliance.hbs @@ -90,9 +90,11 @@ {{/if}} - {{#if (and (eq editStepIndex 0) (not _hasBadData))}} - {{json-upload receiveJsonFile=(action "onComplianceJsonUpload") class="secondary-actions__action"}} - {{/if}} + {{#unless schemaless}} + {{#if (and (eq editStepIndex 0) (not _hasBadData))}} + {{json-upload receiveJsonFile=(action "onComplianceJsonUpload") class="secondary-actions__action"}} + {{/if}} + {{/unless}} {{#if (or isReadOnly (eq editStepIndex 2))}} {{partial "datasets/dataset-compliance/dataset-classification"}} @@ -108,10 +110,25 @@ }} {{/if}} - {{#if (or isReadOnly (eq editStepIndex 0))}} - {{partial "datasets/dataset-compliance/dataset-compliance-entities"}} - {{/if}} + {{#if schemaless}} + {{#if (or isReadOnly (eq editStepIndex 0))}} + {{datasets/schemaless-tagging + isEditable=(not isReadOnly) + classification=(readonly complianceInfo.confidentiality) + containsPersonalData=(readonly complianceInfo.containingPersonalData) + onClassificationChange=(action "onDatasetSecurityClassificationChange") + onPersonalDataChange=(action "onDatasetLevelPolicyChange") + }} + {{/if}} + + {{else}} + + {{#if (or isReadOnly (eq editStepIndex 0))}} + {{partial "datasets/dataset-compliance/dataset-compliance-entities"}} + {{/if}} + + {{/if}} {{yield}} diff --git a/wherehows-web/app/templates/components/datasets/schemaless-tagging.hbs b/wherehows-web/app/templates/components/datasets/schemaless-tagging.hbs new file mode 100644 index 0000000000..d2376a69fd --- /dev/null +++ b/wherehows-web/app/templates/components/datasets/schemaless-tagging.hbs @@ -0,0 +1,41 @@ + + +
+

+ Dataset contains personally identifiable information? +

+ +
+ {{input + id=(concat elementId '-schemaless-checkbox') + type="checkbox" + class="toggle-switch toggle-switch--light" + disabled=(not isEditable) + checked=(readonly containsPersonalData) + change=(action "onPersonalDataToggle" value="target.checked") + }} + +
+
+ +
+

+ Dataset security classification +

+ +
+ {{ember-selector + values=classifiers + selected=classification + disabled=(not isEditable) + selectionDidChange=(action "onSecurityClassificationChange") + }} +
+
diff --git a/wherehows-web/app/templates/datasets/dataset.hbs b/wherehows-web/app/templates/datasets/dataset.hbs index 8a5c46b942..36616851b0 100644 --- a/wherehows-web/app/templates/datasets/dataset.hbs +++ b/wherehows-web/app/templates/datasets/dataset.hbs @@ -268,6 +268,7 @@
{{dataset-compliance datasetName=model.name + schemaless=schemaless platform=datasetView.platform complianceInfo=complianceInfo complianceSuggestion=complianceSuggestion diff --git a/wherehows-web/app/typings/api/datasets/columns.d.ts b/wherehows-web/app/typings/api/datasets/columns.d.ts index 3c003933b9..9202a80710 100644 --- a/wherehows-web/app/typings/api/datasets/columns.d.ts +++ b/wherehows-web/app/typings/api/datasets/columns.d.ts @@ -31,8 +31,9 @@ interface IDatasetColumnWithHtmlComments extends IDatasetColumn { */ interface IDatasetColumnsGetResponse { status: ApiStatus; - columns?: Array | null; + columns?: Array; message?: string; + schemaless: boolean; } export { IDatasetColumn, IDatasetColumnWithHtmlComments, IDatasetColumnsGetResponse }; diff --git a/wherehows-web/app/utils/api/datasets/columns.ts b/wherehows-web/app/utils/api/datasets/columns.ts index 967fe8f00c..941d474224 100644 --- a/wherehows-web/app/utils/api/datasets/columns.ts +++ b/wherehows-web/app/utils/api/datasets/columns.ts @@ -64,18 +64,20 @@ const augmentObjectsWithHtmlComments = arrayMap(augmentWithHtmlComment); const columnDataTypesAndFieldNames = arrayMap(columnDataTypeAndFieldName); /** - * Gets the dataset columns for a dataset with the id specified + * Gets the dataset columns for a dataset with the id specified and the schemaless flag * @param {number} id the id of the dataset - * @return {Promise>} + * @return {(Promise<{schemaless: boolean; columns: Array}>)} */ -const readDatasetColumns = async (id: number): Promise> => { - const { status, columns, message = datasetColumnsException } = await getJSON({ +const readDatasetColumns = async (id: number): Promise<{ schemaless: boolean; columns: Array }> => { + const { status, columns = [], schemaless, message = datasetColumnsException } = await getJSON< + IDatasetColumnsGetResponse + >({ url: datasetColumnUrlById(id) }); // Returns an empty list if the status is ok but the columns is falsey if (status === ApiStatus.OK) { - return columns || []; + return { schemaless, columns }; } throw new Error(message); diff --git a/wherehows-web/tests/integration/components/datasets/schemaless-tagging-test.js b/wherehows-web/tests/integration/components/datasets/schemaless-tagging-test.js new file mode 100644 index 0000000000..ea0c1a3056 --- /dev/null +++ b/wherehows-web/tests/integration/components/datasets/schemaless-tagging-test.js @@ -0,0 +1,94 @@ +import { moduleForComponent, test } from 'ember-qunit'; +import hbs from 'htmlbars-inline-precompile'; + +import { Classification } from 'wherehows-web/constants'; +import { triggerEvent } from 'ember-native-dom-helpers'; + +moduleForComponent('datasets/schemaless-tagging', 'Integration | Component | datasets/schemaless tagging', { + integration: true +}); + +test('it renders', function(assert) { + assert.expect(2); + const elementId = 'test-schemaless-component-1337'; + this.set('elementId', elementId); + this.render(hbs`{{datasets/schemaless-tagging elementId=elementId}}`); + + assert.ok(document.querySelector(`#${elementId}-schemaless-checkbox`), 'it renders a checkbox component'); + assert.ok(document.querySelector(`#${elementId} select`), 'it renders a select drop down'); +}); + +test('it shows the current classification', function(assert) { + assert.expect(3); + this.render(hbs`{{datasets/schemaless-tagging classification=classification}}`); + + assert.equal(document.querySelector(`select`).value, '', "displays '' when not set"); + + this.set('classification', Classification.LimitedDistribution); + + assert.equal( + document.querySelector(`select`).value, + Classification.LimitedDistribution, + `displays ${Classification.LimitedDistribution} when set` + ); + + this.set('classification', Classification.Confidential); + + assert.equal( + document.querySelector('select').value, + Classification.Confidential, + `displays ${Classification.Confidential} when changed` + ); +}); + +test('it correctly indicates if the dataset has pii', function(assert) { + assert.expect(2); + this.set('containsPersonalData', true); + + this.render(hbs`{{datasets/schemaless-tagging containsPersonalData=containsPersonalData}}`); + + assert.equal(document.querySelector('.toggle-switch').checked, true, 'checkbox is checked when true'); + + this.set('containsPersonalData', false); + + assert.notOk(document.querySelector('.toggle-switch').checked, 'checkbox is unchecked when false'); +}); + +test('it invokes the onClassificationChange external action when change is triggered', function(assert) { + assert.expect(2); + let onClassificationChangeCallCount = 0; + + this.set('isEditable', true); + this.set('classification', Classification.LimitedDistribution); + this.set('onClassificationChange', () => { + assert.equal(++onClassificationChangeCallCount, 1, 'successfully invokes the external action'); + }); + + this.render( + hbs`{{datasets/schemaless-tagging isEditable=isEditable onClassificationChange=onClassificationChange classification=classification}}` + ); + + assert.equal(onClassificationChangeCallCount, 0, 'external action is not invoked on instantiation'); + + triggerEvent('select', 'change'); +}); + +test('it invokes the onPersonalDataChange external action on when toggled', function(assert) { + assert.expect(3); + + let onPersonalDataChangeCallCount = 0; + + this.set('isEditable', true); + this.set('containsPersonalData', false); + this.set('onPersonalDataChange', containsPersonalData => { + assert.equal(++onPersonalDataChangeCallCount, 1, 'successfully invokes the external action'); + assert.ok(containsPersonalData, 'flag value is truthy'); + }); + + this.render( + hbs`{{datasets/schemaless-tagging isEditable=isEditable onPersonalDataChange=onPersonalDataChange containsPersonalData=containsPersonalData}}` + ); + + assert.equal(onPersonalDataChangeCallCount, 0, 'external action is not invoked on instantiation'); + triggerEvent('[type=checkbox]', 'click'); +}); diff --git a/wherehows-web/tests/unit/constants/dataset-compliance-test.js b/wherehows-web/tests/unit/constants/dataset-compliance-test.js new file mode 100644 index 0000000000..398ac11724 --- /dev/null +++ b/wherehows-web/tests/unit/constants/dataset-compliance-test.js @@ -0,0 +1,22 @@ +import { module, test } from 'qunit'; +import { getComplianceSteps, complianceSteps } from 'wherehows-web/constants'; + +module('Unit | Constants | dataset compliance'); + +test('getComplianceSteps function should behave as expected', function(assert) { + assert.expect(3); + const piiTaggingStep = { 0: { name: 'editDatasetLevelCompliancePolicy' } }; + let result; + + assert.equal(typeof getComplianceSteps, 'function', 'getComplianceSteps is a function'); + result = getComplianceSteps(); + + assert.deepEqual(result, complianceSteps, 'getComplianceSteps result is expected shape when no args are passed'); + + result = getComplianceSteps({ hasSchema: false }); + assert.deepEqual( + result, + { ...complianceSteps, ...piiTaggingStep }, + 'getComplianceSteps result is expected shape when hasSchema attribute is false' + ); +}); diff --git a/wherehows-web/tests/unit/utils/datasets/metadata-acquisition-test.js b/wherehows-web/tests/unit/utils/datasets/metadata-acquisition-test.js index e50012e0d1..20c98a3657 100644 --- a/wherehows-web/tests/unit/utils/datasets/metadata-acquisition-test.js +++ b/wherehows-web/tests/unit/utils/datasets/metadata-acquisition-test.js @@ -2,7 +2,8 @@ import { lastSeenSuggestionInterval, lowQualitySuggestionConfidenceThreshold, defaultFieldDataTypeClassification, - logicalTypeValueLabel + logicalTypeValueLabel, + formatAsCapitalizedStringWithSpaces } from 'wherehows-web/constants/metadata-acquisition'; import { Classification, @@ -81,3 +82,13 @@ test('logicalTypeValueLabel generates correct labels for generic type', function assert.ok(idFieldLogicalTypeValues.includes(value), `Value ${value} found in ${idFieldLogicalTypeValues}`); }); }); + +test('formatAsCapitalizedStringWithSpaces generates the correct display string', function(assert) { + [ + ['confidential', 'Confidential'], + ['limitedDistribution', 'Limited Distribution'], + ['highlyConfidential', 'Highly Confidential'] + ].forEach(([source, target]) => { + assert.equal(formatAsCapitalizedStringWithSpaces(source), target, `correctly converts ${source}`); + }); +});