mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-23 02:41:39 +00:00
Compare commits
10 Commits
users/aisa
...
users/sind
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e904e305e7 | ||
|
|
96822af37f | ||
|
|
4801aae754 | ||
|
|
2a02112d87 | ||
|
|
bbfff77495 | ||
|
|
f695b42071 | ||
|
|
a8a96e22b4 | ||
|
|
a1b026544d | ||
|
|
a912233b33 | ||
|
|
487130f6e3 |
@@ -1482,6 +1482,9 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
itemKey: SettingsV2TabTypes[tab.tab],
|
||||
style: { marginTop: 20 },
|
||||
headerText: getTabTitle(tab.tab),
|
||||
headerButtonProps: {
|
||||
"data-test": `settings-tab-header/${SettingsV2TabTypes[tab.tab]}`,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -127,9 +127,9 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
||||
};
|
||||
|
||||
private ttlChoiceGroupOptions: IChoiceGroupOption[] = [
|
||||
{ key: TtlType.Off, text: "Off" },
|
||||
{ key: TtlType.OnNoDefault, text: "On (no default)" },
|
||||
{ key: TtlType.On, text: "On" },
|
||||
{ key: TtlType.Off, text: "Off", ariaLabel: "ttl-off-option" },
|
||||
{ key: TtlType.OnNoDefault, text: "On (no default)", ariaLabel: "ttl-on-no-default-option" },
|
||||
{ key: TtlType.On, text: "On", ariaLabel: "ttl-on-option" },
|
||||
];
|
||||
|
||||
public getTtlValue = (value: string): TtlType => {
|
||||
@@ -223,6 +223,7 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
||||
onChange={this.onTimeToLiveSecondsChange}
|
||||
suffix="second(s)"
|
||||
ariaLabel={`Time to live in seconds`}
|
||||
data-test="ttl-input"
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
@@ -503,7 +503,9 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
<span style={{ float: "left", transform: "translateX(-50%)" }}>
|
||||
{this.props.instantMaximumThroughput.toLocaleString()}
|
||||
</span>
|
||||
<span style={{ float: "right" }}>{this.props.softAllowedMaximumThroughput.toLocaleString()}</span>
|
||||
<span style={{ float: "right" }} data-test="soft-allowed-maximum-throughput">
|
||||
{this.props.softAllowedMaximumThroughput.toLocaleString()}
|
||||
</span>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
<ProgressIndicator
|
||||
@@ -626,11 +628,12 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
min={autoPilotThroughput1K}
|
||||
onGetErrorMessage={(value: string) => {
|
||||
const sanitizedValue = getSanitizedInputValue(value);
|
||||
return sanitizedValue % 1000
|
||||
? "Throughput value must be in increments of 1000"
|
||||
: this.props.throughputError;
|
||||
const errorMessage: string =
|
||||
sanitizedValue % 1000 ? "Throughput value must be in increments of 1000" : this.props.throughputError;
|
||||
return <span data-test="autopilot-throughput-input-error">{errorMessage}</span>;
|
||||
}}
|
||||
validateOnLoad={false}
|
||||
data-test="autopilot-throughput-input"
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
@@ -650,7 +653,10 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
}
|
||||
onChange={this.onThroughputChange}
|
||||
min={this.props.minimum}
|
||||
errorMessage={this.props.throughputError}
|
||||
onGetErrorMessage={() => {
|
||||
return <span data-test="manual-throughput-input-error">{this.props.throughputError}</span>;
|
||||
}}
|
||||
data-test="manual-throughput-input"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -273,6 +273,7 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
|
||||
/>
|
||||
</Stack>
|
||||
<StyledTextFieldBase
|
||||
data-test="autopilot-throughput-input"
|
||||
disabled={true}
|
||||
id="autopilotInput"
|
||||
key="auto pilot throughput input"
|
||||
@@ -333,6 +334,7 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
|
||||
5,000
|
||||
</span>
|
||||
<span
|
||||
data-test="soft-allowed-maximum-throughput"
|
||||
style={
|
||||
{
|
||||
"float": "right",
|
||||
@@ -752,11 +754,13 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
|
||||
}
|
||||
>
|
||||
<StyledTextFieldBase
|
||||
data-test="manual-throughput-input"
|
||||
disabled={false}
|
||||
id="throughputInput"
|
||||
key="provisioned throughput input"
|
||||
min={10000}
|
||||
onChange={[Function]}
|
||||
onGetErrorMessage={[Function]}
|
||||
required={true}
|
||||
step={100}
|
||||
styles={
|
||||
@@ -811,6 +815,7 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
|
||||
5,000
|
||||
</span>
|
||||
<span
|
||||
data-test="soft-allowed-maximum-throughput"
|
||||
style={
|
||||
{
|
||||
"float": "right",
|
||||
@@ -1206,11 +1211,13 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = `
|
||||
}
|
||||
>
|
||||
<StyledTextFieldBase
|
||||
data-test="manual-throughput-input"
|
||||
disabled={false}
|
||||
id="throughputInput"
|
||||
key="provisioned throughput input"
|
||||
min={10000}
|
||||
onChange={[Function]}
|
||||
onGetErrorMessage={[Function]}
|
||||
required={true}
|
||||
step={100}
|
||||
styles={
|
||||
@@ -1265,6 +1272,7 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = `
|
||||
5,000
|
||||
</span>
|
||||
<span
|
||||
data-test="soft-allowed-maximum-throughput"
|
||||
style={
|
||||
{
|
||||
"float": "right",
|
||||
|
||||
@@ -22,14 +22,17 @@ exports[`SubSettingsComponent analyticalTimeToLive hidden 1`] = `
|
||||
options={
|
||||
[
|
||||
{
|
||||
"ariaLabel": "ttl-off-option",
|
||||
"key": "off",
|
||||
"text": "Off",
|
||||
},
|
||||
{
|
||||
"ariaLabel": "ttl-on-no-default-option",
|
||||
"key": "on-nodefault",
|
||||
"text": "On (no default)",
|
||||
},
|
||||
{
|
||||
"ariaLabel": "ttl-on-option",
|
||||
"key": "on",
|
||||
"text": "On",
|
||||
},
|
||||
@@ -63,6 +66,7 @@ exports[`SubSettingsComponent analyticalTimeToLive hidden 1`] = `
|
||||
/>
|
||||
<StyledTextFieldBase
|
||||
ariaLabel="Time to live in seconds"
|
||||
data-test="ttl-input"
|
||||
id="timeToLiveSeconds"
|
||||
max={2147483647}
|
||||
min={1}
|
||||
@@ -284,14 +288,17 @@ exports[`SubSettingsComponent analyticalTimeToLiveSeconds hidden 1`] = `
|
||||
options={
|
||||
[
|
||||
{
|
||||
"ariaLabel": "ttl-off-option",
|
||||
"key": "off",
|
||||
"text": "Off",
|
||||
},
|
||||
{
|
||||
"ariaLabel": "ttl-on-no-default-option",
|
||||
"key": "on-nodefault",
|
||||
"text": "On (no default)",
|
||||
},
|
||||
{
|
||||
"ariaLabel": "ttl-on-option",
|
||||
"key": "on",
|
||||
"text": "On",
|
||||
},
|
||||
@@ -325,6 +332,7 @@ exports[`SubSettingsComponent analyticalTimeToLiveSeconds hidden 1`] = `
|
||||
/>
|
||||
<StyledTextFieldBase
|
||||
ariaLabel="Time to live in seconds"
|
||||
data-test="ttl-input"
|
||||
id="timeToLiveSeconds"
|
||||
max={2147483647}
|
||||
min={1}
|
||||
@@ -601,14 +609,17 @@ exports[`SubSettingsComponent changeFeedPolicy hidden 1`] = `
|
||||
options={
|
||||
[
|
||||
{
|
||||
"ariaLabel": "ttl-off-option",
|
||||
"key": "off",
|
||||
"text": "Off",
|
||||
},
|
||||
{
|
||||
"ariaLabel": "ttl-on-no-default-option",
|
||||
"key": "on-nodefault",
|
||||
"text": "On (no default)",
|
||||
},
|
||||
{
|
||||
"ariaLabel": "ttl-on-option",
|
||||
"key": "on",
|
||||
"text": "On",
|
||||
},
|
||||
@@ -642,6 +653,7 @@ exports[`SubSettingsComponent changeFeedPolicy hidden 1`] = `
|
||||
/>
|
||||
<StyledTextFieldBase
|
||||
ariaLabel="Time to live in seconds"
|
||||
data-test="ttl-input"
|
||||
id="timeToLiveSeconds"
|
||||
max={2147483647}
|
||||
min={1}
|
||||
@@ -878,14 +890,17 @@ exports[`SubSettingsComponent renders 1`] = `
|
||||
options={
|
||||
[
|
||||
{
|
||||
"ariaLabel": "ttl-off-option",
|
||||
"key": "off",
|
||||
"text": "Off",
|
||||
},
|
||||
{
|
||||
"ariaLabel": "ttl-on-no-default-option",
|
||||
"key": "on-nodefault",
|
||||
"text": "On (no default)",
|
||||
},
|
||||
{
|
||||
"ariaLabel": "ttl-on-option",
|
||||
"key": "on",
|
||||
"text": "On",
|
||||
},
|
||||
@@ -919,6 +934,7 @@ exports[`SubSettingsComponent renders 1`] = `
|
||||
/>
|
||||
<StyledTextFieldBase
|
||||
ariaLabel="Time to live in seconds"
|
||||
data-test="ttl-input"
|
||||
id="timeToLiveSeconds"
|
||||
max={2147483647}
|
||||
min={1}
|
||||
@@ -1220,14 +1236,17 @@ exports[`SubSettingsComponent timeToLiveSeconds hidden 1`] = `
|
||||
options={
|
||||
[
|
||||
{
|
||||
"ariaLabel": "ttl-off-option",
|
||||
"key": "off",
|
||||
"text": "Off",
|
||||
},
|
||||
{
|
||||
"ariaLabel": "ttl-on-no-default-option",
|
||||
"key": "on-nodefault",
|
||||
"text": "On (no default)",
|
||||
},
|
||||
{
|
||||
"ariaLabel": "ttl-on-option",
|
||||
"key": "on",
|
||||
"text": "On",
|
||||
},
|
||||
|
||||
@@ -12,6 +12,11 @@ exports[`SettingsComponent renders 1`] = `
|
||||
selectedKey="ScaleTab"
|
||||
>
|
||||
<PivotItem
|
||||
headerButtonProps={
|
||||
{
|
||||
"data-test": "settings-tab-header/ScaleTab",
|
||||
}
|
||||
}
|
||||
headerText="Scale"
|
||||
itemKey="ScaleTab"
|
||||
key="ScaleTab"
|
||||
@@ -102,6 +107,11 @@ exports[`SettingsComponent renders 1`] = `
|
||||
/>
|
||||
</PivotItem>
|
||||
<PivotItem
|
||||
headerButtonProps={
|
||||
{
|
||||
"data-test": "settings-tab-header/SubSettingsTab",
|
||||
}
|
||||
}
|
||||
headerText="Settings"
|
||||
itemKey="SubSettingsTab"
|
||||
key="SubSettingsTab"
|
||||
@@ -201,6 +211,11 @@ exports[`SettingsComponent renders 1`] = `
|
||||
/>
|
||||
</PivotItem>
|
||||
<PivotItem
|
||||
headerButtonProps={
|
||||
{
|
||||
"data-test": "settings-tab-header/ContainerVectorPolicyTab",
|
||||
}
|
||||
}
|
||||
headerText="Container Policies"
|
||||
itemKey="ContainerVectorPolicyTab"
|
||||
key="ContainerVectorPolicyTab"
|
||||
@@ -227,6 +242,11 @@ exports[`SettingsComponent renders 1`] = `
|
||||
/>
|
||||
</PivotItem>
|
||||
<PivotItem
|
||||
headerButtonProps={
|
||||
{
|
||||
"data-test": "settings-tab-header/IndexingPolicyTab",
|
||||
}
|
||||
}
|
||||
headerText="Indexing Policy"
|
||||
itemKey="IndexingPolicyTab"
|
||||
key="IndexingPolicyTab"
|
||||
@@ -263,6 +283,11 @@ exports[`SettingsComponent renders 1`] = `
|
||||
/>
|
||||
</PivotItem>
|
||||
<PivotItem
|
||||
headerButtonProps={
|
||||
{
|
||||
"data-test": "settings-tab-header/PartitionKeyTab",
|
||||
}
|
||||
}
|
||||
headerText="Partition Keys (preview)"
|
||||
itemKey="PartitionKeyTab"
|
||||
key="PartitionKeyTab"
|
||||
@@ -370,6 +395,11 @@ exports[`SettingsComponent renders 1`] = `
|
||||
/>
|
||||
</PivotItem>
|
||||
<PivotItem
|
||||
headerButtonProps={
|
||||
{
|
||||
"data-test": "settings-tab-header/ComputedPropertiesTab",
|
||||
}
|
||||
}
|
||||
headerText="Computed Properties"
|
||||
itemKey="ComputedPropertiesTab"
|
||||
key="ComputedPropertiesTab"
|
||||
@@ -404,6 +434,11 @@ exports[`SettingsComponent renders 1`] = `
|
||||
/>
|
||||
</PivotItem>
|
||||
<PivotItem
|
||||
headerButtonProps={
|
||||
{
|
||||
"data-test": "settings-tab-header/GlobalSecondaryIndexTab",
|
||||
}
|
||||
}
|
||||
headerText="Global Secondary Index (Preview)"
|
||||
itemKey="GlobalSecondaryIndexTab"
|
||||
key="GlobalSecondaryIndexTab"
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Environment, getEnvironment } from "Common/EnvironmentUtility";
|
||||
import { KeyboardAction } from "KeyboardShortcuts";
|
||||
import { isDataplaneRbacSupported } from "Utils/APITypeUtils";
|
||||
import * as React from "react";
|
||||
@@ -239,10 +238,6 @@ function areScriptsSupported(): boolean {
|
||||
}
|
||||
|
||||
function createOpenSynapseLinkDialogButton(container: Explorer): CommandButtonComponentProps {
|
||||
if ([Environment.Prod, Environment.Mpac].includes(getEnvironment())) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (configContext.platform === Platform.Emulator) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@ export class NotificationConsoleComponent extends React.Component<
|
||||
</span>
|
||||
</span>
|
||||
<span className="consoleSplitter" />
|
||||
<span className="headerStatus">
|
||||
<span className="headerStatus" data-test="notification-console/header-status">
|
||||
<span className="headerStatusEllipsis" aria-live="assertive" aria-atomic="true">
|
||||
{this.state.headerStatus}
|
||||
</span>
|
||||
|
||||
@@ -78,6 +78,7 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
|
||||
/>
|
||||
<span
|
||||
className="headerStatus"
|
||||
data-test="notification-console/header-status"
|
||||
>
|
||||
<span
|
||||
aria-atomic="true"
|
||||
@@ -261,6 +262,7 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
|
||||
/>
|
||||
<span
|
||||
className="headerStatus"
|
||||
data-test="notification-console/header-status"
|
||||
>
|
||||
<span
|
||||
aria-atomic="true"
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
Icon,
|
||||
IconButton,
|
||||
IDropdownOption,
|
||||
IRenderFunction,
|
||||
Link,
|
||||
ProgressIndicator,
|
||||
Separator,
|
||||
@@ -18,7 +19,6 @@ import {
|
||||
import * as Constants from "Common/Constants";
|
||||
import { createCollection } from "Common/dataAccess/createCollection";
|
||||
import { getNewDatabaseSharedThroughputDefault } from "Common/DatabaseUtility";
|
||||
import { getEnvironment } from "Common/EnvironmentUtility";
|
||||
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
|
||||
import { configContext, Platform } from "ConfigContext";
|
||||
import * as DataModels from "Contracts/DataModels";
|
||||
@@ -161,7 +161,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
|
||||
render(): JSX.Element {
|
||||
const isFirstResourceCreated = useDatabases.getState().isFirstResourceCreated();
|
||||
console.log(getEnvironment())
|
||||
|
||||
return (
|
||||
<form className="panelFormWrapper" onSubmit={this.submit.bind(this)} id="panelContainer">
|
||||
{this.state.errorMessage && (
|
||||
@@ -406,6 +406,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
}
|
||||
defaultSelectedKey={this.props.databaseId}
|
||||
responsiveMode={999}
|
||||
onRenderOption={this.onRenderDatabaseOption}
|
||||
/>
|
||||
)}
|
||||
<Separator className="panelSeparator" style={{ marginTop: -4, marginBottom: -4 }} />
|
||||
@@ -691,8 +692,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||
content={`You can optionally provision dedicated throughput for a ${getCollectionName().toLocaleLowerCase()} within a database that has throughput
|
||||
provisioned. This dedicated throughput amount will not be shared with other ${getCollectionName(
|
||||
true,
|
||||
).toLocaleLowerCase()} in the database and
|
||||
true,
|
||||
).toLocaleLowerCase()} in the database and
|
||||
does not count towards the throughput you provisioned for the database. This throughput amount will be
|
||||
billed in addition to the throughput amount you provisioned at the database level.`}
|
||||
>
|
||||
@@ -702,8 +703,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
tabIndex={0}
|
||||
ariaLabel={`You can optionally provision dedicated throughput for a ${getCollectionName().toLocaleLowerCase()} within a database that has throughput
|
||||
provisioned. This dedicated throughput amount will not be shared with other ${getCollectionName(
|
||||
true,
|
||||
).toLocaleLowerCase()} in the database and
|
||||
true,
|
||||
).toLocaleLowerCase()} in the database and
|
||||
does not count towards the throughput you provisioned for the database. This throughput amount will be
|
||||
billed in addition to the throughput amount you provisioned at the database level.`}
|
||||
/>
|
||||
@@ -882,8 +883,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
onExpand={() => {
|
||||
scrollToSection("collapsibleFullTextPolicySectionContent");
|
||||
}}
|
||||
//TODO: uncomment when learn more text becomes available
|
||||
// tooltipContent={this.getContainerFullTextPolicyTooltipContent()}
|
||||
//TODO: uncomment when learn more text becomes available
|
||||
// tooltipContent={this.getContainerFullTextPolicyTooltipContent()}
|
||||
>
|
||||
<Stack id="collapsibleFullTextPolicySectionContent" styles={{ root: { position: "relative" } }}>
|
||||
<Stack styles={{ root: { paddingLeft: 40 } }}>
|
||||
@@ -1301,15 +1302,15 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
const partitionKeyVersion = this.state.useHashV1 ? undefined : 2;
|
||||
const partitionKey: DataModels.PartitionKey = partitionKeyString
|
||||
? {
|
||||
paths: [
|
||||
partitionKeyString,
|
||||
...(userContext.apiType === "SQL" && this.state.subPartitionKeys.length > 0
|
||||
? this.state.subPartitionKeys
|
||||
: []),
|
||||
],
|
||||
kind: userContext.apiType === "SQL" && this.state.subPartitionKeys.length > 0 ? "MultiHash" : "Hash",
|
||||
version: partitionKeyVersion,
|
||||
}
|
||||
paths: [
|
||||
partitionKeyString,
|
||||
...(userContext.apiType === "SQL" && this.state.subPartitionKeys.length > 0
|
||||
? this.state.subPartitionKeys
|
||||
: []),
|
||||
],
|
||||
kind: userContext.apiType === "SQL" && this.state.subPartitionKeys.length > 0 ? "MultiHash" : "Hash",
|
||||
version: partitionKeyVersion,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const indexingPolicy: DataModels.IndexingPolicy = this.state.enableIndexing
|
||||
@@ -1434,4 +1435,19 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
TelemetryProcessor.traceFailure(Action.CreateCollection, failureTelemetryData, startKey);
|
||||
}
|
||||
}
|
||||
|
||||
private onRenderDatabaseOption = (
|
||||
option?: IDropdownOption,
|
||||
defaultRender?: (props?: IDropdownOption) => JSX.Element,
|
||||
): JSX.Element | null => {
|
||||
if (!option) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div data-testid={`database-option-${option.key}`}>
|
||||
{defaultRender ? defaultRender(option) : <span>{option.text}</span>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { DirectionalHint, Icon, Link, Stack, Text, TooltipHost } from "@fluentui/react";
|
||||
import * as Constants from "Common/Constants";
|
||||
import { Environment, getEnvironment } from "Common/EnvironmentUtility";
|
||||
import { configContext, Platform } from "ConfigContext";
|
||||
import * as DataModels from "Contracts/DataModels";
|
||||
import { getFullTextLanguageOptions } from "Explorer/Controls/FullTextSeach/FullTextPoliciesComponent";
|
||||
@@ -86,10 +85,7 @@ export function UniqueKeysHeader(): JSX.Element {
|
||||
}
|
||||
|
||||
export function shouldShowAnalyticalStoreOptions(): boolean {
|
||||
if (
|
||||
[Environment.Mpac, Environment.Prod].includes(getEnvironment()) ||
|
||||
isFabricNative() ||
|
||||
configContext.platform === Platform.Emulator) {
|
||||
if (isFabricNative() || configContext.platform === Platform.Emulator) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -40,13 +40,13 @@ To use this script, there are a few prerequisites that must be done at least onc
|
||||
5. Ensure you have a Resource Group _ready_ to deploy into, the deploy script requires an existing resource group. This resource group should be named `[username]-e2e-testing`, where `[username]` is your Windows username, (**Microsoft employees:** This should be your alias). The easiest way to do this is by running the `create-resource-group.ps1` script, specifying the Subscription (Name or ID) and Location in which you want to create the Resource Group. For example:
|
||||
|
||||
```powershell
|
||||
.\test\resources\create-resource-group.ps1 -SubscriptionName "My Subscription" -Location "West US 3"
|
||||
.\test\resources\create-resource-group.ps1 -SubscriptionId "My Subscription Id" -Location "West US 3"
|
||||
```
|
||||
|
||||
Then, whenever you want to create/update the resources, you can run the `deploy.ps1` script in the `resources` directory. As long as you're using the default naming convention (`[username]-e2e-testing`), you just need to specify the Subscription. For example:
|
||||
|
||||
```powershell
|
||||
.\test\resources\deploy.ps1 -SubscriptionName "My Subscription"
|
||||
.\test\resources\deploy.ps1 -Subscription "My Subscription"
|
||||
```
|
||||
|
||||
You'll get a confirmation prompt before anything is deployed:
|
||||
|
||||
31
test/fx.ts
31
test/fx.ts
@@ -1,6 +1,7 @@
|
||||
import { DefaultAzureCredential } from "@azure/identity";
|
||||
import { Frame, Locator, Page, expect } from "@playwright/test";
|
||||
import crypto from "crypto";
|
||||
import { TestContainerContext, TestDatabaseContext } from "./testData";
|
||||
|
||||
const RETRY_COUNT = 3;
|
||||
|
||||
@@ -55,6 +56,11 @@ export const defaultAccounts: Record<TestAccount, string> = {
|
||||
export const resourceGroupName = process.env.DE_TEST_RESOURCE_GROUP ?? "de-e2e-tests";
|
||||
export const subscriptionId = process.env.DE_TEST_SUBSCRIPTION_ID ?? "69e02f2d-f059-4409-9eac-97e8a276ae2c";
|
||||
export const TEST_AUTOSCALE_THROUGHPUT_RU = 1000;
|
||||
export const TEST_MANUAL_THROUGHPUT_RU = 800;
|
||||
export const TEST_AUTOSCALE_MAX_THROUGHPUT_RU_2K = 2000;
|
||||
export const TEST_AUTOSCALE_MAX_THROUGHPUT_RU_4K = 4000;
|
||||
export const TEST_MANUAL_THROUGHPUT_RU_2K = 2000;
|
||||
export const ONE_MINUTE_MS: number = 60 * 1000;
|
||||
|
||||
function tryGetStandardName(accountType: TestAccount) {
|
||||
if (process.env.DE_TEST_ACCOUNT_PREFIX) {
|
||||
@@ -319,6 +325,11 @@ type PanelOpenOptions = {
|
||||
closeTimeout?: number;
|
||||
};
|
||||
|
||||
export enum CommandBarButton {
|
||||
Save = "Save",
|
||||
ExecuteQuery = "Execute Query",
|
||||
}
|
||||
|
||||
/** Helper class that provides locator methods for DataExplorer components, on top of a Frame */
|
||||
export class DataExplorer {
|
||||
constructor(public frame: Frame) {}
|
||||
@@ -348,8 +359,8 @@ export class DataExplorer {
|
||||
}
|
||||
|
||||
/** Select the command bar button with the specified label */
|
||||
commandBarButton(label: string): Locator {
|
||||
return this.frame.getByTestId(`CommandBar/Button:${label}`).and(this.frame.locator("css=button"));
|
||||
commandBarButton(commandBarButton: CommandBarButton): Locator {
|
||||
return this.frame.getByTestId(`CommandBar/Button:${commandBarButton}`).and(this.frame.locator("css=button"));
|
||||
}
|
||||
|
||||
dialogButton(label: string): Locator {
|
||||
@@ -445,6 +456,22 @@ export class DataExplorer {
|
||||
await panel.waitFor({ state: "detached", timeout: options.closeTimeout });
|
||||
}
|
||||
|
||||
/** Opens the Scale & Settings panel for the specified container */
|
||||
async openScaleAndSettings(context: TestContainerContext): Promise<void> {
|
||||
const containerNode = await this.waitForContainerNode(context.database.id, context.container.id);
|
||||
await containerNode.expand();
|
||||
|
||||
const scaleAndSettingsButton = this.frame.getByTestId(
|
||||
`TreeNode:${context.database.id}/${context.container.id}/Scale & Settings`,
|
||||
);
|
||||
await scaleAndSettingsButton.click();
|
||||
}
|
||||
|
||||
/** Gets the console message element */
|
||||
getConsoleMessage(): Locator {
|
||||
return this.frame.getByTestId("notification-console/header-status");
|
||||
}
|
||||
|
||||
/** Waits for the Data Explorer app to load */
|
||||
static async waitForExplorer(page: Page) {
|
||||
const iframeElement = await page.getByTestId("DataExplorerFrame").elementHandle();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
import { DataExplorer, Editor, QueryTab, TestAccount } from "../fx";
|
||||
import { CommandBarButton, DataExplorer, Editor, QueryTab, TestAccount } from "../fx";
|
||||
import { TestContainerContext, TestItem, createTestSQLContainer } from "../testData";
|
||||
|
||||
let context: TestContainerContext = null!;
|
||||
@@ -37,7 +37,7 @@ test.afterAll("Delete Test Database", async () => {
|
||||
test("Query results", async () => {
|
||||
// Run the query and verify the results
|
||||
await queryEditor.locator.click();
|
||||
const executeQueryButton = explorer.commandBarButton("Execute Query");
|
||||
const executeQueryButton = explorer.commandBarButton(CommandBarButton.ExecuteQuery);
|
||||
await executeQueryButton.click();
|
||||
await expect(queryTab.resultsEditor.locator).toBeAttached({ timeout: 60 * 1000 });
|
||||
|
||||
@@ -59,7 +59,7 @@ test("Query results", async () => {
|
||||
test("Query stats", async () => {
|
||||
// Run the query and verify the results
|
||||
await queryEditor.locator.click();
|
||||
const executeQueryButton = explorer.commandBarButton("Execute Query");
|
||||
const executeQueryButton = explorer.commandBarButton(CommandBarButton.ExecuteQuery);
|
||||
await executeQueryButton.click();
|
||||
await expect(queryTab.resultsEditor.locator).toBeAttached({ timeout: 60 * 1000 });
|
||||
|
||||
@@ -77,7 +77,7 @@ test("Query errors", async () => {
|
||||
await queryEditor.setText("SELECT\n glarb(c.id),\n blarg(c.id)\nFROM c");
|
||||
|
||||
// Run the query and verify the results
|
||||
const executeQueryButton = explorer.commandBarButton("Execute Query");
|
||||
const executeQueryButton = explorer.commandBarButton(CommandBarButton.ExecuteQuery);
|
||||
await executeQueryButton.click();
|
||||
|
||||
await expect(queryTab.errorList).toBeAttached({ timeout: 60 * 1000 });
|
||||
|
||||
129
test/sql/scaleAndSettings/scale.spec.ts
Normal file
129
test/sql/scaleAndSettings/scale.spec.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import { expect, Locator, test } from "@playwright/test";
|
||||
import {
|
||||
CommandBarButton,
|
||||
DataExplorer,
|
||||
ONE_MINUTE_MS,
|
||||
TEST_AUTOSCALE_MAX_THROUGHPUT_RU_2K,
|
||||
TEST_MANUAL_THROUGHPUT_RU_2K,
|
||||
TestAccount,
|
||||
} from "../../fx";
|
||||
import { createTestSQLContainer, TestContainerContext } from "../../testData";
|
||||
|
||||
test.describe("Autoscale and Manual throughput", () => {
|
||||
let context: TestContainerContext = null!;
|
||||
let explorer: DataExplorer = null!;
|
||||
|
||||
test.beforeAll("Create Test Database", async () => {
|
||||
context = await createTestSQLContainer(true);
|
||||
});
|
||||
|
||||
test.beforeEach("Open container settings", async ({ page }) => {
|
||||
explorer = await DataExplorer.open(page, TestAccount.SQL);
|
||||
|
||||
// Click Scale & Settings and open Scale tab
|
||||
await explorer.openScaleAndSettings(context);
|
||||
const scaleTab = explorer.frame.getByTestId("settings-tab-header/ScaleTab");
|
||||
await scaleTab.click();
|
||||
});
|
||||
|
||||
test.afterAll("Delete Test Database", async () => {
|
||||
await context?.dispose();
|
||||
});
|
||||
|
||||
test("Update autoscale max throughput", async () => {
|
||||
// By default the created container has manual throughput (Containers created via JS SDK v4.7.0 cannot be created with autoscale throughput)
|
||||
await switchManualToAutoscaleThroughput();
|
||||
|
||||
// Update autoscale max throughput
|
||||
await getThroughputInput("autopilot").fill(TEST_AUTOSCALE_MAX_THROUGHPUT_RU_2K.toString());
|
||||
|
||||
// Save
|
||||
await explorer.commandBarButton(CommandBarButton.Save).click();
|
||||
|
||||
// Read console message
|
||||
await expect(explorer.getConsoleMessage()).toContainText(
|
||||
`Successfully updated offer for collection ${context.container.id}`,
|
||||
{
|
||||
timeout: 2 * ONE_MINUTE_MS,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("Update autoscale max throughput passed allowed limit", async () => {
|
||||
// By default the created container has manual throughput (Containers created via JS SDK v4.7.0 cannot be created with autoscale throughput)
|
||||
await switchManualToAutoscaleThroughput();
|
||||
|
||||
// Get soft allowed max throughput and remove commas
|
||||
const softAllowedMaxThroughputString = await explorer.frame
|
||||
.getByTestId("soft-allowed-maximum-throughput")
|
||||
.innerText();
|
||||
const softAllowedMaxThroughput = Number(softAllowedMaxThroughputString.replace(/,/g, ""));
|
||||
|
||||
// Try to set autoscale max throughput above allowed limit
|
||||
await getThroughputInput("autopilot").fill((softAllowedMaxThroughput * 10).toString());
|
||||
await expect(explorer.commandBarButton(CommandBarButton.Save)).toBeDisabled();
|
||||
await expect(getThroughputInputErrorMessage("autopilot")).toContainText(
|
||||
"This update isn't possible because it would increase the total throughput",
|
||||
);
|
||||
});
|
||||
|
||||
test("Update autoscale max throughput with invalid increment", async () => {
|
||||
// By default the created container has manual throughput (Containers created via JS SDK v4.7.0 cannot be created with autoscale throughput)
|
||||
await switchManualToAutoscaleThroughput();
|
||||
|
||||
// Try to set autoscale max throughput with invalid increment
|
||||
await getThroughputInput("autopilot").fill("1100");
|
||||
await expect(explorer.commandBarButton(CommandBarButton.Save)).toBeDisabled();
|
||||
await expect(getThroughputInputErrorMessage("autopilot")).toContainText(
|
||||
"Throughput value must be in increments of 1000",
|
||||
);
|
||||
});
|
||||
|
||||
test("Update manual throughput", async () => {
|
||||
await getThroughputInput("manual").fill(TEST_MANUAL_THROUGHPUT_RU_2K.toString());
|
||||
await explorer.commandBarButton(CommandBarButton.Save).click();
|
||||
await expect(explorer.getConsoleMessage()).toContainText(
|
||||
`Successfully updated offer for collection ${context.container.id}`,
|
||||
{
|
||||
timeout: 2 * ONE_MINUTE_MS,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("Update manual throughput passed allowed limit", async () => {
|
||||
// Get soft allowed max throughput and remove commas
|
||||
const softAllowedMaxThroughputString = await explorer.frame
|
||||
.getByTestId("soft-allowed-maximum-throughput")
|
||||
.innerText();
|
||||
const softAllowedMaxThroughput = Number(softAllowedMaxThroughputString.replace(/,/g, ""));
|
||||
|
||||
// Try to set manual throughput above allowed limit
|
||||
await getThroughputInput("manual").fill((softAllowedMaxThroughput * 10).toString());
|
||||
await expect(explorer.commandBarButton(CommandBarButton.Save)).toBeDisabled();
|
||||
await expect(getThroughputInputErrorMessage("manual")).toContainText(
|
||||
"This update isn't possible because it would increase the total throughput",
|
||||
);
|
||||
});
|
||||
|
||||
// Helper methods
|
||||
const getThroughputInput = (type: "manual" | "autopilot"): Locator => {
|
||||
return explorer.frame.getByTestId(`${type}-throughput-input`);
|
||||
};
|
||||
|
||||
const getThroughputInputErrorMessage = (type: "manual" | "autopilot"): Locator => {
|
||||
return explorer.frame.getByTestId(`${type}-throughput-input-error`);
|
||||
};
|
||||
|
||||
const switchManualToAutoscaleThroughput = async (): Promise<void> => {
|
||||
const autoscaleRadioButton = explorer.frame.getByText("Autoscale", { exact: true });
|
||||
await autoscaleRadioButton.click();
|
||||
await expect(explorer.commandBarButton(CommandBarButton.Save)).toBeEnabled();
|
||||
await explorer.commandBarButton(CommandBarButton.Save).click();
|
||||
await expect(explorer.getConsoleMessage()).toContainText(
|
||||
`Successfully updated offer for collection ${context.container.id}`,
|
||||
{
|
||||
timeout: ONE_MINUTE_MS,
|
||||
},
|
||||
);
|
||||
};
|
||||
});
|
||||
70
test/sql/scaleAndSettings/settings.spec.ts
Normal file
70
test/sql/scaleAndSettings/settings.spec.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { CommandBarButton, DataExplorer, ONE_MINUTE_MS, TestAccount } from "../../fx";
|
||||
import { createTestSQLContainer, TestContainerContext } from "../../testData";
|
||||
|
||||
test.describe("Settings under Scale & Settings", () => {
|
||||
let context: TestContainerContext = null!;
|
||||
let explorer: DataExplorer = null!;
|
||||
|
||||
test.beforeAll("Create Test Database", async () => {
|
||||
context = await createTestSQLContainer(true);
|
||||
});
|
||||
|
||||
test.beforeEach("Open Settings tab under Scale & Settings", async ({ page }) => {
|
||||
explorer = await DataExplorer.open(page, TestAccount.SQL);
|
||||
const containerNode = await explorer.waitForContainerNode(context.database.id, context.container.id);
|
||||
await containerNode.expand();
|
||||
|
||||
// Click Scale & Settings and open Scale tab
|
||||
await explorer.openScaleAndSettings(context);
|
||||
const settingsTab = explorer.frame.getByTestId("settings-tab-header/SubSettingsTab");
|
||||
await settingsTab.click();
|
||||
});
|
||||
|
||||
test.afterAll("Delete Test Database", async () => {
|
||||
await context?.dispose();
|
||||
});
|
||||
|
||||
test("Update TTL to On (no default)", async () => {
|
||||
const ttlOnNoDefaultRadioButton = explorer.frame.getByRole("radio", { name: "ttl-on-no-default-option" });
|
||||
await ttlOnNoDefaultRadioButton.click();
|
||||
|
||||
await explorer.commandBarButton(CommandBarButton.Save).click();
|
||||
await expect(explorer.getConsoleMessage()).toContainText(`Successfully updated container ${context.container.id}`, {
|
||||
timeout: ONE_MINUTE_MS,
|
||||
});
|
||||
});
|
||||
|
||||
test("Update TTL to On (with user entry)", async () => {
|
||||
const ttlOnRadioButton = explorer.frame.getByRole("radio", { name: "ttl-on-option" });
|
||||
await ttlOnRadioButton.click();
|
||||
|
||||
// Enter TTL seconds
|
||||
const ttlInput = explorer.frame.getByTestId("ttl-input");
|
||||
await ttlInput.fill("30000");
|
||||
|
||||
await explorer.commandBarButton(CommandBarButton.Save).click();
|
||||
await expect(explorer.getConsoleMessage()).toContainText(`Successfully updated container ${context.container.id}`, {
|
||||
timeout: ONE_MINUTE_MS,
|
||||
});
|
||||
});
|
||||
|
||||
test("Update TTL to Off", async () => {
|
||||
// By default TTL is set to off so we need to first set it to On
|
||||
const ttlOnNoDefaultRadioButton = explorer.frame.getByRole("radio", { name: "ttl-on-no-default-option" });
|
||||
await ttlOnNoDefaultRadioButton.click();
|
||||
await explorer.commandBarButton(CommandBarButton.Save).click();
|
||||
await expect(explorer.getConsoleMessage()).toContainText(`Successfully updated container ${context.container.id}`, {
|
||||
timeout: ONE_MINUTE_MS,
|
||||
});
|
||||
|
||||
// Set it to Off
|
||||
const ttlOffRadioButton = explorer.frame.getByRole("radio", { name: "ttl-off-option" });
|
||||
await ttlOffRadioButton.click();
|
||||
|
||||
await explorer.commandBarButton(CommandBarButton.Save).click();
|
||||
await expect(explorer.getConsoleMessage()).toContainText(`Successfully updated container ${context.container.id}`, {
|
||||
timeout: ONE_MINUTE_MS,
|
||||
});
|
||||
});
|
||||
});
|
||||
288
test/sql/scaleAndSettings/sharedThroughput.spec.ts
Normal file
288
test/sql/scaleAndSettings/sharedThroughput.spec.ts
Normal file
@@ -0,0 +1,288 @@
|
||||
import { CosmosDBManagementClient } from "@azure/arm-cosmosdb";
|
||||
import { CosmosClient, CosmosClientOptions, Database } from "@azure/cosmos";
|
||||
import { AzureIdentityCredentialAdapter } from "@azure/ms-rest-js";
|
||||
import { Locator, expect, test } from "@playwright/test";
|
||||
import {
|
||||
CommandBarButton,
|
||||
DataExplorer,
|
||||
ONE_MINUTE_MS,
|
||||
TEST_AUTOSCALE_MAX_THROUGHPUT_RU_4K,
|
||||
TEST_MANUAL_THROUGHPUT_RU,
|
||||
TestAccount,
|
||||
generateUniqueName,
|
||||
getAccountName,
|
||||
getAzureCLICredentials,
|
||||
resourceGroupName,
|
||||
subscriptionId,
|
||||
} from "../../fx";
|
||||
|
||||
// Helper class for database context
|
||||
class TestDatabaseContext {
|
||||
constructor(
|
||||
public armClient: CosmosDBManagementClient,
|
||||
public client: CosmosClient,
|
||||
public database: Database,
|
||||
) {}
|
||||
|
||||
async dispose() {
|
||||
await this.database.delete();
|
||||
}
|
||||
}
|
||||
|
||||
// Options for creating test database
|
||||
interface CreateTestDBOptions {
|
||||
throughput?: number;
|
||||
maxThroughput?: number; // For autoscale
|
||||
}
|
||||
|
||||
// Helper function to create a test database with shared throughput
|
||||
async function createTestDB(options?: CreateTestDBOptions): Promise<TestDatabaseContext> {
|
||||
const databaseId = generateUniqueName("db");
|
||||
const credentials = getAzureCLICredentials();
|
||||
const adaptedCredentials = new AzureIdentityCredentialAdapter(credentials);
|
||||
const armClient = new CosmosDBManagementClient(adaptedCredentials, subscriptionId);
|
||||
const accountName = getAccountName(TestAccount.SQL);
|
||||
const account = await armClient.databaseAccounts.get(resourceGroupName, accountName);
|
||||
|
||||
const clientOptions: CosmosClientOptions = {
|
||||
endpoint: account.documentEndpoint!,
|
||||
};
|
||||
|
||||
const nosqlAccountRbacToken = process.env.NOSQL_TESTACCOUNT_TOKEN;
|
||||
if (nosqlAccountRbacToken) {
|
||||
clientOptions.tokenProvider = async (): Promise<string> => {
|
||||
const AUTH_PREFIX = `type=aad&ver=1.0&sig=`;
|
||||
const authorizationToken = `${AUTH_PREFIX}${nosqlAccountRbacToken}`;
|
||||
return authorizationToken;
|
||||
};
|
||||
} else {
|
||||
const keys = await armClient.databaseAccounts.listKeys(resourceGroupName, accountName);
|
||||
clientOptions.key = keys.primaryMasterKey;
|
||||
}
|
||||
|
||||
const client = new CosmosClient(clientOptions);
|
||||
|
||||
// Create database with provisioned throughput (shared throughput)
|
||||
// This checks the "Provision database throughput" option
|
||||
const { database } = await client.databases.create({
|
||||
id: databaseId,
|
||||
throughput: options?.throughput, // Manual throughput (e.g., 400)
|
||||
maxThroughput: options?.maxThroughput, // Autoscale max throughput (e.g., 1000)
|
||||
});
|
||||
|
||||
return new TestDatabaseContext(armClient, client, database);
|
||||
}
|
||||
|
||||
test.describe("Database with Shared Throughput", () => {
|
||||
let dbContext: TestDatabaseContext = null!;
|
||||
let explorer: DataExplorer = null!;
|
||||
const containerId = "sharedcontainer";
|
||||
|
||||
// Helper methods
|
||||
const getThroughputInput = (type: "manual" | "autopilot"): Locator => {
|
||||
return explorer.frame.getByTestId(`${type}-throughput-input`);
|
||||
};
|
||||
|
||||
test.afterEach(async () => {
|
||||
// Clean up: delete the created database
|
||||
await dbContext?.dispose();
|
||||
});
|
||||
|
||||
test.describe("Manual Throughput Tests", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
explorer = await DataExplorer.open(page, TestAccount.SQL);
|
||||
});
|
||||
|
||||
test("Create database with shared manual throughput and verify Scale node in UI", async () => {
|
||||
test.setTimeout(120000); // 2 minutes timeout
|
||||
// Create database with shared manual throughput (400 RU/s)
|
||||
dbContext = await createTestDB({ throughput: 400 });
|
||||
|
||||
// Verify database node appears in the tree
|
||||
const databaseNode = await explorer.waitForNode(dbContext.database.id);
|
||||
expect(databaseNode).toBeDefined();
|
||||
|
||||
// Expand the database node to see child nodes
|
||||
await databaseNode.expand();
|
||||
|
||||
// Verify that "Scale" node appears under the database
|
||||
const scaleNode = await explorer.waitForNode(`${dbContext.database.id}/Scale`);
|
||||
expect(scaleNode).toBeDefined();
|
||||
await expect(scaleNode.element).toBeVisible();
|
||||
});
|
||||
|
||||
test("Add container to shared database without dedicated throughput", async () => {
|
||||
// Create database with shared manual throughput
|
||||
dbContext = await createTestDB({ throughput: 400 });
|
||||
|
||||
// Wait for the database to appear in the tree
|
||||
await explorer.waitForNode(dbContext.database.id);
|
||||
|
||||
// Add a container to the shared database via UI
|
||||
await explorer.globalCommandButton("New Container").click();
|
||||
|
||||
await explorer.whilePanelOpen(
|
||||
"New Container",
|
||||
async (panel, okButton) => {
|
||||
// Select "Use existing" database
|
||||
const useExistingRadio = panel.getByRole("radio", { name: /Use existing/i });
|
||||
await useExistingRadio.click();
|
||||
|
||||
// Select the database from dropdown using the new data-testid
|
||||
const databaseDropdown = panel.getByRole("combobox", { name: "Choose an existing database" });
|
||||
await databaseDropdown.click();
|
||||
|
||||
await explorer.frame.getByRole("option", { name: dbContext.database.id }).click();
|
||||
// Now you can target the specific database option by its data-testid
|
||||
//await panel.getByTestId(`database-option-${dbContext.database.id}`).click();
|
||||
// Fill container id
|
||||
await panel.getByRole("textbox", { name: "Container id, Example Container1" }).fill(containerId);
|
||||
|
||||
// Fill partition key
|
||||
await panel.getByRole("textbox", { name: "Partition key" }).fill("/pk");
|
||||
|
||||
// Ensure "Provision dedicated throughput" is NOT checked
|
||||
const dedicatedThroughputCheckbox = panel.getByRole("checkbox", {
|
||||
name: /Provision dedicated throughput for this container/i,
|
||||
});
|
||||
|
||||
if (await dedicatedThroughputCheckbox.isVisible()) {
|
||||
const isChecked = await dedicatedThroughputCheckbox.isChecked();
|
||||
if (isChecked) {
|
||||
await dedicatedThroughputCheckbox.uncheck();
|
||||
}
|
||||
}
|
||||
|
||||
await okButton.click();
|
||||
},
|
||||
{ closeTimeout: 5 * ONE_MINUTE_MS },
|
||||
);
|
||||
|
||||
// Verify container was created under the database
|
||||
const containerNode = await explorer.waitForContainerNode(dbContext.database.id, containerId);
|
||||
expect(containerNode).toBeDefined();
|
||||
});
|
||||
|
||||
test("Scale shared database manual throughput", async () => {
|
||||
// Create database with shared manual throughput (400 RU/s)
|
||||
dbContext = await createTestDB({ throughput: 400 });
|
||||
|
||||
// Navigate to the scale settings by clicking the "Scale" node in the tree
|
||||
const databaseNode = await explorer.waitForNode(dbContext.database.id);
|
||||
await databaseNode.expand();
|
||||
const scaleNode = await explorer.waitForNode(`${dbContext.database.id}/Scale`);
|
||||
await scaleNode.element.click();
|
||||
|
||||
// Update manual throughput from 400 to 800
|
||||
await getThroughputInput("manual").fill(TEST_MANUAL_THROUGHPUT_RU.toString());
|
||||
|
||||
// Save changes
|
||||
await explorer.commandBarButton(CommandBarButton.Save).click();
|
||||
|
||||
// Verify success message
|
||||
await expect(explorer.getConsoleMessage()).toContainText(
|
||||
`Successfully updated offer for database ${dbContext.database.id}`,
|
||||
{ timeout: 2 * ONE_MINUTE_MS },
|
||||
);
|
||||
});
|
||||
|
||||
test("Scale shared database from manual to autoscale", async () => {
|
||||
// Create database with shared manual throughput (400 RU/s)
|
||||
dbContext = await createTestDB({ throughput: 400 });
|
||||
|
||||
// Open database settings by clicking the "Scale" node
|
||||
const databaseNode = await explorer.waitForNode(dbContext.database.id);
|
||||
await databaseNode.expand();
|
||||
const scaleNode = await explorer.waitForNode(`${dbContext.database.id}/Scale`);
|
||||
await scaleNode.element.click();
|
||||
|
||||
// Switch to Autoscale
|
||||
const autoscaleRadio = explorer.frame.getByText("Autoscale", { exact: true });
|
||||
await autoscaleRadio.click();
|
||||
|
||||
// Set autoscale max throughput to 1000
|
||||
//await getThroughputInput("autopilot").fill(TEST_AUTOSCALE_THROUGHPUT_RU.toString());
|
||||
|
||||
// Save changes
|
||||
await explorer.commandBarButton(CommandBarButton.Save).click();
|
||||
|
||||
// Verify success message
|
||||
await expect(explorer.getConsoleMessage()).toContainText(
|
||||
`Successfully updated offer for database ${dbContext.database.id}`,
|
||||
{ timeout: 2 * ONE_MINUTE_MS },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Autoscale Throughput Tests", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
explorer = await DataExplorer.open(page, TestAccount.SQL);
|
||||
});
|
||||
|
||||
test("Create database with shared autoscale throughput and verify in UI", async () => {
|
||||
test.setTimeout(120000); // 2 minutes timeout
|
||||
|
||||
// Create database with shared autoscale throughput (max 1000 RU/s)
|
||||
dbContext = await createTestDB({ maxThroughput: 1000 });
|
||||
|
||||
// Verify database node appears
|
||||
const databaseNode = await explorer.waitForNode(dbContext.database.id);
|
||||
expect(databaseNode).toBeDefined();
|
||||
|
||||
// Expand the database node to see child nodes
|
||||
await databaseNode.expand();
|
||||
|
||||
// Verify that "Scale" node appears under the database
|
||||
const scaleNode = await explorer.waitForNode(`${dbContext.database.id}/Scale`);
|
||||
expect(scaleNode).toBeDefined();
|
||||
await expect(scaleNode.element).toBeVisible();
|
||||
});
|
||||
|
||||
test("Scale shared database autoscale throughput", async () => {
|
||||
// Create database with shared autoscale throughput (max 1000 RU/s)
|
||||
dbContext = await createTestDB({ maxThroughput: 1000 });
|
||||
|
||||
// Open database settings
|
||||
const databaseNode = await explorer.waitForNode(dbContext.database.id);
|
||||
await databaseNode.expand();
|
||||
const scaleNode = await explorer.waitForNode(`${dbContext.database.id}/Scale`);
|
||||
await scaleNode.element.click();
|
||||
|
||||
// Update autoscale max throughput from 1000 to 4000
|
||||
await getThroughputInput("autopilot").fill(TEST_AUTOSCALE_MAX_THROUGHPUT_RU_4K.toString());
|
||||
|
||||
// Save changes
|
||||
await explorer.commandBarButton(CommandBarButton.Save).click();
|
||||
|
||||
// Verify success message
|
||||
await expect(explorer.getConsoleMessage()).toContainText(
|
||||
`Successfully updated offer for database ${dbContext.database.id}`,
|
||||
{ timeout: 2 * ONE_MINUTE_MS },
|
||||
);
|
||||
});
|
||||
|
||||
test("Scale shared database from autoscale to manual", async () => {
|
||||
// Create database with shared autoscale throughput (max 1000 RU/s)
|
||||
dbContext = await createTestDB({ maxThroughput: 1000 });
|
||||
|
||||
// Open database settings
|
||||
const databaseNode = await explorer.waitForNode(dbContext.database.id);
|
||||
await databaseNode.expand();
|
||||
const scaleNode = await explorer.waitForNode(`${dbContext.database.id}/Scale`);
|
||||
await scaleNode.element.click();
|
||||
|
||||
// Switch to Manual
|
||||
const manualRadio = explorer.frame.getByText("Manual", { exact: true });
|
||||
await manualRadio.click();
|
||||
|
||||
// Save changes
|
||||
await explorer.commandBarButton(CommandBarButton.Save).click();
|
||||
|
||||
// Verify success message
|
||||
await expect(explorer.getConsoleMessage()).toContainText(
|
||||
`Successfully updated offer for database ${dbContext.database.id}`,
|
||||
{ timeout: 2 * ONE_MINUTE_MS },
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -74,6 +74,60 @@ export class TestContainerContext {
|
||||
}
|
||||
}
|
||||
|
||||
export class TestDatabaseContext {
|
||||
constructor(
|
||||
public armClient: CosmosDBManagementClient,
|
||||
public client: CosmosClient,
|
||||
public database: Database,
|
||||
) {}
|
||||
|
||||
async dispose() {
|
||||
await this.database.delete();
|
||||
}
|
||||
}
|
||||
|
||||
export interface CreateTestDBOptions {
|
||||
throughput?: number;
|
||||
maxThroughput?: number; // For autoscale
|
||||
}
|
||||
|
||||
export async function createTestDB(options?: CreateTestDBOptions): Promise<TestDatabaseContext> {
|
||||
const databaseId = generateUniqueName("db");
|
||||
const credentials = getAzureCLICredentials();
|
||||
const adaptedCredentials = new AzureIdentityCredentialAdapter(credentials);
|
||||
const armClient = new CosmosDBManagementClient(adaptedCredentials, subscriptionId);
|
||||
const accountName = getAccountName(TestAccount.SQL);
|
||||
const account = await armClient.databaseAccounts.get(resourceGroupName, accountName);
|
||||
|
||||
const clientOptions: CosmosClientOptions = {
|
||||
endpoint: account.documentEndpoint!,
|
||||
};
|
||||
|
||||
const nosqlAccountRbacToken = process.env.NOSQL_TESTACCOUNT_TOKEN;
|
||||
if (nosqlAccountRbacToken) {
|
||||
clientOptions.tokenProvider = async (): Promise<string> => {
|
||||
const AUTH_PREFIX = `type=aad&ver=1.0&sig=`;
|
||||
const authorizationToken = `${AUTH_PREFIX}${nosqlAccountRbacToken}`;
|
||||
return authorizationToken;
|
||||
};
|
||||
} else {
|
||||
const keys = await armClient.databaseAccounts.listKeys(resourceGroupName, accountName);
|
||||
clientOptions.key = keys.primaryMasterKey;
|
||||
}
|
||||
|
||||
const client = new CosmosClient(clientOptions);
|
||||
|
||||
// Create database with provisioned throughput (shared throughput)
|
||||
// This checks the "Provision database throughput" option
|
||||
const { database } = await client.databases.create({
|
||||
id: databaseId,
|
||||
throughput: options?.throughput, // Manual throughput (e.g., 400)
|
||||
maxThroughput: options?.maxThroughput, // Autoscale max throughput (e.g., 1000)
|
||||
});
|
||||
|
||||
return new TestDatabaseContext(armClient, client, database);
|
||||
}
|
||||
|
||||
export async function createTestSQLContainer(includeTestData?: boolean) {
|
||||
const databaseId = generateUniqueName("db");
|
||||
const containerId = "testcontainer"; // A unique container name isn't needed because the database is unique
|
||||
|
||||
Reference in New Issue
Block a user