Compare commits

...

5 Commits

Author SHA1 Message Date
Sakshi Gupta
8eade71456 dark mode for header 2026-01-02 14:36:42 +05:30
BChoudhury-ms
6167f94bc3 fix: restore SidePanel component for Container Copy feature (#2295) 2025-12-29 21:46:47 +05:30
BChoudhury-ms
be89c634f3 Add E2E tests for partition key change workflow (#2293) 2025-12-29 15:08:54 +05:30
sakshigupta12feb
42e230b88b Few more UI observation (fixes) (#2283)
* fixed bottom border for fabric

* fixed scrollbar

* reverted last

* updated the review comments

* Fixed scroll , updated the home page UI box shadow, header font weight, margin between boxed , subtab underline for fabric fixed

---------

Co-authored-by: Sakshi Gupta <sakshig@microsoft.com>
2025-12-17 23:54:30 +05:30
sakshigupta12feb
6196ba4722 Fixed bottom border for fabric and small UI changes (#2282)
* fixed bottom border for fabric

* updated the review comments

---------

Co-authored-by: Sakshi Gupta <sakshig@microsoft.com>
2025-12-16 21:35:08 +05:30
20 changed files with 295 additions and 42 deletions

View File

@@ -128,7 +128,7 @@
@provisionDatabaseThroughputInfo: 200px; @provisionDatabaseThroughputInfo: 200px;
//tabs container //tabs container
@ActiveTabHeight: 31px; @ActiveTabHeight: 32px;
@ActiveTabWidth: 141px; @ActiveTabWidth: 141px;
@TabsHeight: 30px; @TabsHeight: 30px;
@TabsWidth: 140px; @TabsWidth: 140px;

View File

@@ -2643,7 +2643,7 @@ a:link {
.tabPanesContainer { .tabPanesContainer {
flex-grow: 1; flex-grow: 1;
overflow-y: scroll; overflow: hidden;
background-color: var(--colorNeutralBackground1); background-color: var(--colorNeutralBackground1);
color: var(--colorNeutralForeground1); color: var(--colorNeutralForeground1);
} }
@@ -2651,6 +2651,7 @@ a:link {
.tabs-container { .tabs-container {
height: 100%; height: 100%;
width: 100%; width: 100%;
overflow-y: auto;
} }
.paddingspan4 { .paddingspan4 {
@@ -2677,7 +2678,7 @@ a:link {
width: @ActiveTabWidth; width: @ActiveTabWidth;
} }
.nav-tabs > li.active .contentWrapper { .nav-tabs > li.active .contentWrapper .tabNavText {
border-bottom: 2px solid var(--colorCompoundBrandBackground); border-bottom: 2px solid var(--colorCompoundBrandBackground);
} }

View File

@@ -7,6 +7,7 @@ html {
body { body {
font-family: @FabricFont; font-family: @FabricFont;
background-color: #f5f5f5; background-color: #f5f5f5;
--colorCompoundBrandBackground: @FabricAccentMedium;
} }
a { a {
@@ -41,7 +42,7 @@ a:focus {
} }
.nav-tabs-margin { .nav-tabs-margin {
padding-top: 5px; padding-top: 0px;
background-color: #ffffff; background-color: #ffffff;
} }
@@ -68,17 +69,20 @@ a:focus {
} }
.nav-tabs > li > .tabNavContentContainer > .tab_Content:hover { .nav-tabs > li > .tabNavContentContainer > .tab_Content:hover {
border-bottom: 2px solid #e0e0e0; border-bottom: none;
} }
.nav-tabs > li.active > .tabNavContentContainer > .tab_Content, .nav-tabs > li.active > .tabNavContentContainer > .tab_Content,
.nav-tabs > li.active > .tabNavContentContainer > .tab_Content:hover { .nav-tabs > li.active > .tabNavContentContainer > .tab_Content:hover {
border-bottom: 2px solid @FabricAccentMedium; border-bottom: none;
} }
.nav-tabs > li.active > .tabNavContentContainer > .tab_Content > .contentWrapper > .tabNavText { .nav-tabs > li.active > .tabNavContentContainer > .tab_Content > .contentWrapper > .tabNavText {
border-bottom: 0px none transparent; border-bottom: 0px none transparent;
} }
.nav-tabs > li.active .contentWrapper .tabNavText {
border-bottom: 2px solid @FabricAccentMedium;
}
.tabNavContentContainer { .tabNavContentContainer {
padding: @SmallSpace 0px @SmallSpace 0px; padding: @SmallSpace 0px @SmallSpace 0px;

View File

@@ -17,6 +17,38 @@
position: fixed; position: fixed;
top: -200px; top: -200px;
} }
body.isDarkMode .ms-Layer {
.ms-Callout-main {
background-color: @BaseHigh !important;
}
.ms-Callout-beak {
background-color: @BaseHigh !important;
}
.ms-ContextualMenu {
background-color: @BaseHigh !important;
}
.ms-Dropdown-items {
background-color: @BaseHigh !important;
}
.ms-Dropdown-item {
background-color: @BaseHigh !important;
color: @BaseLight !important;
&:hover {
background-color: @BaseMediumHigh !important;
color: @BaseLight !important;
}
&.is-selected {
background-color: @BaseMediumHigh !important;
color: @BaseLight !important;
}
}
}
html { html {
font-family: wf_segoe-ui_normal, "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif; font-family: wf_segoe-ui_normal, "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif;
@@ -129,6 +161,65 @@ body {
} }
} }
&.isDarkMode .accountSwitchContextualMenu {
background-color: @BaseHigh;
.ms-Callout-main {
background-color: @BaseHigh;
}
.ms-ContextualMenu-item {
background-color: @BaseHigh;
}
.ms-Dropdown {
.ms-Dropdown-title {
background-color: @BaseDark;
color: @BaseLight;
border-color: @BaseMediumHigh;
}
.ms-Dropdown-caretDownWrapper {
color: @BaseLight;
}
&:hover .ms-Dropdown-title {
background-color: @BaseHigh;
color: @BaseLight;
border-color: @BaseMedium;
}
}
.ms-Label {
color: @BaseLight;
}
}
&.isDarkMode .ms-Dropdown-callout {
.ms-Callout-main {
background-color: @BaseHigh;
}
.ms-Dropdown-items {
background-color: @BaseHigh;
}
.ms-Dropdown-item {
background-color: @BaseHigh;
color: @BaseLight;
&:hover {
background-color: @BaseMediumHigh;
color: @BaseLight;
}
&.is-selected {
background-color: @BaseMediumHigh;
color: @BaseLight;
}
}
}
.fixedleftpane { .fixedleftpane {
background: @BaseLow; background: @BaseLow;
height: 100vh; height: 100vh;

View File

@@ -187,7 +187,7 @@ export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({
<Text styles={textSubHeadingStyle}>Current {partitionKeyName.toLowerCase()}</Text> <Text styles={textSubHeadingStyle}>Current {partitionKeyName.toLowerCase()}</Text>
<Text styles={textSubHeadingStyle}>Partitioning</Text> <Text styles={textSubHeadingStyle}>Partitioning</Text>
</Stack> </Stack>
<Stack tokens={{ childrenGap: 5 }}> <Stack tokens={{ childrenGap: 5 }} data-test="partition-key-values">
<Text styles={textSubHeadingStyle1}>{partitionKeyValue}</Text> <Text styles={textSubHeadingStyle1}>{partitionKeyValue}</Text>
<Text styles={textSubHeadingStyle1}> <Text styles={textSubHeadingStyle1}>
{isHierarchicalPartitionedContainer() ? "Hierarchical" : "Non-hierarchical"} {isHierarchicalPartitionedContainer() ? "Hierarchical" : "Non-hierarchical"}
@@ -199,6 +199,7 @@ export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({
{!isReadOnly && ( {!isReadOnly && (
<> <>
<MessageBar <MessageBar
data-test="partition-key-warning"
messageBarType={MessageBarType.warning} messageBarType={MessageBarType.warning}
messageBarIconProps={{ iconName: "WarningSolid", className: "messageBarWarningIcon" }} messageBarIconProps={{ iconName: "WarningSolid", className: "messageBarWarningIcon" }}
styles={darkThemeMessageBarStyles} styles={darkThemeMessageBarStyles}
@@ -220,6 +221,7 @@ export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({
</Text> </Text>
{configContext.platform !== Platform.Emulator && ( {configContext.platform !== Platform.Emulator && (
<PrimaryButton <PrimaryButton
data-test="change-partition-key-button"
styles={{ root: { width: "fit-content" } }} styles={{ root: { width: "fit-content" } }}
text="Change" text="Change"
onClick={startPartitionkeyChangeWorkflow} onClick={startPartitionkeyChangeWorkflow}

View File

@@ -78,6 +78,7 @@ exports[`PartitionKeyComponent renders default component and matches snapshot 1`
</Text> </Text>
</Stack> </Stack>
<Stack <Stack
data-test="partition-key-values"
tokens={ tokens={
{ {
"childrenGap": 5, "childrenGap": 5,
@@ -108,6 +109,7 @@ exports[`PartitionKeyComponent renders default component and matches snapshot 1`
</Stack> </Stack>
</Stack> </Stack>
<StyledMessageBar <StyledMessageBar
data-test="partition-key-warning"
messageBarIconProps={ messageBarIconProps={
{ {
"className": "messageBarWarningIcon", "className": "messageBarWarningIcon",
@@ -160,6 +162,7 @@ exports[`PartitionKeyComponent renders default component and matches snapshot 1`
To change the partition key, a new destination container must be created or an existing destination container selected. Data will then be copied to the destination container. To change the partition key, a new destination container must be created or an existing destination container selected. Data will then be copied to the destination container.
</Text> </Text>
<CustomizedPrimaryButton <CustomizedPrimaryButton
data-test="change-partition-key-button"
onClick={[Function]} onClick={[Function]}
styles={ styles={
{ {
@@ -237,6 +240,7 @@ exports[`PartitionKeyComponent renders read-only component and matches snapshot
</Text> </Text>
</Stack> </Stack>
<Stack <Stack
data-test="partition-key-values"
tokens={ tokens={
{ {
"childrenGap": 5, "childrenGap": 5,

View File

@@ -208,7 +208,7 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
</div> </div>
</Stack> </Stack>
{createNewContainer ? ( {createNewContainer ? (
<Stack> <Stack data-test="create-new-container-form">
<MessageBar>All configurations except for unique keys will be copied from the source container</MessageBar> <MessageBar>All configurations except for unique keys will be copied from the source container</MessageBar>
<Stack className="panelGroupSpacing"> <Stack className="panelGroupSpacing">
<Stack horizontal> <Stack horizontal>
@@ -230,6 +230,7 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
</TooltipHost> </TooltipHost>
</Stack> </Stack>
<input <input
data-test="new-container-id-input"
name="collectionId" name="collectionId"
id="collectionId" id="collectionId"
type="text" type="text"
@@ -271,6 +272,7 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
<input <input
type="text" type="text"
data-test="new-container-partition-key-input"
id="addCollection-partitionKeyValue" id="addCollection-partitionKeyValue"
aria-required aria-required
required required
@@ -304,6 +306,7 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
type="text" type="text"
id="addCollection-partitionKeyValue" id="addCollection-partitionKeyValue"
key={`addCollection-partitionKeyValue_${index}`} key={`addCollection-partitionKeyValue_${index}`}
data-test={`new-container-sub-partition-key-input-${index}`}
aria-required aria-required
required required
size={40} size={40}
@@ -327,6 +330,8 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
}} }}
/> />
<IconButton <IconButton
data-test={`remove-sub-partition-key-button-${index}`}
ariaLabel="Remove hierarchical partition key"
iconProps={{ iconName: "Delete" }} iconProps={{ iconName: "Delete" }}
style={{ height: 27 }} style={{ height: 27 }}
onClick={() => { onClick={() => {
@@ -339,6 +344,7 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
})} })}
<Stack className="panelGroupSpacing"> <Stack className="panelGroupSpacing">
<DefaultButton <DefaultButton
data-test="add-sub-partition-key-button"
styles={{ root: { padding: 0, width: 200, height: 30 }, label: { fontSize: 12 } }} styles={{ root: { padding: 0, width: 200, height: 30 }, label: { fontSize: 12 } }}
disabled={subPartitionKeys.length >= Constants.BackendDefaults.maxNumMultiHashPartition} disabled={subPartitionKeys.length >= Constants.BackendDefaults.maxNumMultiHashPartition}
onClick={() => setSubPartitionKeys([...subPartitionKeys, ""])} onClick={() => setSubPartitionKeys([...subPartitionKeys, ""])}
@@ -346,7 +352,11 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
Add hierarchical partition key Add hierarchical partition key
</DefaultButton> </DefaultButton>
{subPartitionKeys.length > 0 && ( {subPartitionKeys.length > 0 && (
<Text variant="small" style={{ color: "var(--colorNeutralForeground1)" }}> <Text
data-test="hierarchical-partitioning-info-text"
variant="small"
style={{ color: "var(--colorNeutralForeground1)" }}
>
<Icon iconName="InfoSolid" className="removeIcon" tabIndex={0} /> This feature allows you to <Icon iconName="InfoSolid" className="removeIcon" tabIndex={0} /> This feature allows you to
partition your data with up to three levels of keys for better data distribution. Requires .NET V3, partition your data with up to three levels of keys for better data distribution. Requires .NET V3,
Java V4 SDK, or preview JavaScript V3 SDK.{" "} Java V4 SDK, or preview JavaScript V3 SDK.{" "}
@@ -359,7 +369,7 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
</Stack> </Stack>
</Stack> </Stack>
) : ( ) : (
<Stack> <Stack data-test="use-existing-container-form">
<Stack horizontal> <Stack horizontal>
<span className="mandatoryStar">*&nbsp;</span> <span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small"> <Text className="panelTextBold" variant="small">
@@ -390,6 +400,7 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
}} }}
defaultSelectedKey={targetCollectionId} defaultSelectedKey={targetCollectionId}
responsiveMode={999} responsiveMode={999}
ariaLabel="Existing Containers"
/> />
</Stack> </Stack>
)} )}

View File

@@ -61,7 +61,8 @@ const useStyles = makeStyles({
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
alignItems: "center", alignItems: "center",
minHeight: "100vh", height: "100%",
overflowY: "auto",
backgroundColor: "var(--colorNeutralBackground1)", backgroundColor: "var(--colorNeutralBackground1)",
color: "var(--colorNeutralForeground1)", color: "var(--colorNeutralForeground1)",
}, },
@@ -73,20 +74,19 @@ const useStyles = makeStyles({
}, },
title: { title: {
fontSize: "48px", fontSize: "48px",
fontWeight: "500", fontWeight: "400",
margin: "16px auto", margin: "16px auto",
color: "var(--colorNeutralForeground1)", color: "var(--colorNeutralForeground1)",
}, },
subtitle: { subtitle: {
fontSize: "18px", fontSize: "18px",
marginBottom: "40px",
color: "var(--colorNeutralForeground2)", color: "var(--colorNeutralForeground2)",
}, },
cardContainer: { cardContainer: {
display: "grid", display: "grid",
gridTemplateColumns: "repeat(2, 1fr)", gridTemplateColumns: "repeat(2, 1fr)",
gap: "16px", gap: "16px",
width: "66%", width: "60%",
margin: "0 auto", margin: "0 auto",
backgroundColor: "var(--colorNeutralBackground1)", backgroundColor: "var(--colorNeutralBackground1)",
color: "var(--colorNeutralForeground1)", color: "var(--colorNeutralForeground1)",
@@ -100,7 +100,7 @@ const useStyles = makeStyles({
color: "var(--colorNeutralForeground1)", color: "var(--colorNeutralForeground1)",
border: "1px solid var(--colorNeutralStroke1)", border: "1px solid var(--colorNeutralStroke1)",
borderRadius: "4px", borderRadius: "4px",
boxShadow: "var(--shadow4)", boxShadow: "rgba(0, 0, 0, 0.25) 0px 4px 4px",
cursor: "pointer", cursor: "pointer",
minHeight: "150px", minHeight: "150px",
"&:hover": { "&:hover": {
@@ -128,11 +128,10 @@ const useStyles = makeStyles({
textAlign: "left", textAlign: "left",
}, },
moreStuffContainer: { moreStuffContainer: {
display: "grid", display: "flex",
gridTemplateColumns: "repeat(3, 1fr)", justifyContent: "space-between",
gap: "32px", gap: "32px",
width: "66%", width: "90%",
margin: "40px auto",
}, },
moreStuffColumn: { moreStuffColumn: {
display: "flex", display: "flex",
@@ -227,7 +226,7 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
return ( return (
<Stack <Stack
className="splashStackContainer" className="splashStackContainer"
style={{ width: "66%", cursor: "pointer", margin: "40px auto" }} style={{ width: "60%", cursor: "pointer", margin: "40px auto" }}
tokens={{ childrenGap: 16 }} tokens={{ childrenGap: 16 }}
> >
<Stack className="splashStackRow" horizontal> <Stack className="splashStackRow" horizontal>
@@ -903,9 +902,9 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
return ( return (
<div className={styles.splashScreenContainer}> <div className={styles.splashScreenContainer}>
<div className={styles.splashScreen}> <div className={styles.splashScreen}>
<h1 className={styles.title} role="heading" aria-label="Welcome to Azure Cosmos DB"> <h2 className={styles.title} role="heading" aria-label="Welcome to Azure Cosmos DB">
Welcome to Azure Cosmos DB<span className="activePatch"></span> Welcome to Azure Cosmos DB<span className="activePatch"></span>
</h1> </h2>
<div className={styles.subtitle}>Globally distributed, multi-model database service for any scale</div> <div className={styles.subtitle}>Globally distributed, multi-model database service for any scale</div>
{getSplashScreenButtons()} {getSplashScreenButtons()}
{useCarousel.getState().showCoachMark && ( {useCarousel.getState().showCoachMark && (

View File

@@ -15,7 +15,7 @@ const useStyles = makeStyles({
button: { button: {
border: "1px solid var(--colorNeutralStroke1)", border: "1px solid var(--colorNeutralStroke1)",
boxSizing: "border-box", boxSizing: "border-box",
boxShadow: "var(--shadow4)", boxShadow: "rgba(0, 0, 0, 0.25) 0px 4px 4px",
borderRadius: "4px", borderRadius: "4px",
padding: "32px 16px", padding: "32px 16px",
backgroundColor: "var(--colorNeutralBackground1)", backgroundColor: "var(--colorNeutralBackground1)",

View File

@@ -1,5 +1,6 @@
import { initializeIcons } from "@fluentui/react"; import { initializeIcons } from "@fluentui/react";
import { useBoolean } from "@fluentui/react-hooks"; import { useBoolean } from "@fluentui/react-hooks";
import { MessageTypes } from "Contracts/MessageTypes";
import { AadAuthorizationFailure } from "Platform/Hosted/Components/AadAuthorizationFailure"; import { AadAuthorizationFailure } from "Platform/Hosted/Components/AadAuthorizationFailure";
import * as React from "react"; import * as React from "react";
import { render } from "react-dom"; import { render } from "react-dom";
@@ -21,9 +22,33 @@ import "./Shared/appInsights";
import { useAADAuth } from "./hooks/useAADAuth"; import { useAADAuth } from "./hooks/useAADAuth";
import { useConfig } from "./hooks/useConfig"; import { useConfig } from "./hooks/useConfig";
import { useTokenMetadata } from "./hooks/usePortalAccessToken"; import { useTokenMetadata } from "./hooks/usePortalAccessToken";
import { THEME_MODE_DARK, useThemeStore } from "./hooks/useTheme";
initializeIcons(); initializeIcons();
if (typeof window !== "undefined") {
window.addEventListener("message", (event) => {
const messageData = event.data?.data || event.data;
const messageType = messageData?.type;
if (messageType === MessageTypes.UpdateTheme) {
const themeData = messageData?.params?.theme || messageData?.theme;
if (themeData && themeData.mode !== undefined) {
const isDark = themeData.mode === THEME_MODE_DARK;
useThemeStore.setState({
isDarkMode: isDark,
themeMode: themeData.mode,
});
if (isDark) {
document.body.classList.add("isDarkMode");
} else {
document.body.classList.remove("isDarkMode");
}
}
}
});
}
const App: React.FunctionComponent = () => { const App: React.FunctionComponent = () => {
// For handling encrypted portal tokens sent via query paramter // For handling encrypted portal tokens sent via query paramter
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);

View File

@@ -125,7 +125,10 @@ const App = (): JSX.Element => {
<KeyboardShortcutRoot> <KeyboardShortcutRoot>
<div className="flexContainer" aria-hidden="false"> <div className="flexContainer" aria-hidden="false">
{userContext.features.enableContainerCopy && userContext.apiType === "SQL" ? ( {userContext.features.enableContainerCopy && userContext.apiType === "SQL" ? (
<ContainerCopyPanel explorer={explorer} /> <>
<ContainerCopyPanel explorer={explorer} />
<SidePanel />
</>
) : ( ) : (
<DivExplorer explorer={explorer} /> <DivExplorer explorer={explorer} />
)} )}

View File

@@ -1,7 +1,7 @@
// TODO: Renable this rule for the file or turn it off everywhere // TODO: Renable this rule for the file or turn it off everywhere
/* eslint-disable react/display-name */ /* eslint-disable react/display-name */
import { DefaultButton, IButtonStyles, IContextualMenuItem } from "@fluentui/react"; import { DefaultButton, IButtonStyles, IContextualMenuItem, IContextualMenuProps } from "@fluentui/react";
import * as React from "react"; import * as React from "react";
import { FunctionComponent, useEffect, useState } from "react"; import { FunctionComponent, useEffect, useState } from "react";
import { StyleConstants } from "../../../Common/StyleConstants"; import { StyleConstants } from "../../../Common/StyleConstants";
@@ -92,14 +92,16 @@ export const AccountSwitcher: FunctionComponent<Props> = ({ armToken, setDatabas
}, },
]; ];
const menuProps: IContextualMenuProps = {
directionalHintFixed: true,
className: "accountSwitchContextualMenu",
items,
};
return ( return (
<DefaultButton <DefaultButton
text={buttonText} text={buttonText}
menuProps={{ menuProps={menuProps}
directionalHintFixed: true,
className: "accountSwitchContextualMenu",
items,
}}
styles={buttonStyles} styles={buttonStyles}
className="accountSwitchButton" className="accountSwitchButton"
id="accountSwitchButton" id="accountSwitchButton"

View File

@@ -31,9 +31,6 @@ export const SwitchAccount: FunctionComponent<Props> = ({
}} }}
defaultSelectedKey={selectedAccount?.name} defaultSelectedKey={selectedAccount?.name}
placeholder={accounts && accounts.length === 0 ? "No Accounts Found" : "Select an Account"} placeholder={accounts && accounts.length === 0 ? "No Accounts Found" : "Select an Account"}
styles={{
callout: "accountSwitchAccountDropdownMenu",
}}
/> />
); );
}; };

View File

@@ -30,9 +30,6 @@ export const SwitchSubscription: FunctionComponent<Props> = ({
}} }}
defaultSelectedKey={selectedSubscription?.subscriptionId} defaultSelectedKey={selectedSubscription?.subscriptionId}
placeholder={subscriptions && subscriptions.length === 0 ? "No Subscriptions Found" : "Select a Subscription"} placeholder={subscriptions && subscriptions.length === 0 ? "No Subscriptions Found" : "Select a Subscription"}
styles={{
callout: "accountSwitchSubscriptionDropdownMenu",
}}
/> />
); );
}; };

View File

@@ -470,6 +470,15 @@ export class DataExplorer {
return this.frame.getByTestId("notification-console/header-status"); return this.frame.getByTestId("notification-console/header-status");
} }
async getDropdownItemByName(name: string, ariaLabel?: string): Promise<Locator> {
const dropdownItemsWrapper = this.frame.locator("div.ms-Dropdown-items");
if (ariaLabel) {
expect(await dropdownItemsWrapper.getAttribute("aria-label")).toEqual(ariaLabel);
}
const containerDropdownItems = dropdownItemsWrapper.locator("button.ms-Dropdown-item[role='option']");
return containerDropdownItems.filter({ hasText: name });
}
/** Waits for the Data Explorer app to load */ /** Waits for the Data Explorer app to load */
static async waitForExplorer(page: Page) { static async waitForExplorer(page: Page) {
const iframeElement = await page.getByTestId("DataExplorerFrame").elementHandle(); const iframeElement = await page.getByTestId("DataExplorerFrame").elementHandle();

View File

@@ -9,7 +9,7 @@ let queryTab: QueryTab = null!;
let queryEditor: Editor = null!; let queryEditor: Editor = null!;
test.beforeAll("Create Test Database", async () => { test.beforeAll("Create Test Database", async () => {
context = await createTestSQLContainer(true); context = await createTestSQLContainer({ includeTestData: true });
}); });
test.beforeEach("Open new query tab", async ({ page }) => { test.beforeEach("Open new query tab", async ({ page }) => {

View File

@@ -0,0 +1,98 @@
import { expect, Page, test } from "@playwright/test";
import { DataExplorer, TestAccount } from "../../fx";
import { createTestSQLContainer, TestContainerContext } from "../../testData";
test.describe("Change Partition Key", () => {
let pageInstance: Page;
let context: TestContainerContext = null!;
let explorer: DataExplorer = null!;
const newPartitionKeyPath = "/newPartitionKey";
const newContainerId = "testcontainer_1";
test.beforeAll("Create Test Database", async () => {
context = await createTestSQLContainer();
});
test.beforeEach("Open container settings", async ({ page }) => {
pageInstance = page;
explorer = await DataExplorer.open(page, TestAccount.SQL);
// Click Scale & Settings and open Partition Key tab
await explorer.openScaleAndSettings(context);
const PartitionKeyTab = explorer.frame.getByTestId("settings-tab-header/PartitionKeyTab");
await PartitionKeyTab.click();
});
test.afterAll("Delete Test Database", async () => {
await context?.dispose();
});
test("Change partition key path", async () => {
await expect(explorer.frame.getByText("/partitionKey")).toBeVisible();
await expect(explorer.frame.getByText("Change partition key")).toBeVisible();
await expect(explorer.frame.getByText(/To safeguard the integrity of/)).toBeVisible();
await expect(explorer.frame.getByText(/To change the partition key/)).toBeVisible();
const changePartitionKeyButton = explorer.frame.getByTestId("change-partition-key-button");
expect(changePartitionKeyButton).toBeVisible();
await changePartitionKeyButton.click();
// Fill out new partition key form in the panel
const changePkPanel = explorer.frame.getByTestId(`Panel:Change partition key`);
await expect(changePkPanel.getByText(context.database.id)).toBeVisible();
await expect(explorer.frame.getByRole("heading", { name: "Change partition key" })).toBeVisible();
await expect(explorer.frame.getByText(/When changing a container/)).toBeVisible();
// Try to switch to new container
await expect(changePkPanel.getByText("New container")).toBeVisible();
await expect(changePkPanel.getByText("Existing container")).toBeVisible();
await expect(changePkPanel.getByTestId("new-container-id-input")).toBeVisible();
changePkPanel.getByTestId("new-container-id-input").fill(newContainerId);
await expect(changePkPanel.getByTestId("new-container-partition-key-input")).toBeVisible();
changePkPanel.getByTestId("new-container-partition-key-input").fill(newPartitionKeyPath);
await expect(changePkPanel.getByTestId("add-sub-partition-key-button")).toBeVisible();
changePkPanel.getByTestId("add-sub-partition-key-button").click();
await expect(changePkPanel.getByTestId("new-container-sub-partition-key-input-0")).toBeVisible();
await expect(changePkPanel.getByTestId("remove-sub-partition-key-button-0")).toBeVisible();
await expect(changePkPanel.getByTestId("hierarchical-partitioning-info-text")).toBeVisible();
changePkPanel.getByTestId("new-container-sub-partition-key-input-0").fill("/customerId");
await changePkPanel.getByTestId("Panel/OkButton").click();
await pageInstance.waitForLoadState("networkidle");
await expect(changePkPanel).not.toBeVisible({ timeout: 60 * 1000 });
// Verify partition key change job
const jobText = explorer.frame.getByText(/Partition key change job/);
await expect(jobText).toBeVisible();
await expect(explorer.frame.locator(".ms-ProgressIndicator-itemName")).toContainText("Portal_testcontainer_1");
const jobRow = explorer.frame.locator(".ms-ProgressIndicator-itemDescription");
await expect(jobRow.getByText("Completed")).toBeVisible({ timeout: 30 * 1000 });
const newContainerNode = await explorer.waitForContainerNode(context.database.id, newContainerId);
expect(newContainerNode).not.toBeNull();
// Now try to switch to existing container
await changePartitionKeyButton.click();
await changePkPanel.getByText("Existing container").click();
await changePkPanel.getByLabel("Use existing container").check();
await changePkPanel.getByText("Choose an existing container").click();
const containerDropdownItem = await explorer.getDropdownItemByName(newContainerId, "Existing Containers");
await containerDropdownItem.click();
await changePkPanel.getByTestId("Panel/OkButton").click();
await explorer.frame.getByRole("button", { name: "Cancel" }).click();
// Dismiss overlay if it appears
const overlayFrame = explorer.frame.locator("#webpack-dev-server-client-overlay").first();
if (await overlayFrame.count()) {
await overlayFrame.contentFrame().getByLabel("Dismiss").click();
}
const cancelledJobRow = explorer.frame.getByTestId("Tab:tab0");
await expect(cancelledJobRow.getByText("Cancelled")).toBeVisible({ timeout: 30 * 1000 });
});
});

View File

@@ -14,7 +14,7 @@ test.describe("Autoscale and Manual throughput", () => {
let explorer: DataExplorer = null!; let explorer: DataExplorer = null!;
test.beforeAll("Create Test Database", async () => { test.beforeAll("Create Test Database", async () => {
context = await createTestSQLContainer(true); context = await createTestSQLContainer({ includeTestData: true });
}); });
test.beforeEach("Open container settings", async ({ page }) => { test.beforeEach("Open container settings", async ({ page }) => {

View File

@@ -7,7 +7,7 @@ test.describe("Settings under Scale & Settings", () => {
let explorer: DataExplorer = null!; let explorer: DataExplorer = null!;
test.beforeAll("Create Test Database", async () => { test.beforeAll("Create Test Database", async () => {
context = await createTestSQLContainer(true); context = await createTestSQLContainer({ includeTestData: true });
}); });
test.beforeEach("Open Settings tab under Scale & Settings", async ({ page }) => { test.beforeEach("Open Settings tab under Scale & Settings", async ({ page }) => {

View File

@@ -74,8 +74,18 @@ export class TestContainerContext {
} }
} }
export async function createTestSQLContainer(includeTestData?: boolean) { type createTestSqlContainerConfig = {
const databaseId = generateUniqueName("db"); includeTestData?: boolean;
partitionKey?: string;
databaseName?: string;
};
export async function createTestSQLContainer({
includeTestData = false,
partitionKey = "/partitionKey",
databaseName = "",
}: createTestSqlContainerConfig = {}) {
const databaseId = databaseName ? databaseName : generateUniqueName("db");
const containerId = "testcontainer"; // A unique container name isn't needed because the database is unique const containerId = "testcontainer"; // A unique container name isn't needed because the database is unique
const credentials = getAzureCLICredentials(); const credentials = getAzureCLICredentials();
const adaptedCredentials = new AzureIdentityCredentialAdapter(credentials); const adaptedCredentials = new AzureIdentityCredentialAdapter(credentials);
@@ -104,7 +114,7 @@ export async function createTestSQLContainer(includeTestData?: boolean) {
try { try {
const { container } = await database.containers.createIfNotExists({ const { container } = await database.containers.createIfNotExists({
id: containerId, id: containerId,
partitionKey: "/partitionKey", partitionKey,
}); });
if (includeTestData) { if (includeTestData) {
const batchCount = TestData.length / 100; const batchCount = TestData.length / 100;