Added Computed Properties tab to scale and settings

This commit is contained in:
Chuck Skelton 2023-06-23 16:02:11 -07:00
parent 15e8c66aa4
commit 263ad9a5e6
15 changed files with 369 additions and 35 deletions

View File

@ -154,6 +154,7 @@ export interface Collection extends Resource {
geospatialConfig?: GeospatialConfig; geospatialConfig?: GeospatialConfig;
schema?: ISchema; schema?: ISchema;
requestSchema?: () => void; requestSchema?: () => void;
computedProperties?: ComputedProperties;
} }
export interface CollectionsWithPagination { export interface CollectionsWithPagination {
@ -165,7 +166,7 @@ export interface Database extends Resource {
collections?: Collection[]; collections?: Collection[];
} }
export interface DocumentId extends Resource {} export interface DocumentId extends Resource { }
export interface ConflictId extends Resource { export interface ConflictId extends Resource {
resourceId?: string; resourceId?: string;
@ -192,6 +193,13 @@ export interface IndexingPolicy {
spatialIndexes?: any; spatialIndexes?: any;
} }
export interface ComputedProperty {
name: string;
query: string;
}
export type ComputedProperties = ComputedProperty[]
export interface PartitionKey { export interface PartitionKey {
paths: string[]; paths: string[];
kind: "Hash" | "Range" | "MultiHash"; kind: "Hash" | "Range" | "MultiHash";

View File

@ -135,6 +135,7 @@ export interface Collection extends CollectionBase {
changeFeedPolicy: ko.Observable<DataModels.ChangeFeedPolicy>; changeFeedPolicy: ko.Observable<DataModels.ChangeFeedPolicy>;
geospatialConfig: ko.Observable<DataModels.GeospatialConfig>; geospatialConfig: ko.Observable<DataModels.GeospatialConfig>;
documentIds: ko.ObservableArray<DocumentId>; documentIds: ko.ObservableArray<DocumentId>;
computedProperties: ko.Observable<DataModels.ComputedProperties>;
cassandraKeys: CassandraTableKeys; cassandraKeys: CassandraTableKeys;
cassandraSchema: CassandraTableKey[]; cassandraSchema: CassandraTableKey[];

View File

@ -1,22 +1,26 @@
import { IPivotItemProps, IPivotProps, Pivot, PivotItem } from "@fluentui/react"; import { IPivotItemProps, IPivotProps, Pivot, PivotItem } from "@fluentui/react";
import {
ComputedPropertiesComponent,
ComputedPropertiesComponentProps,
} from "Explorer/Controls/Settings/SettingsSubComponents/ComputedPropertiesComponent";
import { useDatabases } from "Explorer/useDatabases"; import { useDatabases } from "Explorer/useDatabases";
import * as React from "react"; import * as React from "react";
import DiscardIcon from "../../../../images/discard.svg"; import DiscardIcon from "../../../../images/discard.svg";
import SaveIcon from "../../../../images/save-cosmos.svg"; import SaveIcon from "../../../../images/save-cosmos.svg";
import { AuthType } from "../../../AuthType"; import { AuthType } from "../../../AuthType";
import * as Constants from "../../../Common/Constants"; import * as Constants from "../../../Common/Constants";
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
import { getIndexTransformationProgress } from "../../../Common/dataAccess/getIndexTransformationProgress"; import { getIndexTransformationProgress } from "../../../Common/dataAccess/getIndexTransformationProgress";
import { readMongoDBCollectionThroughRP } from "../../../Common/dataAccess/readMongoDBCollection"; import { readMongoDBCollectionThroughRP } from "../../../Common/dataAccess/readMongoDBCollection";
import { updateCollection } from "../../../Common/dataAccess/updateCollection"; import { updateCollection } from "../../../Common/dataAccess/updateCollection";
import { updateOffer } from "../../../Common/dataAccess/updateOffer"; import { updateOffer } from "../../../Common/dataAccess/updateOffer";
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
import * as DataModels from "../../../Contracts/DataModels"; import * as DataModels from "../../../Contracts/DataModels";
import * as ViewModels from "../../../Contracts/ViewModels"; import * as ViewModels from "../../../Contracts/ViewModels";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import { trace, traceFailure, traceStart, traceSuccess } from "../../../Shared/Telemetry/TelemetryProcessor"; import { trace, traceFailure, traceStart, traceSuccess } from "../../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../../UserContext"; import { userContext } from "../../../UserContext";
import { MongoDBCollectionResource, MongoIndex } from "../../../Utils/arm/generatedClients/cosmos/types";
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils"; import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
import { MongoDBCollectionResource, MongoIndex } from "../../../Utils/arm/generatedClients/cosmos/types";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import { useCommandBar } from "../../Menus/CommandBar/CommandBarComponentAdapter"; import { useCommandBar } from "../../Menus/CommandBar/CommandBarComponentAdapter";
import { SettingsTabV2 } from "../../Tabs/SettingsTabV2"; import { SettingsTabV2 } from "../../Tabs/SettingsTabV2";
@ -37,15 +41,15 @@ import {
AddMongoIndexProps, AddMongoIndexProps,
ChangeFeedPolicyState, ChangeFeedPolicyState,
GeospatialConfigType, GeospatialConfigType,
MongoIndexTypes,
SettingsV2TabTypes,
TtlType,
getMongoNotification, getMongoNotification,
getTabTitle, getTabTitle,
hasDatabaseSharedThroughput, hasDatabaseSharedThroughput,
isDirty, isDirty,
MongoIndexTypes,
parseConflictResolutionMode, parseConflictResolutionMode,
parseConflictResolutionProcedure, parseConflictResolutionProcedure,
SettingsV2TabTypes,
TtlType,
} from "./SettingsUtils"; } from "./SettingsUtils";
interface SettingsV2TabInfo { interface SettingsV2TabInfo {
@ -101,6 +105,11 @@ export interface SettingsComponentState {
indexesToAdd: AddMongoIndexProps[]; indexesToAdd: AddMongoIndexProps[];
indexTransformationProgress: number; indexTransformationProgress: number;
computedPropertiesContent: DataModels.ComputedProperties;
computedPropertiesContentBaseline: DataModels.ComputedProperties;
shouldDiscardComputedProperties: boolean;
isComputedPropertiesDirty: boolean;
conflictResolutionPolicyMode: DataModels.ConflictResolutionMode; conflictResolutionPolicyMode: DataModels.ConflictResolutionMode;
conflictResolutionPolicyModeBaseline: DataModels.ConflictResolutionMode; conflictResolutionPolicyModeBaseline: DataModels.ConflictResolutionMode;
conflictResolutionPolicyPath: string; conflictResolutionPolicyPath: string;
@ -125,6 +134,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
private offer: DataModels.Offer; private offer: DataModels.Offer;
private changeFeedPolicyVisible: boolean; private changeFeedPolicyVisible: boolean;
private isFixedContainer: boolean; private isFixedContainer: boolean;
private shouldShowComputedPropertiesEditor: boolean;
private shouldShowIndexingPolicyEditor: boolean; private shouldShowIndexingPolicyEditor: boolean;
private totalThroughputUsed: number; private totalThroughputUsed: number;
public mongoDBCollectionResource: MongoDBCollectionResource; public mongoDBCollectionResource: MongoDBCollectionResource;
@ -137,6 +147,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
this.collection = this.props.settingsTab.collection as ViewModels.Collection; this.collection = this.props.settingsTab.collection as ViewModels.Collection;
this.offer = this.collection?.offer(); this.offer = this.collection?.offer();
this.isAnalyticalStorageEnabled = !!this.collection?.analyticalStorageTtl(); this.isAnalyticalStorageEnabled = !!this.collection?.analyticalStorageTtl();
this.shouldShowComputedPropertiesEditor = userContext.apiType === "SQL";
this.shouldShowIndexingPolicyEditor = userContext.apiType !== "Cassandra" && userContext.apiType !== "Mongo"; this.shouldShowIndexingPolicyEditor = userContext.apiType !== "Cassandra" && userContext.apiType !== "Mongo";
this.changeFeedPolicyVisible = userContext.features.enableChangeFeedPolicy; this.changeFeedPolicyVisible = userContext.features.enableChangeFeedPolicy;
@ -187,6 +198,11 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
isMongoIndexingPolicyDiscardable: false, isMongoIndexingPolicyDiscardable: false,
indexTransformationProgress: undefined, indexTransformationProgress: undefined,
computedPropertiesContent: undefined,
computedPropertiesContentBaseline: undefined,
shouldDiscardComputedProperties: false,
isComputedPropertiesDirty: false,
conflictResolutionPolicyMode: undefined, conflictResolutionPolicyMode: undefined,
conflictResolutionPolicyModeBaseline: undefined, conflictResolutionPolicyModeBaseline: undefined,
conflictResolutionPolicyPath: undefined, conflictResolutionPolicyPath: undefined,
@ -277,6 +293,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
this.state.isSubSettingsSaveable || this.state.isSubSettingsSaveable ||
this.state.isIndexingPolicyDirty || this.state.isIndexingPolicyDirty ||
this.state.isConflictResolutionDirty || this.state.isConflictResolutionDirty ||
this.state.isComputedPropertiesDirty ||
(!!this.state.currentMongoIndexes && this.state.isMongoIndexingPolicySaveable) (!!this.state.currentMongoIndexes && this.state.isMongoIndexingPolicySaveable)
); );
}; };
@ -287,6 +304,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
this.state.isSubSettingsDiscardable || this.state.isSubSettingsDiscardable ||
this.state.isIndexingPolicyDirty || this.state.isIndexingPolicyDirty ||
this.state.isConflictResolutionDirty || this.state.isConflictResolutionDirty ||
this.state.isComputedPropertiesDirty ||
(!!this.state.currentMongoIndexes && this.state.isMongoIndexingPolicyDiscardable) (!!this.state.currentMongoIndexes && this.state.isMongoIndexingPolicyDiscardable)
); );
}; };
@ -390,6 +408,9 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
isMongoIndexingPolicySaveable: false, isMongoIndexingPolicySaveable: false,
isMongoIndexingPolicyDiscardable: false, isMongoIndexingPolicyDiscardable: false,
isConflictResolutionDirty: false, isConflictResolutionDirty: false,
computedPropertiesContent: this.state.computedPropertiesContentBaseline,
shouldDiscardComputedProperties: true,
isComputedPropertiesDirty: false,
}); });
}; };
@ -506,6 +527,31 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
private onMongoIndexingPolicyDiscardableChange = (isMongoIndexingPolicyDiscardable: boolean): void => private onMongoIndexingPolicyDiscardableChange = (isMongoIndexingPolicyDiscardable: boolean): void =>
this.setState({ isMongoIndexingPolicyDiscardable }); this.setState({ isMongoIndexingPolicyDiscardable });
private onComputedPropertiesContentChange = (newComputedProperties: DataModels.ComputedProperties): void =>
this.setState({ computedPropertiesContent: newComputedProperties });
private resetShouldDiscardComputedProperties = (): void => this.setState({ shouldDiscardComputedProperties: false });
private logComputedPropertiesSuccessMessage = (): void => {
if (this.props.settingsTab.onLoadStartKey) {
traceSuccess(
Action.Tab,
{
databaseName: this.collection.databaseId,
collectionName: this.collection.id(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.props.settingsTab.tabTitle(),
},
this.props.settingsTab.onLoadStartKey
);
this.props.settingsTab.onLoadStartKey = undefined;
}
};
private onComputedPropertiesDirtyChange = (isComputedPropertiesDirty: boolean): void =>
this.setState({ isComputedPropertiesDirty: isComputedPropertiesDirty });
private calculateTotalThroughputUsed = (): void => { private calculateTotalThroughputUsed = (): void => {
this.totalThroughputUsed = 0; this.totalThroughputUsed = 0;
(useDatabases.getState().databases || []).forEach(async (database) => { (useDatabases.getState().databases || []).forEach(async (database) => {
@ -626,7 +672,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
const indexingPolicyContent = this.collection.indexingPolicy(); const indexingPolicyContent = this.collection.indexingPolicy();
const conflictResolutionPolicy: DataModels.ConflictResolutionPolicy = const conflictResolutionPolicy: DataModels.ConflictResolutionPolicy =
this.collection.conflictResolutionPolicy && this.collection.conflictResolutionPolicy(); this.collection.conflictResolutionPolicy && this.collection.conflictResolutionPolicy();
const conflictResolutionPolicyMode = parseConflictResolutionMode(conflictResolutionPolicy?.mode); const conflictResolutionPolicyMode = parseConflictResolutionMode(conflictResolutionPolicy?.mode);
const conflictResolutionPolicyPath = conflictResolutionPolicy?.conflictResolutionPath; const conflictResolutionPolicyPath = conflictResolutionPolicy?.conflictResolutionPath;
const conflictResolutionPolicyProcedure = parseConflictResolutionProcedure( const conflictResolutionPolicyProcedure = parseConflictResolutionProcedure(
@ -635,6 +680,12 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
const geospatialConfigTypeString: string = const geospatialConfigTypeString: string =
(this.collection.geospatialConfig && this.collection.geospatialConfig()?.type) || GeospatialConfigType.Geometry; (this.collection.geospatialConfig && this.collection.geospatialConfig()?.type) || GeospatialConfigType.Geometry;
const geoSpatialConfigType = GeospatialConfigType[geospatialConfigTypeString as keyof typeof GeospatialConfigType]; const geoSpatialConfigType = GeospatialConfigType[geospatialConfigTypeString as keyof typeof GeospatialConfigType];
let computedPropertiesContent = this.collection.computedProperties();
if (!computedPropertiesContent || computedPropertiesContent.length == 0) {
computedPropertiesContent = [
{ name: "name_of_property", query: "query_to_compute_property" },
] as DataModels.ComputedProperties;
}
return { return {
throughput: offerThroughput, throughput: offerThroughput,
@ -659,6 +710,8 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
conflictResolutionPolicyProcedureBaseline: conflictResolutionPolicyProcedure, conflictResolutionPolicyProcedureBaseline: conflictResolutionPolicyProcedure,
geospatialConfigType: geoSpatialConfigType, geospatialConfigType: geoSpatialConfigType,
geospatialConfigTypeBaseline: geoSpatialConfigType, geospatialConfigTypeBaseline: geoSpatialConfigType,
computedPropertiesContent: computedPropertiesContent,
computedPropertiesContentBaseline: computedPropertiesContent,
}; };
}; };
@ -775,7 +828,12 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
private saveCollectionSettings = async (startKey: number): Promise<void> => { private saveCollectionSettings = async (startKey: number): Promise<void> => {
const newCollection: DataModels.Collection = { ...this.collection.rawDataModel }; const newCollection: DataModels.Collection = { ...this.collection.rawDataModel };
if (this.state.isSubSettingsSaveable || this.state.isIndexingPolicyDirty || this.state.isConflictResolutionDirty) { if (
this.state.isSubSettingsSaveable ||
this.state.isIndexingPolicyDirty ||
this.state.isConflictResolutionDirty ||
this.state.isComputedPropertiesDirty
) {
let defaultTtl: number; let defaultTtl: number;
switch (this.state.timeToLive) { switch (this.state.timeToLive) {
case TtlType.On: case TtlType.On:
@ -813,6 +871,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
newCollection.conflictResolutionPolicy = conflictResolutionChanges; newCollection.conflictResolutionPolicy = conflictResolutionChanges;
} }
if (this.state.isComputedPropertiesDirty) {
newCollection.computedProperties = this.state.computedPropertiesContent;
}
const updatedCollection: DataModels.Collection = await updateCollection( const updatedCollection: DataModels.Collection = await updateCollection(
this.collection.databaseId, this.collection.databaseId,
this.collection.id(), this.collection.id(),
@ -826,6 +888,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
this.collection.conflictResolutionPolicy(updatedCollection.conflictResolutionPolicy); this.collection.conflictResolutionPolicy(updatedCollection.conflictResolutionPolicy);
this.collection.changeFeedPolicy(updatedCollection.changeFeedPolicy); this.collection.changeFeedPolicy(updatedCollection.changeFeedPolicy);
this.collection.geospatialConfig(updatedCollection.geospatialConfig); this.collection.geospatialConfig(updatedCollection.geospatialConfig);
this.collection.computedProperties(updatedCollection.computedProperties);
if (wasIndexingPolicyModified) { if (wasIndexingPolicyModified) {
await this.refreshIndexTransformationProgress(); await this.refreshIndexTransformationProgress();
@ -836,6 +899,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
isSubSettingsDiscardable: false, isSubSettingsDiscardable: false,
isIndexingPolicyDirty: false, isIndexingPolicyDirty: false,
isConflictResolutionDirty: false, isConflictResolutionDirty: false,
isComputedPropertiesDirty: false,
}); });
} }
@ -1028,6 +1092,16 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
onMongoIndexingPolicyDiscardableChange: this.onMongoIndexingPolicyDiscardableChange, onMongoIndexingPolicyDiscardableChange: this.onMongoIndexingPolicyDiscardableChange,
}; };
const computedPropertiesComponentProps: ComputedPropertiesComponentProps = {
computedPropertiesContent: this.state.computedPropertiesContent,
computedPropertiesContentBaseline: this.state.computedPropertiesContentBaseline,
logComputedPropertiesSuccessMessage: this.logComputedPropertiesSuccessMessage,
onComputedPropertiesContentChange: this.onComputedPropertiesContentChange,
onComputedPropertiesDirtyChange: this.onComputedPropertiesDirtyChange,
resetShouldDiscardComputedProperties: this.resetShouldDiscardComputedProperties,
shouldDiscardComputedProperties: this.state.shouldDiscardComputedProperties,
};
const conflictResolutionPolicyComponentProps: ConflictResolutionComponentProps = { const conflictResolutionPolicyComponentProps: ConflictResolutionComponentProps = {
collection: this.collection, collection: this.collection,
conflictResolutionPolicyMode: this.state.conflictResolutionPolicyMode, conflictResolutionPolicyMode: this.state.conflictResolutionPolicyMode,
@ -1070,6 +1144,13 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
} }
} }
if (this.shouldShowComputedPropertiesEditor) {
tabs.push({
tab: SettingsV2TabTypes.ComputedPropertiesTab,
content: <ComputedPropertiesComponent {...computedPropertiesComponentProps} />,
});
}
if (this.hasConflictResolution()) { if (this.hasConflictResolution()) {
tabs.push({ tabs.push({
tab: SettingsV2TabTypes.ConflictResolutionTab, tab: SettingsV2TabTypes.ConflictResolutionTab,

View File

@ -60,6 +60,8 @@ export interface PriceBreakdown {
currencySign: string; currencySign: string;
} }
export type editorType = "indexPolicy" | "computedProperties";
export const infoAndToolTipTextStyle: ITextStyles = { root: { fontSize: 14, color: "windowtext" } }; export const infoAndToolTipTextStyle: ITextStyles = { root: { fontSize: 14, color: "windowtext" } };
export const noLeftPaddingCheckBoxStyle: ICheckboxStyles = { export const noLeftPaddingCheckBoxStyle: ICheckboxStyles = {
@ -253,9 +255,10 @@ export const ttlWarning: JSX.Element = (
</Text> </Text>
); );
export const indexingPolicynUnsavedWarningMessage: JSX.Element = ( export const unsavedEditorWarningMessage = (editor: editorType): JSX.Element => (
<Text styles={infoAndToolTipTextStyle}> <Text styles={infoAndToolTipTextStyle}>
You have not saved the latest changes made to your indexing policy. Please click save to confirm the changes. You have not saved the latest changes made to your{" "}
{editor == "indexPolicy" ? "indexing policy" : "computed properties"}. Please click save to confirm the changes.
</Text> </Text>
); );

View File

@ -0,0 +1,56 @@
import * as DataModels from "Contracts/DataModels";
import { shallow } from "enzyme";
import React from "react";
import { ComputedPropertiesComponent, ComputedPropertiesComponentProps } from "./ComputedPropertiesComponent";
describe("ComputedPropertiesComponent", () => {
const initialComputedPropertiesContent: DataModels.ComputedProperties = [
{
name: "prop1",
query: "query1",
},
];
const baseProps: ComputedPropertiesComponentProps = {
computedPropertiesContent: initialComputedPropertiesContent,
computedPropertiesContentBaseline: initialComputedPropertiesContent,
logComputedPropertiesSuccessMessage: () => {
return;
},
onComputedPropertiesContentChange: () => {
return;
},
onComputedPropertiesDirtyChange: () => {
return;
},
resetShouldDiscardComputedProperties: () => {
return;
},
shouldDiscardComputedProperties: false,
};
it("renders", () => {
const wrapper = shallow(<ComputedPropertiesComponent {...baseProps} />);
expect(wrapper).toMatchSnapshot();
});
it("computed properties are reset", () => {
const wrapper = shallow(<ComputedPropertiesComponent {...baseProps} />);
const computedPropertiesComponentInstance = wrapper.instance() as ComputedPropertiesComponent;
const resetComputedPropertiesEditorMockFn = jest.fn();
computedPropertiesComponentInstance.resetComputedPropertiesEditor = resetComputedPropertiesEditorMockFn;
wrapper.setProps({ shouldDiscardComputedProperties: true });
wrapper.update();
expect(resetComputedPropertiesEditorMockFn.mock.calls.length).toEqual(1);
});
it("dirty is set", () => {
let computedPropertiesComponent = new ComputedPropertiesComponent(baseProps);
expect(computedPropertiesComponent.IsComponentDirty()).toEqual(false);
const newProps = { ...baseProps, computedPropertiesContent: undefined as DataModels.ComputedProperties };
computedPropertiesComponent = new ComputedPropertiesComponent(newProps);
expect(computedPropertiesComponent.IsComponentDirty()).toEqual(true);
});
});

View File

@ -0,0 +1,125 @@
// CTODO: Render/edit CP in monaco
// CTODO: Get CPs from RP
// CTODO: Save CPs to RP
import { MessageBar, MessageBarType, Stack } from "@fluentui/react";
import * as DataModels from "Contracts/DataModels";
import { titleAndInputStackProps, unsavedEditorWarningMessage } from "Explorer/Controls/Settings/SettingsRenderUtils";
import { isDirty } from "Explorer/Controls/Settings/SettingsUtils";
import { loadMonaco } from "Explorer/LazyMonaco";
import * as monaco from "monaco-editor";
import * as React from "react";
export interface ComputedPropertiesComponentProps {
computedPropertiesContent: DataModels.ComputedProperties;
computedPropertiesContentBaseline: DataModels.ComputedProperties;
logComputedPropertiesSuccessMessage: () => void;
onComputedPropertiesContentChange: (newComputedProperties: DataModels.ComputedProperties) => void;
onComputedPropertiesDirtyChange: (isComputedPropertiesDirty: boolean) => void;
resetShouldDiscardComputedProperties: () => void;
shouldDiscardComputedProperties: boolean;
}
interface ComputedPropertiesComponentState {
computedPropertiesContentIsValid: boolean;
}
export class ComputedPropertiesComponent extends React.Component<
ComputedPropertiesComponentProps,
ComputedPropertiesComponentState
> {
private shouldCheckComponentIsDirty = true;
private computedPropertiesDiv = React.createRef<HTMLDivElement>();
private computedPropertiesEditor: monaco.editor.IStandaloneCodeEditor;
constructor(props: ComputedPropertiesComponentProps) {
super(props);
this.state = {
computedPropertiesContentIsValid: true,
};
}
componentDidUpdate(): void {
if (this.props.shouldDiscardComputedProperties) {
this.resetComputedPropertiesEditor();
this.props.resetShouldDiscardComputedProperties();
}
this.onComponentUpdate();
}
componentDidMount(): void {
this.resetComputedPropertiesEditor();
this.onComponentUpdate();
}
public resetComputedPropertiesEditor = (): void => {
if (!this.computedPropertiesEditor) {
this.createComputedPropertiesEditor();
} else {
const indexingPolicyEditorModel = this.computedPropertiesEditor.getModel();
const value: string = JSON.stringify(this.props.computedPropertiesContent, undefined, 4);
indexingPolicyEditorModel.setValue(value);
}
this.onComponentUpdate();
};
private onComponentUpdate = (): void => {
if (!this.shouldCheckComponentIsDirty) {
this.shouldCheckComponentIsDirty = true;
return;
}
this.props.onComputedPropertiesDirtyChange(this.IsComponentDirty());
this.shouldCheckComponentIsDirty = false;
};
public IsComponentDirty = (): boolean => {
if (
isDirty(this.props.computedPropertiesContent, this.props.computedPropertiesContentBaseline) &&
this.state.computedPropertiesContentIsValid
) {
return true;
}
return false;
};
private async createComputedPropertiesEditor(): Promise<void> {
const value: string = JSON.stringify(this.props.computedPropertiesContent, undefined, 4);
const monaco = await loadMonaco();
this.computedPropertiesEditor = monaco.editor.create(this.computedPropertiesDiv.current, {
value: value,
language: "json",
ariaLabel: "Indexing Policy",
});
if (this.computedPropertiesEditor) {
const computedPropertiesEditorModel = this.computedPropertiesEditor.getModel();
computedPropertiesEditorModel.onDidChangeContent(this.onEditorContentChange.bind(this));
this.props.logComputedPropertiesSuccessMessage();
}
}
private onEditorContentChange = (): void => {
const computedPropertiesEditorModel = this.computedPropertiesEditor.getModel();
try {
const newComputedPropertiesContent = JSON.parse(
computedPropertiesEditorModel.getValue()
) as DataModels.ComputedProperties;
this.props.onComputedPropertiesContentChange(newComputedPropertiesContent);
this.setState({ computedPropertiesContentIsValid: true });
} catch (e) {
this.setState({ computedPropertiesContentIsValid: false });
}
};
public render(): JSX.Element {
return (
<Stack {...titleAndInputStackProps}>
{isDirty(this.props.computedPropertiesContent, this.props.computedPropertiesContentBaseline) && (
<MessageBar messageBarType={MessageBarType.warning}>
{unsavedEditorWarningMessage("computedProperties")}
</MessageBar>
)}
<div className="settingsV2IndexingPolicyEditor" tabIndex={0} ref={this.computedPropertiesDiv}></div>
</Stack>
);
}
}

View File

@ -3,7 +3,7 @@ import * as monaco from "monaco-editor";
import * as React from "react"; import * as React from "react";
import * as DataModels from "../../../../Contracts/DataModels"; import * as DataModels from "../../../../Contracts/DataModels";
import { loadMonaco } from "../../../LazyMonaco"; import { loadMonaco } from "../../../LazyMonaco";
import { indexingPolicynUnsavedWarningMessage, titleAndInputStackProps } from "../SettingsRenderUtils"; import { titleAndInputStackProps, unsavedEditorWarningMessage } from "../SettingsRenderUtils";
import { isDirty, isIndexTransforming } from "../SettingsUtils"; import { isDirty, isIndexTransforming } from "../SettingsUtils";
import { IndexingPolicyRefreshComponent } from "./IndexingPolicyRefresh/IndexingPolicyRefreshComponent"; import { IndexingPolicyRefreshComponent } from "./IndexingPolicyRefresh/IndexingPolicyRefreshComponent";
@ -120,7 +120,7 @@ export class IndexingPolicyComponent extends React.Component<
refreshIndexTransformationProgress={this.props.refreshIndexTransformationProgress} refreshIndexTransformationProgress={this.props.refreshIndexTransformationProgress}
/> />
{isDirty(this.props.indexingPolicyContent, this.props.indexingPolicyContentBaseline) && ( {isDirty(this.props.indexingPolicyContent, this.props.indexingPolicyContentBaseline) && (
<MessageBar messageBarType={MessageBarType.warning}>{indexingPolicynUnsavedWarningMessage}</MessageBar> <MessageBar messageBarType={MessageBarType.warning}>{unsavedEditorWarningMessage("indexPolicy")}</MessageBar>
)} )}
<div className="settingsV2IndexingPolicyEditor" tabIndex={0} ref={this.indexingPolicyDiv}></div> <div className="settingsV2IndexingPolicyEditor" tabIndex={0} ref={this.indexingPolicyDiv}></div>
</Stack> </Stack>

View File

@ -19,7 +19,6 @@ import {
addMongoIndexStackProps, addMongoIndexStackProps,
createAndAddMongoIndexStackProps, createAndAddMongoIndexStackProps,
customDetailsListStyles, customDetailsListStyles,
indexingPolicynUnsavedWarningMessage,
infoAndToolTipTextStyle, infoAndToolTipTextStyle,
mediumWidthStackStyles, mediumWidthStackStyles,
mongoCompoundIndexNotSupportedMessage, mongoCompoundIndexNotSupportedMessage,
@ -27,15 +26,16 @@ import {
onRenderRow, onRenderRow,
separatorStyles, separatorStyles,
subComponentStackProps, subComponentStackProps,
unsavedEditorWarningMessage,
} from "../../SettingsRenderUtils"; } from "../../SettingsRenderUtils";
import { import {
AddMongoIndexProps, AddMongoIndexProps,
getMongoIndexType,
getMongoIndexTypeText,
isIndexTransforming,
MongoIndexIdField, MongoIndexIdField,
MongoIndexTypes, MongoIndexTypes,
MongoNotificationType, MongoNotificationType,
getMongoIndexType,
getMongoIndexTypeText,
isIndexTransforming,
} from "../../SettingsUtils"; } from "../../SettingsUtils";
import { IndexingPolicyRefreshComponent } from "../IndexingPolicyRefresh/IndexingPolicyRefreshComponent"; import { IndexingPolicyRefreshComponent } from "../IndexingPolicyRefresh/IndexingPolicyRefreshComponent";
import { AddMongoIndexComponent } from "./AddMongoIndexComponent"; import { AddMongoIndexComponent } from "./AddMongoIndexComponent";
@ -297,7 +297,7 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
if (this.getMongoWarningNotificationMessage()) { if (this.getMongoWarningNotificationMessage()) {
warningMessage = this.getMongoWarningNotificationMessage(); warningMessage = this.getMongoWarningNotificationMessage();
} else if (this.isMongoIndexingPolicySaveable()) { } else if (this.isMongoIndexingPolicySaveable()) {
warningMessage = indexingPolicynUnsavedWarningMessage; warningMessage = unsavedEditorWarningMessage("indexPolicy");
} }
return ( return (

View File

@ -0,0 +1,16 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ComputedPropertiesComponent renders 1`] = `
<Stack
tokens={
Object {
"childrenGap": 5,
}
}
>
<div
className="settingsV2IndexingPolicyEditor"
tabIndex={0}
/>
</Stack>
`;

View File

@ -4,7 +4,7 @@ import * as ViewModels from "../../../Contracts/ViewModels";
import { MongoIndex } from "../../../Utils/arm/generatedClients/cosmos/types"; import { MongoIndex } from "../../../Utils/arm/generatedClients/cosmos/types";
const zeroValue = 0; const zeroValue = 0;
export type isDirtyTypes = boolean | string | number | DataModels.IndexingPolicy; export type isDirtyTypes = boolean | string | number | DataModels.IndexingPolicy | DataModels.ComputedProperties;
export const TtlOff = "off"; export const TtlOff = "off";
export const TtlOn = "on"; export const TtlOn = "on";
export const TtlOnNoDefault = "on-nodefault"; export const TtlOnNoDefault = "on-nodefault";
@ -45,6 +45,7 @@ export enum SettingsV2TabTypes {
ConflictResolutionTab, ConflictResolutionTab,
SubSettingsTab, SubSettingsTab,
IndexingPolicyTab, IndexingPolicyTab,
ComputedPropertiesTab,
} }
export interface IsComponentDirtyResult { export interface IsComponentDirtyResult {
@ -146,6 +147,8 @@ export const getTabTitle = (tab: SettingsV2TabTypes): string => {
return "Settings"; return "Settings";
case SettingsV2TabTypes.IndexingPolicyTab: case SettingsV2TabTypes.IndexingPolicyTab:
return "Indexing Policy"; return "Indexing Policy";
case SettingsV2TabTypes.ComputedPropertiesTab:
return "Computed Properties";
default: default:
throw new Error(`Unknown tab ${tab}`); throw new Error(`Unknown tab ${tab}`);
} }

View File

@ -40,6 +40,12 @@ export const collection = ({
version: 2, version: 2,
}, },
partitionKeyProperties: ["partitionKey"], partitionKeyProperties: ["partitionKey"],
computedProperties: ko.observable<DataModels.ComputedProperties>([
{
name: "queryName",
query: "query",
},
]),
readSettings: () => { readSettings: () => {
return; return;
}, },

View File

@ -26,6 +26,7 @@ exports[`SettingsComponent renders 1`] = `
Object { Object {
"analyticalStorageTtl": [Function], "analyticalStorageTtl": [Function],
"changeFeedPolicy": [Function], "changeFeedPolicy": [Function],
"computedProperties": [Function],
"conflictResolutionPolicy": [Function], "conflictResolutionPolicy": [Function],
"container": Explorer { "container": Explorer {
"_isInitializingNotebooks": false, "_isInitializingNotebooks": false,
@ -103,6 +104,7 @@ exports[`SettingsComponent renders 1`] = `
Object { Object {
"analyticalStorageTtl": [Function], "analyticalStorageTtl": [Function],
"changeFeedPolicy": [Function], "changeFeedPolicy": [Function],
"computedProperties": [Function],
"conflictResolutionPolicy": [Function], "conflictResolutionPolicy": [Function],
"container": Explorer { "container": Explorer {
"_isInitializingNotebooks": false, "_isInitializingNotebooks": false,
@ -202,6 +204,40 @@ exports[`SettingsComponent renders 1`] = `
shouldDiscardIndexingPolicy={false} shouldDiscardIndexingPolicy={false}
/> />
</PivotItem> </PivotItem>
<PivotItem
headerText="Computed Properties"
itemKey="ComputedPropertiesTab"
key="ComputedPropertiesTab"
style={
Object {
"marginTop": 20,
}
}
>
<ComputedPropertiesComponent
computedPropertiesContent={
Array [
Object {
"name": "queryName",
"query": "query",
},
]
}
computedPropertiesContentBaseline={
Array [
Object {
"name": "queryName",
"query": "query",
},
]
}
logComputedPropertiesSuccessMessage={[Function]}
onComputedPropertiesContentChange={[Function]}
onComputedPropertiesDirtyChange={[Function]}
resetShouldDiscardComputedProperties={[Function]}
shouldDiscardComputedProperties={false}
/>
</PivotItem>
</StyledPivot> </StyledPivot>
</div> </div>
</div> </div>

View File

@ -99,18 +99,6 @@ exports[`SettingsUtils functions render 1`] = `
</StyledLinkBase> </StyledLinkBase>
. .
</Text> </Text>
<Text
styles={
Object {
"root": Object {
"color": "windowtext",
"fontSize": 14,
},
}
}
>
You have not saved the latest changes made to your indexing policy. Please click save to confirm the changes.
</Text>
<Text <Text
id="updateThroughputDelayedApplyWarningMessage" id="updateThroughputDelayedApplyWarningMessage"
styles={ styles={

View File

@ -3,6 +3,9 @@ import { useNotebook } from "Explorer/Notebook/useNotebook";
import * as ko from "knockout"; import * as ko from "knockout";
import * as _ from "underscore"; import * as _ from "underscore";
import * as Constants from "../../Common/Constants"; import * as Constants from "../../Common/Constants";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import * as Logger from "../../Common/Logger";
import { fetchPortalNotifications } from "../../Common/PortalNotifications";
import { bulkCreateDocument } from "../../Common/dataAccess/bulkCreateDocument"; import { bulkCreateDocument } from "../../Common/dataAccess/bulkCreateDocument";
import { createDocument } from "../../Common/dataAccess/createDocument"; import { createDocument } from "../../Common/dataAccess/createDocument";
import { getCollectionUsageSizeInKB } from "../../Common/dataAccess/getCollectionDataUsageSize"; import { getCollectionUsageSizeInKB } from "../../Common/dataAccess/getCollectionDataUsageSize";
@ -10,19 +13,16 @@ import { readCollectionOffer } from "../../Common/dataAccess/readCollectionOffer
import { readStoredProcedures } from "../../Common/dataAccess/readStoredProcedures"; import { readStoredProcedures } from "../../Common/dataAccess/readStoredProcedures";
import { readTriggers } from "../../Common/dataAccess/readTriggers"; import { readTriggers } from "../../Common/dataAccess/readTriggers";
import { readUserDefinedFunctions } from "../../Common/dataAccess/readUserDefinedFunctions"; import { readUserDefinedFunctions } from "../../Common/dataAccess/readUserDefinedFunctions";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import * as Logger from "../../Common/Logger";
import { fetchPortalNotifications } from "../../Common/PortalNotifications";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { UploadDetailsRecord } from "../../Contracts/ViewModels"; import { UploadDetailsRecord } from "../../Contracts/ViewModels";
import { useTabs } from "../../hooks/useTabs";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import { SqlTriggerResource } from "../../Utils/arm/generatedClients/cosmos/types";
import { isServerlessAccount } from "../../Utils/CapabilityUtils"; import { isServerlessAccount } from "../../Utils/CapabilityUtils";
import { logConsoleInfo } from "../../Utils/NotificationConsoleUtils"; import { logConsoleInfo } from "../../Utils/NotificationConsoleUtils";
import { SqlTriggerResource } from "../../Utils/arm/generatedClients/cosmos/types";
import { useTabs } from "../../hooks/useTabs";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter"; import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
import { CassandraAPIDataClient, CassandraTableKey, CassandraTableKeys } from "../Tables/TableDataClient"; import { CassandraAPIDataClient, CassandraTableKey, CassandraTableKeys } from "../Tables/TableDataClient";
@ -57,6 +57,7 @@ export default class Collection implements ViewModels.Collection {
public indexingPolicy: ko.Observable<DataModels.IndexingPolicy>; public indexingPolicy: ko.Observable<DataModels.IndexingPolicy>;
public uniqueKeyPolicy: DataModels.UniqueKeyPolicy; public uniqueKeyPolicy: DataModels.UniqueKeyPolicy;
public usageSizeInKB: ko.Observable<number>; public usageSizeInKB: ko.Observable<number>;
public computedProperties: ko.Observable<DataModels.ComputedProperties>;
public offer: ko.Observable<DataModels.Offer>; public offer: ko.Observable<DataModels.Offer>;
public conflictResolutionPolicy: ko.Observable<DataModels.ConflictResolutionPolicy>; public conflictResolutionPolicy: ko.Observable<DataModels.ConflictResolutionPolicy>;
@ -120,6 +121,7 @@ export default class Collection implements ViewModels.Collection {
this.schema = data.schema; this.schema = data.schema;
this.requestSchema = data.requestSchema; this.requestSchema = data.requestSchema;
this.geospatialConfig = ko.observable(data.geospatialConfig); this.geospatialConfig = ko.observable(data.geospatialConfig);
this.computedProperties = ko.observable(data.computedProperties);
this.partitionKeyPropertyHeaders = this.partitionKey?.paths; this.partitionKeyPropertyHeaders = this.partitionKey?.paths;
this.partitionKeyProperties = this.partitionKeyPropertyHeaders?.map((partitionKeyPropertyHeader, i) => { this.partitionKeyProperties = this.partitionKeyPropertyHeaders?.map((partitionKeyPropertyHeader, i) => {

View File

@ -1045,6 +1045,8 @@ export interface SqlContainerResource {
/* Analytical TTL. */ /* Analytical TTL. */
analyticalStorageTtl?: number; analyticalStorageTtl?: number;
computedProperties?: ComputedProperties;
} }
/* Cosmos DB indexing policy */ /* Cosmos DB indexing policy */
@ -1067,6 +1069,13 @@ export interface IndexingPolicy {
spatialIndexes?: SpatialSpec[]; spatialIndexes?: SpatialSpec[];
} }
export type ComputedProperties = ComputedProperty[];
export interface ComputedProperty {
name?: string;
query?: string;
}
/* undocumented */ /* undocumented */
export interface ExcludedPath { export interface ExcludedPath {
/* The path for which the indexing behavior applies to. Index paths typically start with root and end with wildcard (/path/*) */ /* The path for which the indexing behavior applies to. Index paths typically start with root and end with wildcard (/path/*) */