mirror of
https://github.com/datahub-project/datahub.git
synced 2025-11-01 19:25:56 +00:00
421 lines
14 KiB
TypeScript
421 lines
14 KiB
TypeScript
import Component from '@ember/component';
|
|
import { get, set, setProperties, getProperties } from '@ember/object';
|
|
import ComputedProperty from '@ember/object/computed';
|
|
import { inject } from '@ember/service';
|
|
import { task } from 'ember-concurrency';
|
|
import { action } from '@ember-decorators/object';
|
|
import Notifications, { NotificationEvent } from 'wherehows-web/services/notifications';
|
|
import { IDatasetColumn } from 'wherehows-web/typings/api/datasets/columns';
|
|
import { IComplianceInfo, IComplianceSuggestion } from 'wherehows-web/typings/api/datasets/compliance';
|
|
import { IDatasetView } from 'wherehows-web/typings/api/datasets/dataset';
|
|
import { IDatasetSchema } from 'wherehows-web/typings/api/datasets/schema';
|
|
import { IComplianceDataType } from 'wherehows-web/typings/api/list/compliance-datatypes';
|
|
import {
|
|
IReadComplianceResult,
|
|
isNotFoundApiError,
|
|
readDatasetComplianceByUrn,
|
|
readDatasetComplianceSuggestionByUrn,
|
|
saveDatasetComplianceByUrn,
|
|
saveDatasetComplianceSuggestionFeedbackByUrn
|
|
} from 'wherehows-web/utils/api';
|
|
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,
|
|
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
|
|
*/
|
|
type BatchComplianceResponse = [
|
|
IReadComplianceResult,
|
|
Array<IComplianceDataType>,
|
|
IComplianceSuggestion,
|
|
IDatasetSchema
|
|
];
|
|
|
|
/**
|
|
* Type alias for the result of invoking the batch operation, properties are set on container
|
|
*/
|
|
type BatchContainerDataResult = Pick<
|
|
DatasetComplianceContainer,
|
|
| 'isNewComplianceInfo'
|
|
| 'complianceInfo'
|
|
| 'complianceDataTypes'
|
|
| 'complianceSuggestion'
|
|
| 'schemaFieldNamesMappedToDataTypes'
|
|
| 'schemaless'
|
|
| 'suggestionConfidenceThreshold'
|
|
>;
|
|
|
|
const { successUpdating, failedUpdating, successUploading, invalidPolicyData } = compliancePolicyStrings;
|
|
|
|
export default class DatasetComplianceContainer extends Component {
|
|
/**
|
|
* External action on parent
|
|
*/
|
|
setOnChangeSetChange: (hasSuggestions: boolean) => void;
|
|
|
|
/**
|
|
* External action to capture changes to dataset pii status
|
|
*/
|
|
notifyPiiStatus: (containingPersonalData: boolean) => void;
|
|
|
|
/**
|
|
* External action on parent
|
|
*/
|
|
setOnComplianceType: (args: { isNewComplianceInfo: boolean; fromUpstream: boolean }) => void;
|
|
|
|
/**
|
|
* External action on parent
|
|
*/
|
|
setOnChangeSetDrift: (hasDrift: boolean) => void;
|
|
|
|
/**
|
|
* Mapping of field names on the dataset schema to the respective dataType
|
|
* @type {Array<Pick<IDatasetColumn, 'dataType' | 'fieldName'>>}
|
|
*/
|
|
schemaFieldNamesMappedToDataTypes: Array<Pick<IDatasetColumn, 'dataType' | 'fieldName'>> = [];
|
|
|
|
/**
|
|
* List of compliance data-type objects
|
|
* @type {Array<IComplianceDataType>}
|
|
*/
|
|
complianceDataTypes: Array<IComplianceDataType> = [];
|
|
|
|
/**
|
|
* Flag indicating that the compliance information for this dataset is new
|
|
* @type {boolean}
|
|
*/
|
|
isNewComplianceInfo: boolean = false;
|
|
|
|
/**
|
|
* Object containing the suggested values for the compliance form
|
|
* @type {IComplianceSuggestion | void}
|
|
*/
|
|
complianceSuggestion: IComplianceSuggestion | void;
|
|
|
|
/**
|
|
* Reference to the application notifications Service
|
|
* @type {ComputedProperty<Notifications>}
|
|
*/
|
|
notifications: ComputedProperty<Notifications> = inject();
|
|
|
|
/**
|
|
* Object containing the compliance information for the dataset
|
|
* @type {IComplianceInfo | void}
|
|
*/
|
|
complianceInfo: IComplianceInfo | void;
|
|
|
|
/**
|
|
* The platform / db that the dataset is persisted
|
|
* @type {IDatasetView.platform}
|
|
*/
|
|
platform: IDatasetView['platform'];
|
|
|
|
/**
|
|
* Flag indicating if the dataset has a schema representation
|
|
* @type {boolean}
|
|
*/
|
|
schemaless: boolean = false;
|
|
|
|
/**
|
|
* The nativeName of the dataset
|
|
* @type {string}
|
|
*/
|
|
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}
|
|
*/
|
|
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: DatasetComplianceContainer
|
|
): IterableIterator<Promise<BatchContainerDataResult>> {
|
|
const { notify } = get(this, 'notifications');
|
|
|
|
try {
|
|
yield this.batchContainerData();
|
|
} catch (e) {
|
|
notify(NotificationEvent.info, { content: e });
|
|
}
|
|
}).drop();
|
|
|
|
/**
|
|
* Sets dataset properties in batch after waiting for all data requests to resolve
|
|
* @return {Promise<Pick<DatasetComplianceContainer, "isNewComplianceInfo" | "complianceInfo" | "complianceDataTypes" | "complianceSuggestion" | "schemaFieldNamesMappedToDataTypes" | "schemaless">>}
|
|
*/
|
|
async batchContainerData(this: DatasetComplianceContainer): Promise<BatchContainerDataResult> {
|
|
const urn: string = get(this, 'urn');
|
|
const [
|
|
{ isNewComplianceInfo, complianceInfo },
|
|
complianceDataTypes,
|
|
complianceSuggestion,
|
|
{ columns, schemaless }
|
|
]: BatchComplianceResponse = await Promise.all([
|
|
readDatasetComplianceByUrn(urn),
|
|
readComplianceDataTypes(),
|
|
readDatasetComplianceSuggestionByUrn(urn),
|
|
readDatasetSchemaByUrn(urn)
|
|
]);
|
|
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,
|
|
complianceSuggestion,
|
|
schemaFieldNamesMappedToDataTypes,
|
|
schemaless
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Reads the compliance properties for the dataset
|
|
* @type {Task<Promise<IReadComplianceResult>, (a?: any) => TaskInstance<Promise<IReadComplianceResult>>>}
|
|
*/
|
|
getComplianceTask = task(function*(
|
|
this: DatasetComplianceContainer
|
|
): IterableIterator<Promise<IReadComplianceResult>> {
|
|
const { isNewComplianceInfo, complianceInfo }: IReadComplianceResult = yield readDatasetComplianceByUrn(
|
|
get(this, 'urn')
|
|
);
|
|
const { containingPersonalData, fromUpstream } = complianceInfo;
|
|
|
|
this.notifyPiiStatus(!!containingPersonalData);
|
|
|
|
this.onCompliancePolicyStateChange({ isNewComplianceInfo, fromUpstream: !!fromUpstream });
|
|
setProperties(this, { isNewComplianceInfo, complianceInfo });
|
|
});
|
|
|
|
/**
|
|
* Reads the compliance data types
|
|
* @type {Task<Promise<Array<IComplianceDataType>>, (a?: any) => TaskInstance<Promise<Array<IComplianceDataType>>>>}
|
|
*/
|
|
getComplianceDataTypesTask = task(function*(
|
|
this: DatasetComplianceContainer
|
|
): IterableIterator<Promise<Array<IComplianceDataType>>> {
|
|
const complianceDataTypes: Array<IComplianceDataType> = yield readComplianceDataTypes();
|
|
|
|
set(this, 'complianceDataTypes', complianceDataTypes);
|
|
});
|
|
|
|
/**
|
|
* Reads the suggestions for the compliance properties on the dataset
|
|
* @type {Task<Promise<IComplianceSuggestion>, (a?: any) => TaskInstance<Promise<IComplianceSuggestion>>>}
|
|
*/
|
|
getComplianceSuggestionsTask = task(function*(
|
|
this: DatasetComplianceContainer
|
|
): IterableIterator<Promise<IComplianceSuggestion>> {
|
|
const complianceSuggestion: IComplianceSuggestion = yield readDatasetComplianceSuggestionByUrn(get(this, 'urn'));
|
|
|
|
set(this, 'complianceSuggestion', complianceSuggestion);
|
|
});
|
|
|
|
/**
|
|
* Reads the schema properties for the dataset
|
|
* @type {Task<Promise<IDatasetSchema>, (a?: any) => TaskInstance<Promise<IDatasetSchema>>>}
|
|
*/
|
|
getDatasetSchemaTask = task(function*(
|
|
this: DatasetComplianceContainer
|
|
): IterableIterator<Promise<IDatasetSchema | Pick<IDatasetColumn, 'dataType' | 'fieldName'>[]>> {
|
|
try {
|
|
const { columns, schemaless }: IDatasetSchema = yield readDatasetSchemaByUrn(get(this, 'urn'));
|
|
const schemaFieldNamesMappedToDataTypes = yield iterateArrayAsync(columnDataTypesAndFieldNames)(columns);
|
|
setProperties(this, { schemaFieldNamesMappedToDataTypes, schemaless });
|
|
} catch (e) {
|
|
// If this schema is missing, silence exception, otherwise propagate
|
|
if (!isNotFoundApiError(e)) {
|
|
throw e;
|
|
}
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Handles user notifications when save succeeds or fails
|
|
* @template T the return type for the save request
|
|
* @param {Promise<T>} request async policy save request
|
|
* @returns {Promise<T>}
|
|
* @memberof DatasetComplianceContainer
|
|
*/
|
|
async notifyOnSave<T>(this: DatasetComplianceContainer, request: Promise<T>): Promise<T> {
|
|
const { notify } = get(this, 'notifications');
|
|
|
|
try {
|
|
await request;
|
|
notify(NotificationEvent.success, { content: successUpdating });
|
|
} catch ({ message }) {
|
|
notify(NotificationEvent.error, { content: `${failedUpdating} ${message}` });
|
|
}
|
|
|
|
return request;
|
|
}
|
|
|
|
/**
|
|
* Persists the updates to the compliance policy on the remote host
|
|
* @return {Promise<void>}
|
|
*/
|
|
@action
|
|
async savePrivacyCompliancePolicy(this: DatasetComplianceContainer): Promise<void> {
|
|
const complianceInfo = get(this, 'complianceInfo');
|
|
if (complianceInfo) {
|
|
const { complianceEntities } = complianceInfo;
|
|
|
|
await this.notifyOnSave<void>(
|
|
saveDatasetComplianceByUrn(get(this, 'urn'), {
|
|
...complianceInfo,
|
|
// filter out readonly entities, then omit readonly attribute from remaining entities before save
|
|
complianceEntities: removeReadonlyAttr(editableTags(complianceEntities))
|
|
})
|
|
);
|
|
|
|
this.resetPrivacyCompliancePolicy.call(this);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Resets the compliance information for the dataset with the previously persisted properties
|
|
*/
|
|
@action
|
|
resetPrivacyCompliancePolicy() {
|
|
get(this, 'getComplianceTask').perform();
|
|
}
|
|
|
|
/**
|
|
* Invokes external action if field suggestions change
|
|
* @param {boolean} hasSuggestions
|
|
*/
|
|
@action
|
|
onSuggestionsChanged(hasSuggestions: boolean) {
|
|
this.setOnChangeSetChange(hasSuggestions);
|
|
}
|
|
|
|
/**
|
|
* Invokes external action if compliance info is new or otherwise
|
|
* @param {boolean} isNewComplianceInfo flag indicating the policy does not exist remotely
|
|
* @param {boolean} fromUpstream flag indicating related dataset compliance info is derived
|
|
*/
|
|
@action
|
|
onCompliancePolicyStateChange({
|
|
isNewComplianceInfo,
|
|
fromUpstream
|
|
}: {
|
|
isNewComplianceInfo: boolean;
|
|
fromUpstream: boolean;
|
|
}) {
|
|
this.setOnComplianceType({ isNewComplianceInfo, fromUpstream });
|
|
}
|
|
|
|
/**
|
|
* Invokes external action on compliance policy fields drift
|
|
* @param {boolean} hasDrift
|
|
*/
|
|
@action
|
|
onCompliancePolicyChangeSetDrift(hasDrift: boolean) {
|
|
this.setOnChangeSetDrift(hasDrift);
|
|
}
|
|
|
|
/**
|
|
* Invokes the external action to save feedback on a compliance suggestion
|
|
* @param {string | null} uid the uuid for the compliance suggestion
|
|
* @param {SuggestionIntent} feedback
|
|
*/
|
|
@action
|
|
onSuggestionsComplianceFeedback(uid: string | null = null, feedback: SuggestionIntent) {
|
|
saveDatasetComplianceSuggestionFeedbackByUrn(get(this, 'urn'), uid, feedback);
|
|
}
|
|
|
|
/**
|
|
* Reapplies the uploaded compliance policy to the container property
|
|
* @param {string} jsonString string representation for the JSON file
|
|
* @memberof DatasetComplianceContainer
|
|
*/
|
|
@action
|
|
async onComplianceJsonUpdate(this: DatasetComplianceContainer, jsonString: string): Promise<void> {
|
|
const {
|
|
complianceInfo,
|
|
notifications: { notify }
|
|
} = getProperties(this, ['complianceInfo', 'notifications']);
|
|
|
|
/**
|
|
* Inner function to wrap call to notify method of notification service
|
|
* @return {void}
|
|
*/
|
|
const updateError = (error: string): void => {
|
|
notify(NotificationEvent.error, {
|
|
content: error
|
|
});
|
|
|
|
throw new Error(error);
|
|
};
|
|
|
|
if (complianceInfo) {
|
|
const entityMetadata: Pick<IComplianceInfo, 'complianceEntities'> = JSON.parse(jsonString);
|
|
|
|
if (validateMetadataObject(entityMetadata, complianceEntitiesTaxonomy)) {
|
|
const { complianceEntities } = entityMetadata;
|
|
const resolvedComplianceInfo = { ...complianceInfo, complianceEntities };
|
|
const { dialogActions, dismissedOrConfirmed } = notificationDialogActionFactory();
|
|
|
|
set(this, 'complianceInfo', resolvedComplianceInfo);
|
|
|
|
notify(NotificationEvent.confirm, {
|
|
header: 'Successfully applied compliance entity metadata',
|
|
content: successUploading,
|
|
dialogActions,
|
|
dismissButtonText: false,
|
|
confirmButtonText: 'Next'
|
|
});
|
|
|
|
return await dismissedOrConfirmed;
|
|
}
|
|
|
|
return updateError(invalidPolicyData);
|
|
}
|
|
|
|
updateError('No Compliance policy found');
|
|
}
|
|
}
|