DDM in DE for NOSQL (#2224)

* ddm for DE for noSQL

* ddm for DE for noSQL

* ddm for DE for noSQL

* ddm fix for the default case and test fix

* formatting issue

* updated the text change

* added validation errors

---------

Co-authored-by: Sakshi Gupta <sakshig@microsoft.com>
This commit is contained in:
sakshigupta12feb
2025-10-27 19:37:40 +05:30
committed by GitHub
parent ff1eb6a78e
commit 0578910b9e
15 changed files with 1212 additions and 96 deletions

View File

@@ -16,7 +16,7 @@ import {
import { useIndexingPolicyStore } from "Explorer/Tabs/QueryTab/ResultsView";
import { useDatabases } from "Explorer/useDatabases";
import { isFabricNative } from "Platform/Fabric/FabricUtil";
import { isVectorSearchEnabled } from "Utils/CapabilityUtils";
import { isCapabilityEnabled, isVectorSearchEnabled } from "Utils/CapabilityUtils";
import { isRunningOnPublicCloud } from "Utils/CloudUtils";
import * as React from "react";
import DiscardIcon from "../../../../images/discard.svg";
@@ -48,6 +48,7 @@ import {
ConflictResolutionComponent,
ConflictResolutionComponentProps,
} from "./SettingsSubComponents/ConflictResolutionComponent";
import { DataMaskingComponent, DataMaskingComponentProps } from "./SettingsSubComponents/DataMaskingComponent";
import {
GlobalSecondaryIndexComponent,
GlobalSecondaryIndexComponentProps,
@@ -151,6 +152,12 @@ export interface SettingsComponentState {
conflictResolutionPolicyProcedureBaseline: string;
isConflictResolutionDirty: boolean;
dataMaskingContent: DataModels.DataMaskingPolicy;
dataMaskingContentBaseline: DataModels.DataMaskingPolicy;
shouldDiscardDataMasking: boolean;
isDataMaskingDirty: boolean;
dataMaskingValidationErrors: string[];
selectedTab: SettingsV2TabTypes;
}
@@ -258,6 +265,12 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
shouldDiscardComputedProperties: false,
isComputedPropertiesDirty: false,
dataMaskingContent: undefined,
dataMaskingContentBaseline: undefined,
shouldDiscardDataMasking: false,
isDataMaskingDirty: false,
dataMaskingValidationErrors: [],
conflictResolutionPolicyMode: undefined,
conflictResolutionPolicyModeBaseline: undefined,
conflictResolutionPolicyPath: undefined,
@@ -345,7 +358,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
};
public isSaveSettingsButtonEnabled = (): boolean => {
if (this.isOfferReplacePending()) {
if (this.isOfferReplacePending() || this.props.settingsTab.isExecuting()) {
return false;
}
@@ -353,6 +366,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
return false;
}
if (this.state.dataMaskingValidationErrors.length > 0) {
return false;
}
return (
this.state.isScaleSaveable ||
this.state.isSubSettingsSaveable ||
@@ -360,12 +377,16 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
this.state.isIndexingPolicyDirty ||
this.state.isConflictResolutionDirty ||
this.state.isComputedPropertiesDirty ||
this.state.isDataMaskingDirty ||
(!!this.state.currentMongoIndexes && this.state.isMongoIndexingPolicySaveable) ||
this.state.isThroughputBucketsSaveable
);
};
public isDiscardSettingsButtonEnabled = (): boolean => {
if (this.props.settingsTab.isExecuting()) {
return false;
}
return (
this.state.isScaleDiscardable ||
this.state.isSubSettingsDiscardable ||
@@ -373,6 +394,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
this.state.isIndexingPolicyDirty ||
this.state.isConflictResolutionDirty ||
this.state.isComputedPropertiesDirty ||
this.state.isDataMaskingDirty ||
(!!this.state.currentMongoIndexes && this.state.isMongoIndexingPolicyDiscardable) ||
this.state.isThroughputBucketsSaveable
);
@@ -428,7 +450,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
: this.saveDatabaseSettings(startKey));
} catch (error) {
this.props.settingsTab.isExecutionError(true);
console.error(error);
traceFailure(
Action.SettingsV2Updated,
{
@@ -445,8 +466,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
} finally {
this.props.settingsTab.isExecuting(false);
// Send message to Fabric no matter success or failure.
// In case of failure, saveCollectionSettings might have partially succeeded and Fabric needs to refresh
if (isFabricNative() && this.isCollectionSettingsTab) {
sendMessage({
type: FabricMessageTypes.ContainerUpdated,
@@ -457,6 +476,9 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
};
public onRevertClick = (): void => {
if (this.props.settingsTab.isExecuting()) {
return;
}
trace(Action.SettingsV2Discarded, ActionModifiers.Mark, {
message: "Settings Discarded",
});
@@ -497,6 +519,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
computedPropertiesContent: this.state.computedPropertiesContentBaseline,
shouldDiscardComputedProperties: true,
isComputedPropertiesDirty: false,
dataMaskingContent: this.state.dataMaskingContentBaseline,
shouldDiscardDataMasking: true,
isDataMaskingDirty: false,
dataMaskingValidationErrors: [],
});
};
@@ -659,6 +685,39 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
private onComputedPropertiesDirtyChange = (isComputedPropertiesDirty: boolean): void =>
this.setState({ isComputedPropertiesDirty: isComputedPropertiesDirty });
private onDataMaskingContentChange = (newDataMasking: DataModels.DataMaskingPolicy): void => {
if (!newDataMasking.excludedPaths) {
newDataMasking.excludedPaths = [];
}
if (!newDataMasking.includedPaths) {
newDataMasking.includedPaths = [];
}
const validationErrors = [];
if (!Array.isArray(newDataMasking.includedPaths)) {
validationErrors.push("includedPaths must be an array");
}
if (!Array.isArray(newDataMasking.excludedPaths)) {
validationErrors.push("excludedPaths must be an array");
}
if (typeof newDataMasking.policyFormatVersion !== "number") {
validationErrors.push("policyFormatVersion must be a number");
}
if (typeof newDataMasking.isPolicyEnabled !== "boolean") {
validationErrors.push("isPolicyEnabled must be a boolean");
}
this.setState({
dataMaskingContent: newDataMasking,
dataMaskingValidationErrors: validationErrors,
});
};
private resetShouldDiscardDataMasking = (): void => this.setState({ shouldDiscardDataMasking: false });
private onDataMaskingDirtyChange = (isDataMaskingDirty: boolean): void =>
this.setState({ isDataMaskingDirty: isDataMaskingDirty });
private calculateTotalThroughputUsed = (): void => {
this.totalThroughputUsed = 0;
(useDatabases.getState().databases || []).forEach(async (database) => {
@@ -783,6 +842,12 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
const fullTextPolicy: DataModels.FullTextPolicy =
this.collection.fullTextPolicy && this.collection.fullTextPolicy();
const indexingPolicyContent = this.collection.indexingPolicy();
const dataMaskingContent: DataModels.DataMaskingPolicy = {
includedPaths: this.collection.dataMaskingPolicy?.()?.includedPaths || [],
excludedPaths: this.collection.dataMaskingPolicy?.()?.excludedPaths || [],
policyFormatVersion: this.collection.dataMaskingPolicy?.()?.policyFormatVersion || 2,
isPolicyEnabled: this.collection.dataMaskingPolicy?.()?.isPolicyEnabled || false,
};
const conflictResolutionPolicy: DataModels.ConflictResolutionPolicy =
this.collection.conflictResolutionPolicy && this.collection.conflictResolutionPolicy();
const conflictResolutionPolicyMode = parseConflictResolutionMode(conflictResolutionPolicy?.mode);
@@ -834,11 +899,14 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
geospatialConfigTypeBaseline: geoSpatialConfigType,
computedPropertiesContent: computedPropertiesContent,
computedPropertiesContentBaseline: computedPropertiesContent,
dataMaskingContent: dataMaskingContent,
dataMaskingContentBaseline: dataMaskingContent,
};
};
private getTabsButtons = (): CommandButtonComponentProps[] => {
const buttons: CommandButtonComponentProps[] = [];
const isExecuting = this.props.settingsTab.isExecuting();
if (this.saveSettingsButton.isVisible()) {
const label = "Save";
buttons.push({
@@ -848,7 +916,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
disabled: !this.saveSettingsButton.isEnabled(),
disabled: isExecuting || !this.saveSettingsButton.isEnabled(),
});
}
@@ -857,11 +925,16 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
buttons.push({
iconSrc: DiscardIcon,
iconAlt: label,
onCommandClick: this.onRevertClick,
onCommandClick: () => {
if (isExecuting) {
return;
}
this.onRevertClick();
},
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
disabled: !this.discardSettingsChangesButton.isEnabled(),
disabled: isExecuting || !this.discardSettingsChangesButton.isEnabled(),
});
}
return buttons;
@@ -980,7 +1053,8 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
this.state.isContainerPolicyDirty ||
this.state.isIndexingPolicyDirty ||
this.state.isConflictResolutionDirty ||
this.state.isComputedPropertiesDirty
this.state.isComputedPropertiesDirty ||
this.state.isDataMaskingDirty
) {
let defaultTtl: number;
switch (this.state.timeToLive) {
@@ -1002,6 +1076,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
newCollection.vectorEmbeddingPolicy = this.state.vectorEmbeddingPolicy;
newCollection.fullTextPolicy = this.state.fullTextPolicy;
newCollection.dataMaskingPolicy = this.state.dataMaskingContent;
newCollection.indexingPolicy = this.state.indexingPolicyContent;
@@ -1048,13 +1123,18 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
await this.refreshIndexTransformationProgress();
}
// Update collection object with new data
this.collection.dataMaskingPolicy(updatedCollection.dataMaskingPolicy);
this.setState({
dataMaskingContentBaseline: this.state.dataMaskingContent,
isSubSettingsSaveable: false,
isSubSettingsDiscardable: false,
isContainerPolicyDirty: false,
isIndexingPolicyDirty: false,
isConflictResolutionDirty: false,
isComputedPropertiesDirty: false,
isDataMaskingDirty: false,
});
}
@@ -1383,6 +1463,31 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
});
}
// Check if DDM should be enabled
const shouldEnableDDM = (): boolean => {
const hasDataMaskingCapability = isCapabilityEnabled(Constants.CapabilityNames.EnableDynamicDataMasking);
const isSqlAccount = userContext.apiType === "SQL";
return isSqlAccount && hasDataMaskingCapability; // Only show for SQL accounts with DDM capability
};
if (shouldEnableDDM()) {
const dataMaskingComponentProps: DataMaskingComponentProps = {
shouldDiscardDataMasking: this.state.shouldDiscardDataMasking,
resetShouldDiscardDataMasking: this.resetShouldDiscardDataMasking,
dataMaskingContent: this.state.dataMaskingContent,
dataMaskingContentBaseline: this.state.dataMaskingContentBaseline,
onDataMaskingContentChange: this.onDataMaskingContentChange,
onDataMaskingDirtyChange: this.onDataMaskingDirtyChange,
validationErrors: this.state.dataMaskingValidationErrors,
};
tabs.push({
tab: SettingsV2TabTypes.DataMaskingTab,
content: <DataMaskingComponent {...dataMaskingComponentProps} />,
});
}
if (this.throughputBucketsEnabled && !hasDatabaseSharedThroughput(this.collection) && this.offer) {
tabs.push({
tab: SettingsV2TabTypes.ThroughputBucketsTab,