mirror of
https://github.com/datahub-project/datahub.git
synced 2025-09-01 13:23:09 +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 Ember from 'ember';
|
||||||
import isTrackingHeaderField from 'wherehows-web/utils/validators/tracking-headers';
|
import isTrackingHeaderField from 'wherehows-web/utils/validators/tracking-headers';
|
||||||
import {
|
import {
|
||||||
classifiers,
|
securityClassificationDropdownOptions,
|
||||||
DatasetClassifiers,
|
DatasetClassifiers,
|
||||||
fieldIdentifierTypes,
|
fieldIdentifierTypes,
|
||||||
fieldIdentifierOptions,
|
fieldIdentifierOptions,
|
||||||
@ -14,7 +14,7 @@ import {
|
|||||||
logicalTypesForGeneric,
|
logicalTypesForGeneric,
|
||||||
hasPredefinedFieldFormat,
|
hasPredefinedFieldFormat,
|
||||||
getDefaultLogicalType,
|
getDefaultLogicalType,
|
||||||
complianceSteps,
|
getComplianceSteps,
|
||||||
hiddenTrackingFields,
|
hiddenTrackingFields,
|
||||||
isExempt
|
isExempt
|
||||||
} from 'wherehows-web/constants';
|
} from 'wherehows-web/constants';
|
||||||
@ -53,13 +53,6 @@ const {
|
|||||||
invalidPolicyData
|
invalidPolicyData
|
||||||
} = compliancePolicyStrings;
|
} = 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
|
* List of non Id field data type classifications
|
||||||
* @type {Array}
|
* @type {Array}
|
||||||
@ -109,13 +102,20 @@ export default Component.extend({
|
|||||||
* @type {number}
|
* @type {number}
|
||||||
*/
|
*/
|
||||||
editStepIndex: initialStepIndex,
|
editStepIndex: initialStepIndex,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts the hash of complianceSteps to a list of steps
|
* Converts the hash of complianceSteps to a list of steps
|
||||||
* @type {Array<{}>}
|
* @type {ComputedProperty<Array<{}>>}
|
||||||
*/
|
*/
|
||||||
editSteps: Object.keys(complianceSteps)
|
editSteps: computed('schemaless', function() {
|
||||||
.sort()
|
const hasSchema = !getWithDefault(this, 'schemaless', false);
|
||||||
.map(key => complianceSteps[key]),
|
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
|
* Handles the transition between steps in the compliance edit wizard
|
||||||
@ -136,9 +136,9 @@ export default Component.extend({
|
|||||||
let lastIndex = initialStepIndex;
|
let lastIndex = initialStepIndex;
|
||||||
|
|
||||||
return function() {
|
return function() {
|
||||||
const currentIndex = get(this, 'editStepIndex');
|
const { editStepIndex: currentIndex, editSteps } = getProperties(this, ['editStepIndex', 'editSteps']);
|
||||||
// the current step in the edit sequence
|
// the current step in the edit sequence
|
||||||
const editStep = this.editSteps[currentIndex] || {};
|
const editStep = editSteps[currentIndex] || {};
|
||||||
const { name } = editStep;
|
const { name } = editStep;
|
||||||
|
|
||||||
if (name) {
|
if (name) {
|
||||||
@ -156,6 +156,7 @@ export default Component.extend({
|
|||||||
if (typeof nextAction === 'function') {
|
if (typeof nextAction === 'function') {
|
||||||
return (previousAction = nextAction);
|
return (previousAction = nextAction);
|
||||||
}
|
}
|
||||||
|
// otherwise clear the previous action
|
||||||
previousAction = noop;
|
previousAction = noop;
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
@ -228,9 +229,13 @@ export default Component.extend({
|
|||||||
|
|
||||||
didReceiveAttrs() {
|
didReceiveAttrs() {
|
||||||
this._super(...Array.from(arguments));
|
this._super(...Array.from(arguments));
|
||||||
this.resetEdit();
|
|
||||||
// Perform validation step on the received component attributes
|
// Perform validation step on the received component attributes
|
||||||
this.validateAttrs();
|
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();
|
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
|
* Ensure that props received from on this component
|
||||||
* are valid, otherwise flag
|
* are valid, otherwise flag
|
||||||
@ -331,11 +329,8 @@ export default Component.extend({
|
|||||||
// Map generic logical type to options consumable in DOM
|
// Map generic logical type to options consumable in DOM
|
||||||
genericLogicalTypes: logicalTypesForGeneric,
|
genericLogicalTypes: logicalTypesForGeneric,
|
||||||
|
|
||||||
// Map classifiers to options better consumed in DOM
|
// Map of classifiers options for drop down
|
||||||
classifiers: ['', ...classifiers.sort()].map(value => ({
|
classifiers: securityClassificationDropdownOptions,
|
||||||
value,
|
|
||||||
label: value ? formatAsCapitalizedStringWithSpaces(value) : '...'
|
|
||||||
})),
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {Boolean} cached boolean flag indicating that fields do contain a `kafka type`
|
* @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);
|
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
|
* 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 Ember from 'ember';
|
||||||
|
import { capitalize } from '@ember/string';
|
||||||
import {
|
import {
|
||||||
Classification,
|
Classification,
|
||||||
nonIdFieldLogicalTypes,
|
nonIdFieldLogicalTypes,
|
||||||
@ -11,6 +12,16 @@ import {
|
|||||||
FieldIdValues
|
FieldIdValues
|
||||||
} from 'wherehows-web/constants/datasets/compliance';
|
} 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
|
* 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
|
* 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
|
* A merge of id and non id field type security classifications
|
||||||
* @type {[K: string] : Classification}
|
* @type {([k: string]: Classification)}
|
||||||
*/
|
*/
|
||||||
const defaultFieldDataTypeClassification = { ...idFieldDataTypeClassification, ...nonIdFieldDataTypeClassification };
|
const defaultFieldDataTypeClassification = { ...idFieldDataTypeClassification, ...nonIdFieldDataTypeClassification };
|
||||||
|
|
||||||
@ -64,6 +75,26 @@ const classifiers = Object.values(defaultFieldDataTypeClassification).filter(
|
|||||||
(classifier, index, iter) => iter.indexOf(classifier) === index
|
(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
|
* Checks if the identifierType is a mixed Id
|
||||||
* @param {string} identifierType
|
* @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
|
* Returns a list of logicalType mappings for displaying its value and a label by logicalType
|
||||||
* @param {('id' | 'generic')} 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 logicalTypeValueLabel = (logicalType: 'id' | 'generic') => {
|
||||||
const logicalTypes: Array<NonIdLogicalType | IdLogicalType> = {
|
const logicalTypes: Array<NonIdLogicalType | IdLogicalType> = {
|
||||||
@ -129,13 +160,13 @@ const logicalTypeValueLabel = (logicalType: 'id' | 'generic') => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Map logicalTypes to options consumable by DOM
|
* Map logicalTypes to options consumable by DOM
|
||||||
* @returns {Array<{value: IdLogicalType; label: string;}>}
|
* @returns {(Array<{value: IdLogicalType; label: string;}>)}
|
||||||
*/
|
*/
|
||||||
const logicalTypesForIds = logicalTypeValueLabel('id');
|
const logicalTypesForIds = logicalTypeValueLabel('id');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Map generic logical type to options consumable in DOM
|
* 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');
|
const logicalTypesForGeneric = logicalTypeValueLabel('generic');
|
||||||
|
|
||||||
@ -155,7 +186,8 @@ const fieldIdentifierTypeValues: Array<FieldIdValues> = Object.values(FieldIdVal
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
defaultFieldDataTypeClassification,
|
defaultFieldDataTypeClassification,
|
||||||
classifiers,
|
securityClassificationDropdownOptions,
|
||||||
|
formatAsCapitalizedStringWithSpaces,
|
||||||
fieldIdentifierTypeIds,
|
fieldIdentifierTypeIds,
|
||||||
fieldIdentifierTypeValues,
|
fieldIdentifierTypeValues,
|
||||||
isMixedId,
|
isMixedId,
|
||||||
|
@ -108,7 +108,7 @@ export default Route.extend({
|
|||||||
let properties;
|
let properties;
|
||||||
|
|
||||||
const [
|
const [
|
||||||
columns,
|
{ schemaless, columns },
|
||||||
compliance,
|
compliance,
|
||||||
complianceSuggestion,
|
complianceSuggestion,
|
||||||
datasetComments,
|
datasetComments,
|
||||||
@ -140,6 +140,7 @@ export default Route.extend({
|
|||||||
isNewComplianceInfo,
|
isNewComplianceInfo,
|
||||||
complianceSuggestion,
|
complianceSuggestion,
|
||||||
datasetComments,
|
datasetComments,
|
||||||
|
schemaless,
|
||||||
schemas,
|
schemas,
|
||||||
isInternal,
|
isInternal,
|
||||||
datasetView,
|
datasetView,
|
||||||
|
@ -52,3 +52,8 @@
|
|||||||
.hidden {
|
.hidden {
|
||||||
display: none;
|
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 'dataset-property/all';
|
||||||
@import 'user-lookup/all';
|
@import 'user-lookup/all';
|
||||||
@import 'pendulum-ellipsis-animation/all';
|
@import 'pendulum-ellipsis-animation/all';
|
||||||
|
@import 'toggle-switch/all';
|
||||||
|
@import 'schemaless-tagging/all';
|
||||||
|
|
||||||
@import 'nacho/nacho-button';
|
@import 'nacho/nacho-button';
|
||||||
@import 'nacho/nacho-global-search';
|
@import 'nacho/nacho-global-search';
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
* Prompt for privacy compliance
|
* Prompt for privacy compliance
|
||||||
*/
|
*/
|
||||||
.metadata-prompt {
|
.metadata-prompt {
|
||||||
margin: 30px 0 20px;
|
margin: 40px 0 10px;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Overrides default styles
|
* 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>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
{{#if (and (eq editStepIndex 0) (not _hasBadData))}}
|
{{#unless schemaless}}
|
||||||
{{json-upload receiveJsonFile=(action "onComplianceJsonUpload") class="secondary-actions__action"}}
|
{{#if (and (eq editStepIndex 0) (not _hasBadData))}}
|
||||||
{{/if}}
|
{{json-upload receiveJsonFile=(action "onComplianceJsonUpload") class="secondary-actions__action"}}
|
||||||
|
{{/if}}
|
||||||
|
{{/unless}}
|
||||||
|
|
||||||
{{#if (or isReadOnly (eq editStepIndex 2))}}
|
{{#if (or isReadOnly (eq editStepIndex 2))}}
|
||||||
{{partial "datasets/dataset-compliance/dataset-classification"}}
|
{{partial "datasets/dataset-compliance/dataset-classification"}}
|
||||||
@ -108,10 +110,25 @@
|
|||||||
}}
|
}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
{{#if (or isReadOnly (eq editStepIndex 0))}}
|
{{#if schemaless}}
|
||||||
{{partial "datasets/dataset-compliance/dataset-compliance-entities"}}
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
|
{{#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>
|
</div>
|
||||||
|
|
||||||
{{yield}}
|
{{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">
|
<div id="compliancetab" class="tab-pane">
|
||||||
{{dataset-compliance
|
{{dataset-compliance
|
||||||
datasetName=model.name
|
datasetName=model.name
|
||||||
|
schemaless=schemaless
|
||||||
platform=datasetView.platform
|
platform=datasetView.platform
|
||||||
complianceInfo=complianceInfo
|
complianceInfo=complianceInfo
|
||||||
complianceSuggestion=complianceSuggestion
|
complianceSuggestion=complianceSuggestion
|
||||||
|
@ -31,8 +31,9 @@ interface IDatasetColumnWithHtmlComments extends IDatasetColumn {
|
|||||||
*/
|
*/
|
||||||
interface IDatasetColumnsGetResponse {
|
interface IDatasetColumnsGetResponse {
|
||||||
status: ApiStatus;
|
status: ApiStatus;
|
||||||
columns?: Array<IDatasetColumn> | null;
|
columns?: Array<IDatasetColumn>;
|
||||||
message?: string;
|
message?: string;
|
||||||
|
schemaless: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export { IDatasetColumn, IDatasetColumnWithHtmlComments, IDatasetColumnsGetResponse };
|
export { IDatasetColumn, IDatasetColumnWithHtmlComments, IDatasetColumnsGetResponse };
|
||||||
|
@ -64,18 +64,20 @@ const augmentObjectsWithHtmlComments = arrayMap(augmentWithHtmlComment);
|
|||||||
const columnDataTypesAndFieldNames = arrayMap(columnDataTypeAndFieldName);
|
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
|
* @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 readDatasetColumns = async (id: number): Promise<{ schemaless: boolean; columns: Array<IDatasetColumn> }> => {
|
||||||
const { status, columns, message = datasetColumnsException } = await getJSON<IDatasetColumnsGetResponse>({
|
const { status, columns = [], schemaless, message = datasetColumnsException } = await getJSON<
|
||||||
|
IDatasetColumnsGetResponse
|
||||||
|
>({
|
||||||
url: datasetColumnUrlById(id)
|
url: datasetColumnUrlById(id)
|
||||||
});
|
});
|
||||||
|
|
||||||
// Returns an empty list if the status is ok but the columns is falsey
|
// Returns an empty list if the status is ok but the columns is falsey
|
||||||
if (status === ApiStatus.OK) {
|
if (status === ApiStatus.OK) {
|
||||||
return columns || [];
|
return { schemaless, columns };
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(message);
|
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,
|
lastSeenSuggestionInterval,
|
||||||
lowQualitySuggestionConfidenceThreshold,
|
lowQualitySuggestionConfidenceThreshold,
|
||||||
defaultFieldDataTypeClassification,
|
defaultFieldDataTypeClassification,
|
||||||
logicalTypeValueLabel
|
logicalTypeValueLabel,
|
||||||
|
formatAsCapitalizedStringWithSpaces
|
||||||
} from 'wherehows-web/constants/metadata-acquisition';
|
} from 'wherehows-web/constants/metadata-acquisition';
|
||||||
import {
|
import {
|
||||||
Classification,
|
Classification,
|
||||||
@ -81,3 +82,13 @@ test('logicalTypeValueLabel generates correct labels for generic type', function
|
|||||||
assert.ok(idFieldLogicalTypeValues.includes(value), `Value ${value} found in ${idFieldLogicalTypeValues}`);
|
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