mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-19 17:01:13 +00:00
Adding throughput bucket settings in Data Explorer (#2044)
* Added throughput bucketing * fix bugs * enable/disable per autoscale selection * Added logic * change query bucket to group * Updated to a tab * Fixed unit tests * Edit package-lock * Compile build fix * fix unit tests * moving the throughput bucket flag to the client generation level
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import { AuthType } from "AuthType";
|
||||
import { shallow } from "enzyme";
|
||||
import ko from "knockout";
|
||||
import { Features } from "Platform/Hosted/extractFeatures";
|
||||
import React from "react";
|
||||
import { updateCollection } from "../../../Common/dataAccess/updateCollection";
|
||||
import { updateOffer } from "../../../Common/dataAccess/updateOffer";
|
||||
@@ -247,4 +249,42 @@ describe("SettingsComponent", () => {
|
||||
expect(conflictResolutionPolicy.mode).toEqual(DataModels.ConflictResolutionMode.Custom);
|
||||
expect(conflictResolutionPolicy.conflictResolutionProcedure).toEqual(expectSprocPath);
|
||||
});
|
||||
|
||||
it("should save throughput bucket changes when Save button is clicked", async () => {
|
||||
updateUserContext({
|
||||
apiType: "SQL",
|
||||
features: { enableThroughputBuckets: true } as Features,
|
||||
authType: AuthType.AAD,
|
||||
});
|
||||
|
||||
const wrapper = shallow(<SettingsComponent {...baseProps} />);
|
||||
|
||||
const settingsComponentInstance = wrapper.instance() as SettingsComponent;
|
||||
const isEnabled = settingsComponentInstance["throughputBucketsEnabled"];
|
||||
expect(isEnabled).toBe(true);
|
||||
|
||||
wrapper.setState({
|
||||
isThroughputBucketsSaveable: true,
|
||||
throughputBuckets: [
|
||||
{ id: 1, maxThroughputPercentage: 70 },
|
||||
{ id: 2, maxThroughputPercentage: 60 },
|
||||
],
|
||||
});
|
||||
|
||||
await settingsComponentInstance.onSaveClick();
|
||||
|
||||
expect(updateOffer).toHaveBeenCalledWith({
|
||||
databaseId: collection.databaseId,
|
||||
collectionId: collection.id(),
|
||||
currentOffer: expect.any(Object),
|
||||
autopilotThroughput: collection.offer().autoscaleMaxThroughput,
|
||||
manualThroughput: collection.offer().manualThroughput,
|
||||
throughputBuckets: [
|
||||
{ id: 1, maxThroughputPercentage: 70 },
|
||||
{ id: 2, maxThroughputPercentage: 60 },
|
||||
],
|
||||
});
|
||||
|
||||
expect(wrapper.state("isThroughputBucketsSaveable")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,10 @@ import {
|
||||
ContainerPolicyComponent,
|
||||
ContainerPolicyComponentProps,
|
||||
} from "Explorer/Controls/Settings/SettingsSubComponents/ContainerPolicyComponent";
|
||||
import {
|
||||
ThroughputBucketsComponent,
|
||||
ThroughputBucketsComponentProps,
|
||||
} from "Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputBucketsComponent";
|
||||
import { useDatabases } from "Explorer/useDatabases";
|
||||
import { isFullTextSearchEnabled, isVectorSearchEnabled } from "Utils/CapabilityUtils";
|
||||
import { isRunningOnPublicCloud } from "Utils/CloudUtils";
|
||||
@@ -86,6 +90,8 @@ export interface SettingsComponentState {
|
||||
wasAutopilotOriginallySet: boolean;
|
||||
isScaleSaveable: boolean;
|
||||
isScaleDiscardable: boolean;
|
||||
throughputBuckets: DataModels.ThroughputBucket[];
|
||||
throughputBucketsBaseline: DataModels.ThroughputBucket[];
|
||||
throughputError: string;
|
||||
|
||||
timeToLive: TtlType;
|
||||
@@ -104,6 +110,7 @@ export interface SettingsComponentState {
|
||||
changeFeedPolicyBaseline: ChangeFeedPolicyState;
|
||||
isSubSettingsSaveable: boolean;
|
||||
isSubSettingsDiscardable: boolean;
|
||||
isThroughputBucketsSaveable: boolean;
|
||||
|
||||
vectorEmbeddingPolicy: DataModels.VectorEmbeddingPolicy;
|
||||
vectorEmbeddingPolicyBaseline: DataModels.VectorEmbeddingPolicy;
|
||||
@@ -158,6 +165,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
private isVectorSearchEnabled: boolean;
|
||||
private isFullTextSearchEnabled: boolean;
|
||||
private totalThroughputUsed: number;
|
||||
private throughputBucketsEnabled: boolean;
|
||||
public mongoDBCollectionResource: MongoDBCollectionResource;
|
||||
|
||||
constructor(props: SettingsComponentProps) {
|
||||
@@ -175,6 +183,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
this.isFullTextSearchEnabled = isFullTextSearchEnabled() && !hasDatabaseSharedThroughput(this.collection);
|
||||
|
||||
this.changeFeedPolicyVisible = userContext.features.enableChangeFeedPolicy;
|
||||
this.throughputBucketsEnabled =
|
||||
userContext.apiType === "SQL" &&
|
||||
userContext.features.enableThroughputBuckets &&
|
||||
userContext.authType === AuthType.AAD;
|
||||
|
||||
// Mongo container with system partition key still treat as "Fixed"
|
||||
this.isFixedContainer =
|
||||
@@ -193,6 +205,8 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
wasAutopilotOriginallySet: false,
|
||||
isScaleSaveable: false,
|
||||
isScaleDiscardable: false,
|
||||
throughputBuckets: undefined,
|
||||
throughputBucketsBaseline: undefined,
|
||||
throughputError: undefined,
|
||||
|
||||
timeToLive: undefined,
|
||||
@@ -211,6 +225,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
changeFeedPolicyBaseline: undefined,
|
||||
isSubSettingsSaveable: false,
|
||||
isSubSettingsDiscardable: false,
|
||||
isThroughputBucketsSaveable: false,
|
||||
|
||||
vectorEmbeddingPolicy: undefined,
|
||||
vectorEmbeddingPolicyBaseline: undefined,
|
||||
@@ -327,7 +342,8 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
this.state.isIndexingPolicyDirty ||
|
||||
this.state.isConflictResolutionDirty ||
|
||||
this.state.isComputedPropertiesDirty ||
|
||||
(!!this.state.currentMongoIndexes && this.state.isMongoIndexingPolicySaveable)
|
||||
(!!this.state.currentMongoIndexes && this.state.isMongoIndexingPolicySaveable) ||
|
||||
this.state.isThroughputBucketsSaveable
|
||||
);
|
||||
};
|
||||
|
||||
@@ -339,7 +355,8 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
this.state.isIndexingPolicyDirty ||
|
||||
this.state.isConflictResolutionDirty ||
|
||||
this.state.isComputedPropertiesDirty ||
|
||||
(!!this.state.currentMongoIndexes && this.state.isMongoIndexingPolicyDiscardable)
|
||||
(!!this.state.currentMongoIndexes && this.state.isMongoIndexingPolicyDiscardable) ||
|
||||
this.state.isThroughputBucketsSaveable
|
||||
);
|
||||
};
|
||||
|
||||
@@ -419,6 +436,8 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
|
||||
this.setState({
|
||||
throughput: this.state.throughputBaseline,
|
||||
throughputBuckets: this.state.throughputBucketsBaseline,
|
||||
throughputBucketsBaseline: this.state.throughputBucketsBaseline,
|
||||
timeToLive: this.state.timeToLiveBaseline,
|
||||
timeToLiveSeconds: this.state.timeToLiveSecondsBaseline,
|
||||
displayedTtlSeconds: this.state.displayedTtlSecondsBaseline,
|
||||
@@ -441,6 +460,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
isScaleSaveable: false,
|
||||
isScaleDiscardable: false,
|
||||
isSubSettingsSaveable: false,
|
||||
isThroughputBucketsSaveable: false,
|
||||
isSubSettingsDiscardable: false,
|
||||
isContainerPolicyDirty: false,
|
||||
isIndexingPolicyDirty: false,
|
||||
@@ -479,6 +499,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
private onIndexingPolicyContentChange = (newIndexingPolicy: DataModels.IndexingPolicy): void =>
|
||||
this.setState({ indexingPolicyContent: newIndexingPolicy });
|
||||
|
||||
private onThroughputBucketsSaveableChange = (isSaveable: boolean): void => {
|
||||
this.setState({ isThroughputBucketsSaveable: isSaveable });
|
||||
};
|
||||
|
||||
private resetShouldDiscardContainerPolicies = (): void => this.setState({ shouldDiscardContainerPolicies: false });
|
||||
|
||||
private resetShouldDiscardIndexingPolicy = (): void => this.setState({ shouldDiscardIndexingPolicy: false });
|
||||
@@ -749,9 +773,13 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
] as DataModels.ComputedProperties;
|
||||
}
|
||||
|
||||
const throughputBuckets = this.offer?.throughputBuckets;
|
||||
|
||||
return {
|
||||
throughput: offerThroughput,
|
||||
throughputBaseline: offerThroughput,
|
||||
throughputBuckets,
|
||||
throughputBucketsBaseline: throughputBuckets,
|
||||
changeFeedPolicy: changeFeedPolicy,
|
||||
changeFeedPolicyBaseline: changeFeedPolicy,
|
||||
timeToLive: timeToLive,
|
||||
@@ -839,6 +867,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
this.setState({ throughput: newThroughput, throughputError });
|
||||
};
|
||||
|
||||
private onThroughputBucketChange = (throughputBuckets: DataModels.ThroughputBucket[]): void => {
|
||||
this.setState({ throughputBuckets });
|
||||
};
|
||||
|
||||
private onAutoPilotSelected = (isAutoPilotSelected: boolean): void =>
|
||||
this.setState({ isAutoPilotSelected: isAutoPilotSelected });
|
||||
|
||||
@@ -1029,6 +1061,24 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
}
|
||||
}
|
||||
|
||||
if (this.throughputBucketsEnabled && this.state.isThroughputBucketsSaveable) {
|
||||
const updatedOffer: DataModels.Offer = await updateOffer({
|
||||
databaseId: this.collection.databaseId,
|
||||
collectionId: this.collection.id(),
|
||||
currentOffer: this.collection.offer(),
|
||||
autopilotThroughput: this.collection.offer().autoscaleMaxThroughput
|
||||
? this.collection.offer().autoscaleMaxThroughput
|
||||
: undefined,
|
||||
manualThroughput: this.collection.offer().manualThroughput
|
||||
? this.collection.offer().manualThroughput
|
||||
: undefined,
|
||||
throughputBuckets: this.state.throughputBuckets,
|
||||
});
|
||||
this.collection.offer(updatedOffer);
|
||||
this.offer = updatedOffer;
|
||||
this.setState({ isThroughputBucketsSaveable: false });
|
||||
}
|
||||
|
||||
if (this.state.isScaleSaveable) {
|
||||
const updateOfferParams: DataModels.UpdateOfferParams = {
|
||||
databaseId: this.collection.databaseId,
|
||||
@@ -1209,6 +1259,13 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
onConflictResolutionDirtyChange: this.onConflictResolutionDirtyChange,
|
||||
};
|
||||
|
||||
const throughputBucketsComponentProps: ThroughputBucketsComponentProps = {
|
||||
currentBuckets: this.state.throughputBuckets,
|
||||
throughputBucketsBaseline: this.state.throughputBucketsBaseline,
|
||||
onBucketsChange: this.onThroughputBucketChange,
|
||||
onSaveableChange: this.onThroughputBucketsSaveableChange,
|
||||
};
|
||||
|
||||
const partitionKeyComponentProps: PartitionKeyComponentProps = {
|
||||
database: useDatabases.getState().findDatabaseWithId(this.collection.databaseId),
|
||||
collection: this.collection,
|
||||
@@ -1271,6 +1328,13 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
});
|
||||
}
|
||||
|
||||
if (this.throughputBucketsEnabled) {
|
||||
tabs.push({
|
||||
tab: SettingsV2TabTypes.ThroughputBucketsTab,
|
||||
content: <ThroughputBucketsComponent {...throughputBucketsComponentProps} />,
|
||||
});
|
||||
}
|
||||
|
||||
const pivotProps: IPivotProps = {
|
||||
onLinkClick: this.onPivotChange,
|
||||
selectedKey: SettingsV2TabTypes[this.state.selectedTab],
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
import "@testing-library/jest-dom";
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import React from "react";
|
||||
import { ThroughputBucketsComponent } from "./ThroughputBucketsComponent";
|
||||
|
||||
describe("ThroughputBucketsComponent", () => {
|
||||
const mockOnBucketsChange = jest.fn();
|
||||
const mockOnSaveableChange = jest.fn();
|
||||
|
||||
const defaultProps = {
|
||||
currentBuckets: [
|
||||
{ id: 1, maxThroughputPercentage: 50 },
|
||||
{ id: 2, maxThroughputPercentage: 60 },
|
||||
],
|
||||
throughputBucketsBaseline: [
|
||||
{ id: 1, maxThroughputPercentage: 40 },
|
||||
{ id: 2, maxThroughputPercentage: 50 },
|
||||
],
|
||||
onBucketsChange: mockOnBucketsChange,
|
||||
onSaveableChange: mockOnSaveableChange,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it("renders the correct number of buckets", () => {
|
||||
render(<ThroughputBucketsComponent {...defaultProps} />);
|
||||
expect(screen.getAllByText(/Group \d+/)).toHaveLength(5);
|
||||
});
|
||||
|
||||
it("renders buckets in the correct order even if input is unordered", () => {
|
||||
const unorderedBuckets = [
|
||||
{ id: 2, maxThroughputPercentage: 60 },
|
||||
{ id: 1, maxThroughputPercentage: 50 },
|
||||
];
|
||||
render(<ThroughputBucketsComponent {...defaultProps} currentBuckets={unorderedBuckets} />);
|
||||
|
||||
const bucketLabels = screen.getAllByText(/Group \d+/).map((el) => el.textContent);
|
||||
expect(bucketLabels).toEqual(["Group 1 (Data Explorer Query Bucket)", "Group 2", "Group 3", "Group 4", "Group 5"]);
|
||||
});
|
||||
|
||||
it("renders all provided buckets even if they exceed the max default bucket count", () => {
|
||||
const oversizedBuckets = [
|
||||
{ id: 1, maxThroughputPercentage: 50 },
|
||||
{ id: 2, maxThroughputPercentage: 60 },
|
||||
{ id: 3, maxThroughputPercentage: 70 },
|
||||
{ id: 4, maxThroughputPercentage: 80 },
|
||||
{ id: 5, maxThroughputPercentage: 90 },
|
||||
{ id: 6, maxThroughputPercentage: 100 },
|
||||
{ id: 7, maxThroughputPercentage: 40 },
|
||||
];
|
||||
|
||||
render(<ThroughputBucketsComponent {...defaultProps} currentBuckets={oversizedBuckets} />);
|
||||
|
||||
expect(screen.getAllByText(/Group \d+/)).toHaveLength(7);
|
||||
|
||||
expect(screen.getByDisplayValue("50")).toBeInTheDocument();
|
||||
expect(screen.getByDisplayValue("60")).toBeInTheDocument();
|
||||
expect(screen.getByDisplayValue("70")).toBeInTheDocument();
|
||||
expect(screen.getByDisplayValue("80")).toBeInTheDocument();
|
||||
expect(screen.getByDisplayValue("90")).toBeInTheDocument();
|
||||
expect(screen.getByDisplayValue("100")).toBeInTheDocument();
|
||||
expect(screen.getByDisplayValue("40")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("calls onBucketsChange when a bucket value changes", () => {
|
||||
render(<ThroughputBucketsComponent {...defaultProps} />);
|
||||
const input = screen.getByDisplayValue("50");
|
||||
fireEvent.change(input, { target: { value: "70" } });
|
||||
|
||||
expect(mockOnBucketsChange).toHaveBeenCalledWith([
|
||||
{ id: 1, maxThroughputPercentage: 70 },
|
||||
{ id: 2, maxThroughputPercentage: 60 },
|
||||
{ id: 3, maxThroughputPercentage: 100 },
|
||||
{ id: 4, maxThroughputPercentage: 100 },
|
||||
{ id: 5, maxThroughputPercentage: 100 },
|
||||
]);
|
||||
});
|
||||
|
||||
it("triggers onSaveableChange when values change", () => {
|
||||
render(<ThroughputBucketsComponent {...defaultProps} />);
|
||||
const input = screen.getByDisplayValue("50");
|
||||
fireEvent.change(input, { target: { value: "80" } });
|
||||
|
||||
expect(mockOnSaveableChange).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it("updates state consistently after multiple changes to different buckets", () => {
|
||||
render(<ThroughputBucketsComponent {...defaultProps} />);
|
||||
|
||||
const input1 = screen.getByDisplayValue("50");
|
||||
fireEvent.change(input1, { target: { value: "70" } });
|
||||
|
||||
const input2 = screen.getByDisplayValue("60");
|
||||
fireEvent.change(input2, { target: { value: "80" } });
|
||||
|
||||
expect(mockOnBucketsChange).toHaveBeenCalledWith([
|
||||
{ id: 1, maxThroughputPercentage: 70 },
|
||||
{ id: 2, maxThroughputPercentage: 80 },
|
||||
{ id: 3, maxThroughputPercentage: 100 },
|
||||
{ id: 4, maxThroughputPercentage: 100 },
|
||||
{ id: 5, maxThroughputPercentage: 100 },
|
||||
]);
|
||||
});
|
||||
|
||||
it("resets to baseline when currentBuckets are reset", () => {
|
||||
const { rerender } = render(<ThroughputBucketsComponent {...defaultProps} />);
|
||||
const input1 = screen.getByDisplayValue("50");
|
||||
fireEvent.change(input1, { target: { value: "70" } });
|
||||
|
||||
rerender(<ThroughputBucketsComponent {...defaultProps} currentBuckets={defaultProps.throughputBucketsBaseline} />);
|
||||
|
||||
expect(screen.getByDisplayValue("40")).toBeInTheDocument();
|
||||
expect(screen.getByDisplayValue("50")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not call onBucketsChange when value remains unchanged", () => {
|
||||
render(<ThroughputBucketsComponent {...defaultProps} />);
|
||||
const input = screen.getByDisplayValue("50");
|
||||
fireEvent.change(input, { target: { value: "50" } });
|
||||
|
||||
expect(mockOnBucketsChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("disables input and slider when maxThroughputPercentage is 100", () => {
|
||||
render(
|
||||
<ThroughputBucketsComponent
|
||||
{...defaultProps}
|
||||
currentBuckets={[
|
||||
{ id: 1, maxThroughputPercentage: 100 },
|
||||
{ id: 2, maxThroughputPercentage: 50 },
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
|
||||
const disabledInputs = screen.getAllByDisplayValue("100");
|
||||
expect(disabledInputs.length).toBeGreaterThan(0);
|
||||
expect(disabledInputs[0]).toBeDisabled();
|
||||
|
||||
const sliders = screen.getAllByRole("slider");
|
||||
expect(sliders.length).toBeGreaterThan(0);
|
||||
expect(sliders[0]).toHaveAttribute("aria-disabled", "true");
|
||||
expect(sliders[1]).toHaveAttribute("aria-disabled", "false");
|
||||
});
|
||||
|
||||
it("toggles bucket value between 50 and 100 with switch", () => {
|
||||
render(<ThroughputBucketsComponent {...defaultProps} />);
|
||||
const toggles = screen.getAllByRole("switch");
|
||||
|
||||
fireEvent.click(toggles[0]);
|
||||
|
||||
expect(mockOnBucketsChange).toHaveBeenCalledWith([
|
||||
{ id: 1, maxThroughputPercentage: 100 },
|
||||
{ id: 2, maxThroughputPercentage: 60 },
|
||||
{ id: 3, maxThroughputPercentage: 100 },
|
||||
{ id: 4, maxThroughputPercentage: 100 },
|
||||
{ id: 5, maxThroughputPercentage: 100 },
|
||||
]);
|
||||
|
||||
fireEvent.click(toggles[0]);
|
||||
|
||||
expect(mockOnBucketsChange).toHaveBeenCalledWith([
|
||||
{ id: 1, maxThroughputPercentage: 50 },
|
||||
{ id: 2, maxThroughputPercentage: 60 },
|
||||
{ id: 3, maxThroughputPercentage: 100 },
|
||||
{ id: 4, maxThroughputPercentage: 100 },
|
||||
{ id: 5, maxThroughputPercentage: 100 },
|
||||
]);
|
||||
});
|
||||
|
||||
it("ensures default buckets are used when no buckets are provided", () => {
|
||||
render(<ThroughputBucketsComponent {...defaultProps} currentBuckets={[]} />);
|
||||
expect(screen.getAllByText(/Group \d+/)).toHaveLength(5);
|
||||
expect(screen.getAllByDisplayValue("100")).toHaveLength(5);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,105 @@
|
||||
import { Label, Slider, Stack, TextField, Toggle } from "@fluentui/react";
|
||||
import { ThroughputBucket } from "Contracts/DataModels";
|
||||
import React, { FC, useEffect, useState } from "react";
|
||||
import { isDirty } from "../../SettingsUtils";
|
||||
|
||||
const MAX_BUCKET_SIZES = 5;
|
||||
|
||||
const DEFAULT_BUCKETS = Array.from({ length: MAX_BUCKET_SIZES }, (_, i) => ({
|
||||
id: i + 1,
|
||||
maxThroughputPercentage: 100,
|
||||
}));
|
||||
|
||||
export interface ThroughputBucketsComponentProps {
|
||||
currentBuckets: ThroughputBucket[];
|
||||
throughputBucketsBaseline: ThroughputBucket[];
|
||||
onBucketsChange: (updatedBuckets: ThroughputBucket[]) => void;
|
||||
onSaveableChange: (isSaveable: boolean) => void;
|
||||
}
|
||||
|
||||
export const ThroughputBucketsComponent: FC<ThroughputBucketsComponentProps> = ({
|
||||
currentBuckets,
|
||||
throughputBucketsBaseline,
|
||||
onBucketsChange,
|
||||
onSaveableChange,
|
||||
}) => {
|
||||
const getThroughputBuckets = (buckets: ThroughputBucket[]): ThroughputBucket[] => {
|
||||
if (!buckets || buckets.length === 0) {
|
||||
return DEFAULT_BUCKETS;
|
||||
}
|
||||
const maxBuckets = Math.max(DEFAULT_BUCKETS.length, buckets.length);
|
||||
const adjustedDefaultBuckets = Array.from({ length: maxBuckets }, (_, i) => ({
|
||||
id: i + 1,
|
||||
maxThroughputPercentage: 100,
|
||||
}));
|
||||
|
||||
return adjustedDefaultBuckets.map(
|
||||
(defaultBucket) => buckets?.find((bucket) => bucket.id === defaultBucket.id) || defaultBucket,
|
||||
);
|
||||
};
|
||||
|
||||
const [throughputBuckets, setThroughputBuckets] = useState<ThroughputBucket[]>(getThroughputBuckets(currentBuckets));
|
||||
|
||||
useEffect(() => {
|
||||
setThroughputBuckets(getThroughputBuckets(currentBuckets));
|
||||
onSaveableChange(false);
|
||||
}, [currentBuckets]);
|
||||
|
||||
useEffect(() => {
|
||||
const isChanged = isDirty(throughputBuckets, getThroughputBuckets(throughputBucketsBaseline));
|
||||
onSaveableChange(isChanged);
|
||||
}, [throughputBuckets]);
|
||||
|
||||
const handleBucketChange = (id: number, newValue: number) => {
|
||||
const updatedBuckets = throughputBuckets.map((bucket) =>
|
||||
bucket.id === id ? { ...bucket, maxThroughputPercentage: newValue } : bucket,
|
||||
);
|
||||
setThroughputBuckets(updatedBuckets);
|
||||
const settingsChanged = isDirty(updatedBuckets, throughputBuckets);
|
||||
settingsChanged && onBucketsChange(updatedBuckets);
|
||||
};
|
||||
|
||||
const onToggle = (id: number, checked: boolean) => {
|
||||
handleBucketChange(id, checked ? 50 : 100);
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack tokens={{ childrenGap: "m" }} styles={{ root: { width: "70%", maxWidth: 700 } }}>
|
||||
<Label>Throughput Buckets</Label>
|
||||
<Stack>
|
||||
{throughputBuckets?.map((bucket) => (
|
||||
<Stack key={bucket.id} horizontal tokens={{ childrenGap: 8 }} verticalAlign="center">
|
||||
<Slider
|
||||
min={1}
|
||||
max={100}
|
||||
step={1}
|
||||
value={bucket.maxThroughputPercentage}
|
||||
onChange={(newValue) => handleBucketChange(bucket.id, newValue)}
|
||||
showValue={false}
|
||||
label={`Group ${bucket.id}${bucket.id === 1 ? " (Data Explorer Query Bucket)" : ""}`}
|
||||
styles={{ root: { flex: 2, maxWidth: 400 } }}
|
||||
disabled={bucket.maxThroughputPercentage === 100}
|
||||
/>
|
||||
<TextField
|
||||
value={bucket.maxThroughputPercentage.toString()}
|
||||
onChange={(event, newValue) => handleBucketChange(bucket.id, parseInt(newValue || "0", 10))}
|
||||
type="number"
|
||||
suffix="%"
|
||||
styles={{
|
||||
fieldGroup: { width: 80 },
|
||||
}}
|
||||
disabled={bucket.maxThroughputPercentage === 100}
|
||||
/>
|
||||
<Toggle
|
||||
onText="Active"
|
||||
offText="Inactive"
|
||||
checked={bucket.maxThroughputPercentage !== 100}
|
||||
onChange={(event, checked) => onToggle(bucket.id, checked)}
|
||||
styles={{ root: { marginBottom: 0 }, text: { fontSize: 12 } }}
|
||||
></Toggle>
|
||||
</Stack>
|
||||
))}
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -11,7 +11,8 @@ export type isDirtyTypes =
|
||||
| DataModels.IndexingPolicy
|
||||
| DataModels.ComputedProperties
|
||||
| DataModels.VectorEmbedding[]
|
||||
| DataModels.FullTextPolicy;
|
||||
| DataModels.FullTextPolicy
|
||||
| DataModels.ThroughputBucket[];
|
||||
export const TtlOff = "off";
|
||||
export const TtlOn = "on";
|
||||
export const TtlOnNoDefault = "on-nodefault";
|
||||
@@ -55,6 +56,7 @@ export enum SettingsV2TabTypes {
|
||||
PartitionKeyTab,
|
||||
ComputedPropertiesTab,
|
||||
ContainerVectorPolicyTab,
|
||||
ThroughputBucketsTab,
|
||||
}
|
||||
|
||||
export enum ContainerPolicyTabTypes {
|
||||
@@ -167,6 +169,8 @@ export const getTabTitle = (tab: SettingsV2TabTypes): string => {
|
||||
return "Computed Properties";
|
||||
case SettingsV2TabTypes.ContainerVectorPolicyTab:
|
||||
return "Container Policies";
|
||||
case SettingsV2TabTypes.ThroughputBucketsTab:
|
||||
return "Throughput Buckets";
|
||||
default:
|
||||
throw new Error(`Unknown tab ${tab}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user