mirror of
https://github.com/datahub-project/datahub.git
synced 2025-08-31 12:52:13 +00:00
Merge pull request #899 from theseyi/schemaless-tagging
schemaless dataset tagging
This commit is contained in:
commit
fc6ff917a5
@ -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<Array<{}>>}
|
||||
*/
|
||||
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
|
||||
*/
|
||||
|
74
wherehows-web/app/components/datasets/schemaless-tagging.ts
Normal file
74
wherehows-web/app/components/datasets/schemaless-tagging.ts
Normal file
@ -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<ISecurityClassificationOption>}
|
||||
* @memberof SchemalessTagging
|
||||
*/
|
||||
classifiers: Array<ISecurityClassificationOption> = 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);
|
||||
}
|
||||
};
|
||||
}
|
@ -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 };
|
||||
|
@ -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<ISecurityClassificationOption>}
|
||||
*/
|
||||
const securityClassificationDropdownOptions: Array<ISecurityClassificationOption> = [
|
||||
'',
|
||||
...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<NonIdLogicalType | IdLogicalType> = {
|
||||
@ -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<FieldIdValues> = Object.values(FieldIdVal
|
||||
|
||||
export {
|
||||
defaultFieldDataTypeClassification,
|
||||
classifiers,
|
||||
securityClassificationDropdownOptions,
|
||||
formatAsCapitalizedStringWithSpaces,
|
||||
fieldIdentifierTypeIds,
|
||||
fieldIdentifierTypeValues,
|
||||
isMixedId,
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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';
|
||||
|
@ -2,7 +2,7 @@
|
||||
* Prompt for privacy compliance
|
||||
*/
|
||||
.metadata-prompt {
|
||||
margin: 30px 0 20px;
|
||||
margin: 40px 0 10px;
|
||||
|
||||
/**
|
||||
* Overrides default styles
|
||||
|
@ -0,0 +1 @@
|
||||
@import 'schemaless-tagging';
|
@ -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%;
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
@import 'toggle-switch';
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -90,9 +90,11 @@
|
||||
</div>
|
||||
{{/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}}
|
||||
</div>
|
||||
|
||||
{{yield}}
|
||||
|
@ -0,0 +1,41 @@
|
||||
<section class="metadata-prompt">
|
||||
<header class="metadata-prompt__header">
|
||||
<p>
|
||||
Dataset <span title="Personally Identifiable Information" class="define-text">PII</span> & Security Classification
|
||||
</p>
|
||||
</header>
|
||||
</section>
|
||||
|
||||
<div class="schemaless-tagging__tag">
|
||||
<h4 class="schemaless-tagging__prompt">
|
||||
Dataset contains personally identifiable information?
|
||||
</h4>
|
||||
|
||||
<div class="schemaless-tagging__input">
|
||||
{{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")
|
||||
}}
|
||||
<label for="{{concat elementId '-schemaless-checkbox'}}" class="toggle-button">
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="schemaless-tagging__tag">
|
||||
<h4 class="schemaless-tagging__prompt">
|
||||
Dataset security classification
|
||||
</h4>
|
||||
|
||||
<div class="schemaless-tagging__input">
|
||||
{{ember-selector
|
||||
values=classifiers
|
||||
selected=classification
|
||||
disabled=(not isEditable)
|
||||
selectionDidChange=(action "onSecurityClassificationChange")
|
||||
}}
|
||||
</div>
|
||||
</div>
|
@ -268,6 +268,7 @@
|
||||
<div id="compliancetab" class="tab-pane">
|
||||
{{dataset-compliance
|
||||
datasetName=model.name
|
||||
schemaless=schemaless
|
||||
platform=datasetView.platform
|
||||
complianceInfo=complianceInfo
|
||||
complianceSuggestion=complianceSuggestion
|
||||
|
@ -31,8 +31,9 @@ interface IDatasetColumnWithHtmlComments extends IDatasetColumn {
|
||||
*/
|
||||
interface IDatasetColumnsGetResponse {
|
||||
status: ApiStatus;
|
||||
columns?: Array<IDatasetColumn> | null;
|
||||
columns?: Array<IDatasetColumn>;
|
||||
message?: string;
|
||||
schemaless: boolean;
|
||||
}
|
||||
|
||||
export { IDatasetColumn, IDatasetColumnWithHtmlComments, IDatasetColumnsGetResponse };
|
||||
|
@ -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<Array<IDatasetColumn>>}
|
||||
* @return {(Promise<{schemaless: boolean; columns: Array<IDatasetColumn>}>)}
|
||||
*/
|
||||
const readDatasetColumns = async (id: number): Promise<Array<IDatasetColumn>> => {
|
||||
const { status, columns, message = datasetColumnsException } = await getJSON<IDatasetColumnsGetResponse>({
|
||||
const readDatasetColumns = async (id: number): Promise<{ schemaless: boolean; columns: Array<IDatasetColumn> }> => {
|
||||
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);
|
||||
|
@ -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');
|
||||
});
|
@ -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'
|
||||
);
|
||||
});
|
@ -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}`);
|
||||
});
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user