Compare commits

..

37 Commits

Author SHA1 Message Date
Bikram Choudhury
0e321146e2 Merge branch 'master' of https://github.com/Azure/cosmos-explorer into user/bchoudhury/cpk-fts-refator 2026-01-06 22:20:01 +05:30
BChoudhury-ms
53288dec6f feat: add test identifiers (data-test) to Container Copy components (#2306) 2026-01-06 21:19:58 +05:30
BChoudhury-ms
258a6286e7 refactor(dataTransfers): replace fetch with armRequest for pagination (#2305)
* refactor(dataTransfers): replace fetch with armRequest for pagination

* added comment for next link url parsing
2026-01-06 21:12:19 +05:30
sakshigupta12feb
c8ebca6da4 Fixed homepage UI for fabric (#2304)
* Fixed homePage UI for fabric


Co-authored-by: Sakshi Gupta <sakshig@microsoft.com>
2026-01-05 17:02:19 +05:30
Bikram Choudhury
ad863bdd58 added try-catch to the error code 2026-01-05 13:12:12 +05:30
Bikram Choudhury
0683bfc3af removed the sub partition key 2026-01-04 01:26:36 +05:30
Bikram Choudhury
1039d835f4 ignore nosql test files and list out the number of files runs on each shard 2026-01-03 00:39:08 +05:30
Bikram Choudhury
227c93a30b remove forward slash 2026-01-02 16:29:40 +05:30
Asier Isayas
1f1ccc7e0e only open scale and settings at the beginning of each test suite 2026-01-01 12:35:02 -05:00
Asier Isayas
7aa62437f0 Don't refresh databases when opening Scale+Settings and only delete database if running locally 2025-12-31 15:48:22 -05:00
Asier Isayas
7ddedf3314 dont delete database after every test. leave it to the CI 2025-12-31 13:20:20 -05:00
Asier Isayas
644226d800 create and delete a container for each individual test 2025-12-31 12:23:38 -05:00
Asier Isayas
e2e9cece5f close notification console window after seeing desired log 2025-12-31 11:04:54 -05:00
Asier Isayas
d235be6a3b when refreshing containers, open console window to check for status completion 2025-12-31 10:10:33 -05:00
Asier Isayas
87d5f84f2e refresh databases in test instead of product code 2025-12-30 16:59:17 -05:00
Asier Isayas
2758e6ac72 increase time for change partition key request 2025-12-30 15:19:45 -05:00
Asier Isayas
1232596c5a increase time for change partition key request 2025-12-30 14:51:05 -05:00
Asier Isayas
eff736596f reload all databases before loading offers 2025-12-30 14:09:53 -05:00
Asier Isayas
44de3ade5c When opening scale and settings, refresh databases 2025-12-30 12:20:20 -05:00
Asier Isayas
459b2c7050 refresh tree before opening scale and settings 2025-12-30 11:22:23 -05:00
Asier Isayas
1736e24429 when disposing of database during playwright test, refresh tree to remove deleted database 2025-12-29 17:13:22 -05:00
Asier Isayas
ff5ebda58e record network calls on all retries 2025-12-29 15:54:33 -05:00
Asier Isayas
79e9f3a843 record network traces 2025-12-29 15:53:18 -05:00
Asier Isayas
9bdb995e14 refactor scale setup and tear down to be within each test 2025-12-29 14:01:44 -05:00
BChoudhury-ms
6167f94bc3 fix: restore SidePanel component for Container Copy feature (#2295) 2025-12-29 21:46:47 +05:30
Asier Isayas
de293d330c Merge branch 'master' of https://github.com/Azure/cosmos-explorer into users/jawelton/reenable-keys-mongocassandratests-122325 2025-12-29 11:07:32 -05:00
Asier Isayas
e9f12298cd run scale tests serially 2025-12-29 10:43:46 -05:00
BChoudhury-ms
be89c634f3 Add E2E tests for partition key change workflow (#2293) 2025-12-29 15:08:54 +05:30
Asier Isayas
d6a84af0a2 for scale and settings, dont create sample data in container 2025-12-26 21:09:36 -05:00
Asier Isayas
5a24db2230 create and delete container for every individual scale test 2025-12-26 18:10:15 -05:00
Asier Isayas
45049425c9 get new table button 2025-12-26 17:32:09 -05:00
Asier Isayas
74363ddfe9 click global new... button then collection in playwright tests 2025-12-26 14:24:38 -05:00
Jade Welton
ef1e26fc0c Another small bump to test shard count. 2025-12-26 07:15:48 -08:00
Jade Welton
e75af41844 Increase number of shards for playwright tests. 2025-12-26 06:47:37 -08:00
Jade Welton
7ac6a264bb Temporarily re-enable key based auth for Mongo and Cassandra tests. 2025-12-23 13:54:37 -08:00
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
68 changed files with 605 additions and 1249 deletions

View File

@@ -164,8 +164,8 @@ jobs:
strategy:
fail-fast: false
matrix:
shardIndex: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
shardTotal: [16]
shardIndex: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
shardTotal: [20]
steps:
- uses: actions/checkout@v4
- name: Use Node.js 18.x
@@ -198,18 +198,20 @@ jobs:
GREMLIN_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-gremlin.documents.azure.com/.default" -o tsv --query accessToken)
echo "::add-mask::$GREMLIN_TESTACCOUNT_TOKEN"
echo GREMLIN_TESTACCOUNT_TOKEN=$GREMLIN_TESTACCOUNT_TOKEN >> $GITHUB_ENV
CASSANDRA_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-cassandra.documents.azure.com/.default" -o tsv --query accessToken)
echo "::add-mask::$CASSANDRA_TESTACCOUNT_TOKEN"
echo CASSANDRA_TESTACCOUNT_TOKEN=$CASSANDRA_TESTACCOUNT_TOKEN >> $GITHUB_ENV
MONGO_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-mongo.documents.azure.com/.default" -o tsv --query accessToken)
echo "::add-mask::$MONGO_TESTACCOUNT_TOKEN"
echo MONGO_TESTACCOUNT_TOKEN=$MONGO_TESTACCOUNT_TOKEN >> $GITHUB_ENV
MONGO32_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-mongo32.documents.azure.com/.default" -o tsv --query accessToken)
echo "::add-mask::$MONGO32_TESTACCOUNT_TOKEN"
echo MONGO32_TESTACCOUNT_TOKEN=$MONGO32_TESTACCOUNT_TOKEN >> $GITHUB_ENV
MONGO_READONLY_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-mongo-readonly.documents.azure.com/.default" -o tsv --query accessToken)
echo "::add-mask::$MONGO_READONLY_TESTACCOUNT_TOKEN"
echo MONGO_READONLY_TESTACCOUNT_TOKEN=$MONGO_READONLY_TESTACCOUNT_TOKEN >> $GITHUB_ENV
# CASSANDRA_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-cassandra.documents.azure.com/.default" -o tsv --query accessToken)
# echo "::add-mask::$CASSANDRA_TESTACCOUNT_TOKEN"
# echo CASSANDRA_TESTACCOUNT_TOKEN=$CASSANDRA_TESTACCOUNT_TOKEN >> $GITHUB_ENV
# MONGO_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-mongo.documents.azure.com/.default" -o tsv --query accessToken)
# echo "::add-mask::$MONGO_TESTACCOUNT_TOKEN"
# echo MONGO_TESTACCOUNT_TOKEN=$MONGO_TESTACCOUNT_TOKEN >> $GITHUB_ENV
# MONGO32_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-mongo32.documents.azure.com/.default" -o tsv --query accessToken)
# echo "::add-mask::$MONGO32_TESTACCOUNT_TOKEN"
# echo MONGO32_TESTACCOUNT_TOKEN=$MONGO32_TESTACCOUNT_TOKEN >> $GITHUB_ENV
# MONGO_READONLY_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-mongo-readonly.documents.azure.com/.default" -o tsv --query accessToken)
# echo "::add-mask::$MONGO_READONLY_TESTACCOUNT_TOKEN"
# echo MONGO_READONLY_TESTACCOUNT_TOKEN=$MONGO_READONLY_TESTACCOUNT_TOKEN >> $GITHUB_ENV
- name: List tests in this shard
run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --list
- name: Run test shard ${{ matrix['shardIndex'] }} of ${{ matrix['shardTotal']}}
run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --workers=3
- name: Upload blob report to GitHub Actions Artifacts

View File

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

View File

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

View File

@@ -7,6 +7,7 @@ html {
body {
font-family: @FabricFont;
background-color: #f5f5f5;
--colorCompoundBrandBackground: @FabricAccentMedium;
}
a {
@@ -41,7 +42,7 @@ a:focus {
}
.nav-tabs-margin {
padding-top: 5px;
padding-top: 0px;
background-color: #ffffff;
}
@@ -68,17 +69,20 @@ a:focus {
}
.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:hover {
border-bottom: 2px solid @FabricAccentMedium;
border-bottom: none;
}
.nav-tabs > li.active > .tabNavContentContainer > .tab_Content > .contentWrapper > .tabNavText {
border-bottom: 0px none transparent;
}
.nav-tabs > li.active .contentWrapper .tabNavText {
border-bottom: 2px solid @FabricAccentMedium;
}
.tabNavContentContainer {
padding: @SmallSpace 0px @SmallSpace 0px;
@@ -214,6 +218,7 @@ a:focus {
.tabPanesContainer {
overflow: auto !important;
display: flex;
}
.tabs-container {

2
package-lock.json generated
View File

@@ -116,8 +116,8 @@
"tinykeys": "2.1.0",
"underscore": "1.12.1",
"utility-types": "3.10.0",
"web-vitals": "4.2.4",
"uuid": "9.0.0",
"web-vitals": "4.2.4",
"zustand": "3.5.0"
},
"devDependencies": {

View File

@@ -4,6 +4,12 @@ import { defineConfig, devices } from "@playwright/test";
*/
export default defineConfig({
testDir: "test",
testIgnore: [
"**/mongo/**",
"**/cassandra/**",
"**/gremlin/**",
"**/tables/**",
],
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 3 : 0,
@@ -11,8 +17,8 @@ export default defineConfig({
reporter: process.env.CI ? "blob" : "html",
timeout: 10 * 60 * 1000,
use: {
trace: "off",
video: "off",
trace: "on-all-retries",
video: "on-first-retry",
screenshot: "on",
testIdAttribute: "data-test",
contextOptions: {

View File

@@ -13,6 +13,7 @@ const LoadingOverlay: React.FC<LoadingOverlayProps> = ({ isLoading, label }) =>
return (
<Overlay
data-test="loading-overlay"
styles={{
root: {
backgroundColor: "rgba(255,255,255,0.9)",

View File

@@ -3,6 +3,7 @@
exports[`LoadingOverlay should handle long labels properly 1`] = `
<div
class="ms-Overlay root-109"
data-test="loading-overlay"
>
<div
class="ms-Spinner root-111"
@@ -22,6 +23,7 @@ exports[`LoadingOverlay should handle long labels properly 1`] = `
exports[`LoadingOverlay should render loading overlay when isLoading is true 1`] = `
<div
class="ms-Overlay root-109"
data-test="loading-overlay"
>
<div
class="ms-Spinner root-111"
@@ -41,6 +43,7 @@ exports[`LoadingOverlay should render loading overlay when isLoading is true 1`]
exports[`LoadingOverlay should render loading overlay with custom label 1`] = `
<div
class="ms-Overlay root-109"
data-test="loading-overlay"
>
<div
class="ms-Spinner root-111"
@@ -60,6 +63,7 @@ exports[`LoadingOverlay should render loading overlay with custom label 1`] = `
exports[`LoadingOverlay should render loading overlay with empty label 1`] = `
<div
class="ms-Overlay root-109"
data-test="loading-overlay"
>
<div
class="ms-Spinner root-111"

View File

@@ -1,3 +1,4 @@
import { configContext } from "ConfigContext";
import { ApiType, userContext } from "UserContext";
import * as NotificationConsoleUtils from "Utils/NotificationConsoleUtils";
import {
@@ -14,9 +15,12 @@ import {
DataTransferJobFeedResults,
DataTransferJobGetResults,
} from "Utils/arm/generatedClients/dataTransferService/types";
import { armRequest } from "Utils/arm/request";
import { addToPolling, removeFromPolling, updateDataTransferJob, useDataTransferJobs } from "hooks/useDataTransferJobs";
import promiseRetry, { AbortError, FailedAttemptError } from "p-retry";
export const DATA_TRANSFER_JOB_API_VERSION = "2025-05-01-preview";
export interface DataTransferParams {
jobName: string;
apiType: ApiType;
@@ -33,26 +37,34 @@ export const getDataTransferJobs = async (
subscriptionId: string,
resourceGroup: string,
accountName: string,
signal?: AbortSignal,
): Promise<DataTransferJobGetResults[]> => {
let dataTransferJobs: DataTransferJobGetResults[] = [];
let dataTransferFeeds: DataTransferJobFeedResults = await listByDatabaseAccount(
subscriptionId,
resourceGroup,
accountName,
signal,
);
dataTransferJobs = [...dataTransferJobs, ...(dataTransferFeeds?.value || [])];
while (dataTransferFeeds?.nextLink) {
const nextResponse = await window.fetch(dataTransferFeeds.nextLink, {
headers: {
Authorization: userContext.authorizationToken,
},
/**
* The `nextLink` URL returned by the Cosmos DB SQL API pointed to an incorrect endpoint, causing timeouts.
* (i.e: https://cdbmgmtprodby.documents.azure.com:450/subscriptions/{subId}/resourceGroups/{rg}/providers/Microsoft.DocumentDB/databaseAccounts/{account}/sql/dataTransferJobs?$top=100&$skiptoken=...)
* We manipulate the URL by parsing it to extract the path and query parameters,
* then construct the correct URL for the Azure Resource Manager (ARM) API.
* This ensures that the request is made to the correct base URL (`configContext.ARM_ENDPOINT`),
* which is required for ARM operations.
*/
const parsedUrl = new URL(dataTransferFeeds.nextLink);
const nextUrlPath = parsedUrl.pathname + parsedUrl.search;
dataTransferFeeds = await armRequest({
host: configContext.ARM_ENDPOINT,
path: nextUrlPath,
method: "GET",
apiVersion: DATA_TRANSFER_JOB_API_VERSION,
});
if (nextResponse.ok) {
dataTransferFeeds = await nextResponse.json();
dataTransferJobs = [...dataTransferJobs, ...(dataTransferFeeds?.value || [])];
} else {
break;
}
dataTransferJobs.push(...(dataTransferFeeds?.value || []));
}
return dataTransferJobs;
};

View File

@@ -35,6 +35,7 @@ const AddManagedIdentity: React.FC<AddManagedIdentityProps> = () => {
<InfoTooltip content={managedIdentityTooltip} />
</Text>
<Toggle
data-test="btn-toggle"
checked={systemAssigned}
onText={ContainerCopyMessages.toggleBtn.onText}
offText={ContainerCopyMessages.toggleBtn.offText}

View File

@@ -65,6 +65,7 @@ const AddReadPermissionToDefaultIdentity: React.FC<AddReadPermissionToDefaultIde
<InfoTooltip content={TooltipContent} />
</Text>
<Toggle
data-test="btn-toggle"
checked={readPermissionAssigned}
onText={ContainerCopyMessages.toggleBtn.onText}
offText={ContainerCopyMessages.toggleBtn.offText}

View File

@@ -12,7 +12,7 @@ import { useCopyJobPrerequisitesCache } from "../../Utils/useCopyJobPrerequisite
import usePermissionSections, { PermissionGroupConfig, PermissionSectionConfig } from "./hooks/usePermissionsSection";
const PermissionSection: React.FC<PermissionSectionConfig> = ({ id, title, Component, completed, disabled }) => (
<AccordionItem key={id} value={id} disabled={disabled}>
<AccordionItem key={id} value={id} disabled={disabled} data-test="accordion-item">
<AccordionHeader className="accordionHeader">
<Text className="accordionHeaderText" variant="medium">
{title}
@@ -25,13 +25,13 @@ const PermissionSection: React.FC<PermissionSectionConfig> = ({ id, title, Compo
height={completed ? 20 : 24}
/>
</AccordionHeader>
<AccordionPanel aria-disabled={disabled} className="accordionPanel">
<AccordionPanel aria-disabled={disabled} className="accordionPanel" data-test="accordion-panel">
<Component />
</AccordionPanel>
</AccordionItem>
);
const PermissionGroup: React.FC<PermissionGroupConfig> = ({ title, description, sections }) => {
const PermissionGroup: React.FC<PermissionGroupConfig> = ({ id, title, description, sections }) => {
const [openItems, setOpenItems] = React.useState<string[]>([]);
useEffect(() => {
@@ -44,6 +44,7 @@ const PermissionGroup: React.FC<PermissionGroupConfig> = ({ title, description,
return (
<Stack
data-test={`permission-group-container-${id}`}
tokens={{ childrenGap: 15 }}
styles={{
root: {
@@ -99,7 +100,11 @@ const AssignPermissions = () => {
}, []);
return (
<Stack className="assignPermissionsContainer" tokens={{ childrenGap: 20 }}>
<Stack
data-test="Panel:AssignPermissionsContainer"
className="assignPermissionsContainer"
tokens={{ childrenGap: 20 }}
>
<Text variant="medium">
{isSameAccount && copyJobState.migrationType === CopyJobMigrationType.Online
? ContainerCopyMessages.assignPermissions.intraAccountOnlineDescription(

View File

@@ -31,6 +31,7 @@ const DefaultManagedIdentity: React.FC<AddManagedIdentityProps> = () => {
<InfoTooltip content={managedIdentityTooltip} />
</div>
<Toggle
data-test="btn-toggle"
checked={defaultSystemAssigned}
onText={ContainerCopyMessages.toggleBtn.onText}
offText={ContainerCopyMessages.toggleBtn.offText}

View File

@@ -127,6 +127,7 @@ const PointInTimeRestore: React.FC = () => {
<Stack.Item>
{showRefreshButton ? (
<PrimaryButton
data-test="pointInTimeRestore:RefreshBtn"
className="fullWidth"
text={ContainerCopyMessages.refreshButtonLabel}
iconProps={{ iconName: "Refresh" }}
@@ -134,6 +135,7 @@ const PointInTimeRestore: React.FC = () => {
/>
) : (
<PrimaryButton
data-test="pointInTimeRestore:PrimaryBtn"
className="fullWidth"
text={loading ? "" : ContainerCopyMessages.pointInTimeRestore.buttonText}
{...(loading ? { iconProps: { iconName: "SyncStatusSolid" } } : {})}

View File

@@ -67,6 +67,7 @@ exports[`AddManagedIdentity Snapshot Tests renders initial state correctly 1`] =
class="ms-Toggle-background pill-117"
data-is-focusable="true"
data-ktp-target="true"
data-test="btn-toggle"
id="Toggle1"
role="switch"
type="button"
@@ -154,6 +155,7 @@ exports[`AddManagedIdentity Snapshot Tests renders loading state 1`] = `
class="ms-Toggle-background pill-121"
data-is-focusable="true"
data-ktp-target="true"
data-test="btn-toggle"
id="Toggle11"
role="switch"
type="button"
@@ -173,10 +175,12 @@ exports[`AddManagedIdentity Snapshot Tests renders loading state 1`] = `
</div>
<div
class="ms-Stack popover-container foreground loading css-123"
data-test="popover-container"
style="max-width: 450px;"
>
<div
class="ms-Overlay root-135"
data-test="loading-overlay"
>
<div
class="ms-Spinner root-137"
@@ -323,6 +327,7 @@ exports[`AddManagedIdentity Snapshot Tests renders with toggle on and popover vi
class="ms-Toggle-background pill-121"
data-is-focusable="true"
data-ktp-target="true"
data-test="btn-toggle"
id="Toggle3"
role="switch"
type="button"
@@ -342,6 +347,7 @@ exports[`AddManagedIdentity Snapshot Tests renders with toggle on and popover vi
</div>
<div
class="ms-Stack popover-container foreground css-123"
data-test="popover-container"
style="max-width: 450px;"
>
<span

View File

@@ -41,6 +41,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Edge Cases should handle m
class="ms-Toggle-background pill-115"
data-is-focusable="true"
data-ktp-target="true"
data-test="btn-toggle"
id="Toggle17"
role="switch"
type="button"
@@ -103,6 +104,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Edge Cases should handle m
class="ms-Toggle-background pill-115"
data-is-focusable="true"
data-ktp-target="true"
data-test="btn-toggle"
id="Toggle16"
role="switch"
type="button"
@@ -165,6 +167,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
class="ms-Toggle-background pill-115"
data-is-focusable="true"
data-ktp-target="true"
data-test="btn-toggle"
id="Toggle3"
role="switch"
type="button"
@@ -227,6 +230,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
class="ms-Toggle-background pill-119"
data-is-focusable="true"
data-ktp-target="true"
data-test="btn-toggle"
id="Toggle1"
role="switch"
type="button"
@@ -314,6 +318,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
class="ms-Toggle-background pill-115"
data-is-focusable="true"
data-ktp-target="true"
data-test="btn-toggle"
id="Toggle0"
role="switch"
type="button"
@@ -376,6 +381,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
class="ms-Toggle-background pill-115"
data-is-focusable="true"
data-ktp-target="true"
data-test="btn-toggle"
id="Toggle2"
role="switch"
type="button"

View File

@@ -4,6 +4,7 @@ exports[`AssignPermissions Component Accordion Behavior should render accordion
<div>
<div
class="ms-Stack assignPermissionsContainer css-109"
data-test="Panel:AssignPermissionsContainer"
>
<span
class="css-110"
@@ -15,6 +16,7 @@ exports[`AssignPermissions Component Accordion Behavior should render accordion
>
<div
class="ms-Stack css-112"
data-test="permission-group-container-testGroup"
>
<div
class="ms-Stack css-113"
@@ -36,6 +38,7 @@ exports[`AssignPermissions Component Accordion Behavior should render accordion
>
<div
class="fui-AccordionItem"
data-test="accordion-item"
>
<div
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
@@ -85,6 +88,7 @@ exports[`AssignPermissions Component Accordion Behavior should render accordion
</div>
<div
class="fui-AccordionItem"
data-test="accordion-item"
>
<div
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
@@ -134,6 +138,7 @@ exports[`AssignPermissions Component Accordion Behavior should render accordion
<div
aria-disabled="false"
class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu"
data-test="accordion-panel"
>
<div>
Incomplete Component
@@ -142,6 +147,7 @@ exports[`AssignPermissions Component Accordion Behavior should render accordion
</div>
<div
class="fui-AccordionItem"
data-test="accordion-item"
>
<div
class="fui-AccordionHeader accordionHeader ___lyghz50_53x5ri0 f1s2aq7o f1c21dwh f1s184ao ft85np5 fwrgwhw"
@@ -201,6 +207,7 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
<div>
<div
class="ms-Stack assignPermissionsContainer css-109"
data-test="Panel:AssignPermissionsContainer"
>
<span
class="css-110"
@@ -212,6 +219,7 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
>
<div
class="ms-Stack css-112"
data-test="permission-group-container-testGroup"
>
<div
class="ms-Stack css-113"
@@ -233,6 +241,7 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
>
<div
class="fui-AccordionItem"
data-test="accordion-item"
>
<div
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
@@ -282,6 +291,7 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
</div>
<div
class="fui-AccordionItem"
data-test="accordion-item"
>
<div
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
@@ -331,6 +341,7 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
<div
aria-disabled="false"
class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu"
data-test="accordion-panel"
>
<div>
Incomplete Component
@@ -339,6 +350,7 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
</div>
<div
class="fui-AccordionItem"
data-test="accordion-item"
>
<div
class="fui-AccordionHeader accordionHeader ___lyghz50_53x5ri0 f1s2aq7o f1c21dwh f1s184ao ft85np5 fwrgwhw"
@@ -398,6 +410,7 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
<div>
<div
class="ms-Stack assignPermissionsContainer css-109"
data-test="Panel:AssignPermissionsContainer"
>
<span
class="css-110"
@@ -409,6 +422,7 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
>
<div
class="ms-Stack css-112"
data-test="permission-group-container-testGroup"
>
<div
class="ms-Stack css-113"
@@ -430,6 +444,7 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
>
<div
class="fui-AccordionItem"
data-test="accordion-item"
>
<div
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
@@ -479,6 +494,7 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
</div>
<div
class="fui-AccordionItem"
data-test="accordion-item"
>
<div
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
@@ -528,6 +544,7 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
<div
aria-disabled="false"
class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu"
data-test="accordion-panel"
>
<div>
Incomplete Component
@@ -536,6 +553,7 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
</div>
<div
class="fui-AccordionItem"
data-test="accordion-item"
>
<div
class="fui-AccordionHeader accordionHeader ___lyghz50_53x5ri0 f1s2aq7o f1c21dwh f1s184ao ft85np5 fwrgwhw"
@@ -595,6 +613,7 @@ exports[`AssignPermissions Component Edge Cases should handle missing account na
<div>
<div
class="ms-Stack assignPermissionsContainer css-109"
data-test="Panel:AssignPermissionsContainer"
>
<span
class="css-110"
@@ -606,6 +625,7 @@ exports[`AssignPermissions Component Edge Cases should handle missing account na
>
<div
class="ms-Stack css-112"
data-test="permission-group-container-testGroup"
>
<div
class="ms-Stack css-113"
@@ -627,6 +647,7 @@ exports[`AssignPermissions Component Edge Cases should handle missing account na
>
<div
class="fui-AccordionItem"
data-test="accordion-item"
>
<div
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
@@ -676,6 +697,7 @@ exports[`AssignPermissions Component Edge Cases should handle missing account na
</div>
<div
class="fui-AccordionItem"
data-test="accordion-item"
>
<div
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
@@ -725,6 +747,7 @@ exports[`AssignPermissions Component Edge Cases should handle missing account na
<div
aria-disabled="false"
class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu"
data-test="accordion-panel"
>
<div>
Incomplete Component
@@ -733,6 +756,7 @@ exports[`AssignPermissions Component Edge Cases should handle missing account na
</div>
<div
class="fui-AccordionItem"
data-test="accordion-item"
>
<div
class="fui-AccordionHeader accordionHeader ___lyghz50_53x5ri0 f1s2aq7o f1c21dwh f1s184ao ft85np5 fwrgwhw"
@@ -792,6 +816,7 @@ exports[`AssignPermissions Component Permission Groups should render multiple pe
<div>
<div
class="ms-Stack assignPermissionsContainer css-109"
data-test="Panel:AssignPermissionsContainer"
>
<span
class="css-110"
@@ -803,6 +828,7 @@ exports[`AssignPermissions Component Permission Groups should render multiple pe
>
<div
class="ms-Stack css-112"
data-test="permission-group-container-crossAccountConfigs"
>
<div
class="ms-Stack css-113"
@@ -824,6 +850,7 @@ exports[`AssignPermissions Component Permission Groups should render multiple pe
>
<div
class="fui-AccordionItem"
data-test="accordion-item"
>
<div
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
@@ -875,6 +902,7 @@ exports[`AssignPermissions Component Permission Groups should render multiple pe
</div>
<div
class="ms-Stack css-112"
data-test="permission-group-container-onlineConfigs"
>
<div
class="ms-Stack css-113"
@@ -896,6 +924,7 @@ exports[`AssignPermissions Component Permission Groups should render multiple pe
>
<div
class="fui-AccordionItem"
data-test="accordion-item"
>
<div
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
@@ -945,6 +974,7 @@ exports[`AssignPermissions Component Permission Groups should render multiple pe
<div
aria-disabled="false"
class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu"
data-test="accordion-panel"
>
<div
data-testid="online-copy-enabled"
@@ -964,6 +994,7 @@ exports[`AssignPermissions Component Permission Groups should render online migr
<div>
<div
class="ms-Stack assignPermissionsContainer css-109"
data-test="Panel:AssignPermissionsContainer"
>
<span
class="css-110"
@@ -975,6 +1006,7 @@ exports[`AssignPermissions Component Permission Groups should render online migr
>
<div
class="ms-Stack css-112"
data-test="permission-group-container-onlineConfigs"
>
<div
class="ms-Stack css-113"
@@ -996,6 +1028,7 @@ exports[`AssignPermissions Component Permission Groups should render online migr
>
<div
class="fui-AccordionItem"
data-test="accordion-item"
>
<div
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
@@ -1045,6 +1078,7 @@ exports[`AssignPermissions Component Permission Groups should render online migr
</div>
<div
class="fui-AccordionItem"
data-test="accordion-item"
>
<div
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
@@ -1094,6 +1128,7 @@ exports[`AssignPermissions Component Permission Groups should render online migr
<div
aria-disabled="false"
class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu"
data-test="accordion-panel"
>
<div
data-testid="online-copy-enabled"
@@ -1113,6 +1148,7 @@ exports[`AssignPermissions Component Permission Groups should render permission
<div>
<div
class="ms-Stack assignPermissionsContainer css-109"
data-test="Panel:AssignPermissionsContainer"
>
<span
class="css-110"
@@ -1124,6 +1160,7 @@ exports[`AssignPermissions Component Permission Groups should render permission
>
<div
class="ms-Stack css-112"
data-test="permission-group-container-crossAccountConfigs"
>
<div
class="ms-Stack css-113"
@@ -1145,6 +1182,7 @@ exports[`AssignPermissions Component Permission Groups should render permission
>
<div
class="fui-AccordionItem"
data-test="accordion-item"
>
<div
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
@@ -1194,6 +1232,7 @@ exports[`AssignPermissions Component Permission Groups should render permission
</div>
<div
class="fui-AccordionItem"
data-test="accordion-item"
>
<div
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
@@ -1243,6 +1282,7 @@ exports[`AssignPermissions Component Permission Groups should render permission
<div
aria-disabled="false"
class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu"
data-test="accordion-panel"
>
<div
data-testid="add-read-permission"
@@ -1262,6 +1302,7 @@ exports[`AssignPermissions Component Rendering should render without crashing wi
<div>
<div
class="ms-Stack assignPermissionsContainer css-109"
data-test="Panel:AssignPermissionsContainer"
>
<span
class="css-110"
@@ -1283,6 +1324,7 @@ exports[`AssignPermissions Component Rendering should render without crashing wi
<div>
<div
class="ms-Stack assignPermissionsContainer css-109"
data-test="Panel:AssignPermissionsContainer"
>
<span
class="css-110"

View File

@@ -41,6 +41,7 @@ exports[`DefaultManagedIdentity Edge Cases should handle missing account name gr
class="ms-Toggle-background pill-115"
data-is-focusable="true"
data-ktp-target="true"
data-test="btn-toggle"
id="Toggle14"
role="switch"
type="button"
@@ -103,6 +104,7 @@ exports[`DefaultManagedIdentity Edge Cases should handle null account 1`] = `
class="ms-Toggle-background pill-115"
data-is-focusable="true"
data-ktp-target="true"
data-test="btn-toggle"
id="Toggle15"
role="switch"
type="button"
@@ -165,6 +167,7 @@ exports[`DefaultManagedIdentity Loading States should render loading state snaps
class="ms-Toggle-background pill-119"
data-is-focusable="true"
data-ktp-target="true"
data-test="btn-toggle"
id="Toggle10"
role="switch"
type="button"
@@ -256,6 +259,7 @@ exports[`DefaultManagedIdentity Rendering should render correctly with default s
class="ms-Toggle-background pill-115"
data-is-focusable="true"
data-ktp-target="true"
data-test="btn-toggle"
id="Toggle0"
role="switch"
type="button"
@@ -318,6 +322,7 @@ exports[`DefaultManagedIdentity Toggle Interactions should render toggle with ch
class="ms-Toggle-background pill-119"
data-is-focusable="true"
data-ktp-target="true"
data-test="btn-toggle"
id="Toggle7"
role="switch"
type="button"

View File

@@ -56,6 +56,7 @@ exports[`PointInTimeRestore Initial Render should render correctly with default
<button
class="ms-Button ms-Button--primary fullWidth root-115"
data-is-focusable="true"
data-test="pointInTimeRestore:PrimaryBtn"
type="button"
>
<span
@@ -131,6 +132,7 @@ exports[`PointInTimeRestore Initial Render should render with empty account name
<button
class="ms-Button ms-Button--primary fullWidth root-115"
data-is-focusable="true"
data-test="pointInTimeRestore:PrimaryBtn"
type="button"
>
<span
@@ -161,6 +163,7 @@ exports[`PointInTimeRestore Snapshots should match snapshot in loading state 1`]
>
<div
class="ms-Overlay root-123"
data-test="loading-overlay"
>
<div
class="ms-Spinner root-125"
@@ -223,6 +226,7 @@ exports[`PointInTimeRestore Snapshots should match snapshot in loading state 1`]
aria-disabled="true"
class="ms-Button ms-Button--primary is-disabled fullWidth root-128"
data-is-focusable="false"
data-test="pointInTimeRestore:PrimaryBtn"
disabled=""
type="button"
>
@@ -301,6 +305,7 @@ exports[`PointInTimeRestore Snapshots should match snapshot with refresh button
<button
class="ms-Button ms-Button--primary fullWidth root-115"
data-is-focusable="true"
data-test="pointInTimeRestore:RefreshBtn"
type="button"
>
<span

View File

@@ -19,9 +19,21 @@ const NavigationControls: React.FC<NavigationControlsProps> = ({
isPreviousDisabled,
}) => (
<Stack horizontal tokens={{ childrenGap: 20 }}>
<PrimaryButton text={primaryBtnText} onClick={onPrimary} allowDisabledFocus disabled={isPrimaryDisabled} />
<DefaultButton text="Previous" onClick={onPrevious} allowDisabledFocus disabled={isPreviousDisabled} />
<DefaultButton text="Cancel" onClick={onCancel} />
<PrimaryButton
data-test="copy-job-primary"
text={primaryBtnText}
onClick={onPrimary}
allowDisabledFocus
disabled={isPrimaryDisabled}
/>
<DefaultButton
data-test="copy-job-previous"
text="Previous"
onClick={onPrevious}
allowDisabledFocus
disabled={isPreviousDisabled}
/>
<DefaultButton data-test="copy-job-cancel" text="Cancel" onClick={onCancel} />
</Stack>
);

View File

@@ -17,6 +17,7 @@ const PopoverContainer: React.FC<PopoverContainerProps> = React.memo(
({ isLoading = false, title, children, onPrimary, onCancel }) => {
return (
<Stack
data-test="popover-container"
className={`popover-container foreground ${isLoading ? "loading" : ""}`}
tokens={{ childrenGap: 20 }}
style={{ maxWidth: 450 }}

View File

@@ -4,6 +4,7 @@ exports[`PopoverMessage Component Edge Cases should handle empty string title 1`
<div>
<div
class="ms-Stack popover-container foreground css-109"
data-test="popover-container"
style="max-width: 450px;"
>
<span
@@ -71,6 +72,7 @@ exports[`PopoverMessage Component Edge Cases should handle null children 1`] = `
<div>
<div
class="ms-Stack popover-container foreground css-109"
data-test="popover-container"
style="max-width: 450px;"
>
<span
@@ -133,6 +135,7 @@ exports[`PopoverMessage Component Edge Cases should handle undefined children 1`
<div>
<div
class="ms-Stack popover-container foreground css-109"
data-test="popover-container"
style="max-width: 450px;"
>
<span
@@ -195,6 +198,7 @@ exports[`PopoverMessage Component Edge Cases should handle very long title 1`] =
<div>
<div
class="ms-Stack popover-container foreground css-109"
data-test="popover-container"
style="max-width: 450px;"
>
<span
@@ -266,6 +270,7 @@ exports[`PopoverMessage Component Rendering should render correctly when visible
<div>
<div
class="ms-Stack popover-container foreground css-109"
data-test="popover-container"
style="max-width: 450px;"
>
<span
@@ -335,6 +340,7 @@ exports[`PopoverMessage Component Rendering should render correctly with differe
<div>
<div
class="ms-Stack popover-container foreground css-109"
data-test="popover-container"
style="max-width: 450px;"
>
<span
@@ -409,6 +415,7 @@ exports[`PopoverMessage Component Rendering should render correctly with differe
<div>
<div
class="ms-Stack popover-container foreground css-109"
data-test="popover-container"
style="max-width: 450px;"
>
<span
@@ -478,6 +485,7 @@ exports[`PopoverMessage Component Rendering should render correctly with loading
<div>
<div
class="ms-Stack popover-container foreground loading css-109"
data-test="popover-container"
style="max-width: 450px;"
>
<div

View File

@@ -22,6 +22,7 @@ const CreateCopyJobScreens: React.FC = () => {
<Stack.Item className="createCopyJobScreensContent">
{contextError && (
<MessageBar
data-test="Panel:ErrorContainer"
className="createCopyJobErrorMessageBar"
messageBarType={MessageBarType.blocked}
isMultiline={false}

View File

@@ -31,17 +31,17 @@ const PreviewCopyJob: React.FC = () => {
}));
};
return (
<Stack tokens={{ childrenGap: 20 }} className="previewCopyJobContainer">
<Stack tokens={{ childrenGap: 20 }} className="previewCopyJobContainer" data-test="Panel:PreviewCopyJob">
<FieldRow label={ContainerCopyMessages.jobNameLabel}>
<TextField value={jobName} onChange={onJobNameChange} />
<TextField data-test="job-name-textfield" value={jobName} onChange={onJobNameChange} />
</FieldRow>
<Stack>
<Text className="bold">{ContainerCopyMessages.sourceSubscriptionLabel}</Text>
<Text>{copyJobState.source?.subscription?.displayName}</Text>
<Text data-test="source-subscription-name">{copyJobState.source?.subscription?.displayName}</Text>
</Stack>
<Stack>
<Text className="bold">{ContainerCopyMessages.sourceAccountLabel}</Text>
<Text>{copyJobState.source?.account?.name}</Text>
<Text data-test="source-account-name">{copyJobState.source?.account?.name}</Text>
</Stack>
<Stack>
<DetailsList

View File

@@ -3,6 +3,7 @@
exports[`PreviewCopyJob should handle special characters in database and container names 1`] = `
<div
class="ms-Stack previewCopyJobContainer css-109"
data-test="Panel:PreviewCopyJob"
>
<div
class="ms-Stack flex-row css-110"
@@ -32,6 +33,7 @@ exports[`PreviewCopyJob should handle special characters in database and contain
<input
aria-invalid="false"
class="ms-TextField-field field-115"
data-test="job-name-textfield"
id="TextField84"
type="text"
value="job-with@special#chars_123"
@@ -51,6 +53,7 @@ exports[`PreviewCopyJob should handle special characters in database and contain
</span>
<span
class="css-125"
data-test="source-subscription-name"
>
Test Subscription
</span>
@@ -65,6 +68,7 @@ exports[`PreviewCopyJob should handle special characters in database and contain
</span>
<span
class="css-125"
data-test="source-account-name"
>
test-account
</span>
@@ -321,6 +325,7 @@ exports[`PreviewCopyJob should handle special characters in database and contain
exports[`PreviewCopyJob should render component with cross-subscription setup 1`] = `
<div
class="ms-Stack previewCopyJobContainer css-109"
data-test="Panel:PreviewCopyJob"
>
<div
class="ms-Stack flex-row css-110"
@@ -350,6 +355,7 @@ exports[`PreviewCopyJob should render component with cross-subscription setup 1`
<input
aria-invalid="false"
class="ms-TextField-field field-115"
data-test="job-name-textfield"
id="TextField96"
type="text"
value=""
@@ -369,6 +375,7 @@ exports[`PreviewCopyJob should render component with cross-subscription setup 1`
</span>
<span
class="css-125"
data-test="source-subscription-name"
>
Test Subscription
</span>
@@ -383,6 +390,7 @@ exports[`PreviewCopyJob should render component with cross-subscription setup 1`
</span>
<span
class="css-125"
data-test="source-account-name"
>
test-account
</span>
@@ -639,6 +647,7 @@ exports[`PreviewCopyJob should render component with cross-subscription setup 1`
exports[`PreviewCopyJob should render with default state and empty job name 1`] = `
<div
class="ms-Stack previewCopyJobContainer css-109"
data-test="Panel:PreviewCopyJob"
>
<div
class="ms-Stack flex-row css-110"
@@ -668,6 +677,7 @@ exports[`PreviewCopyJob should render with default state and empty job name 1`]
<input
aria-invalid="false"
class="ms-TextField-field field-115"
data-test="job-name-textfield"
id="TextField0"
type="text"
value=""
@@ -687,6 +697,7 @@ exports[`PreviewCopyJob should render with default state and empty job name 1`]
</span>
<span
class="css-125"
data-test="source-subscription-name"
>
Test Subscription
</span>
@@ -701,6 +712,7 @@ exports[`PreviewCopyJob should render with default state and empty job name 1`]
</span>
<span
class="css-125"
data-test="source-account-name"
>
test-account
</span>
@@ -957,6 +969,7 @@ exports[`PreviewCopyJob should render with default state and empty job name 1`]
exports[`PreviewCopyJob should render with long subscription and account names 1`] = `
<div
class="ms-Stack previewCopyJobContainer css-109"
data-test="Panel:PreviewCopyJob"
>
<div
class="ms-Stack flex-row css-110"
@@ -986,6 +999,7 @@ exports[`PreviewCopyJob should render with long subscription and account names 1
<input
aria-invalid="false"
class="ms-TextField-field field-115"
data-test="job-name-textfield"
id="TextField60"
type="text"
value=""
@@ -1005,6 +1019,7 @@ exports[`PreviewCopyJob should render with long subscription and account names 1
</span>
<span
class="css-125"
data-test="source-subscription-name"
>
This is a very long subscription name that might cause display issues if not handled properly
</span>
@@ -1019,6 +1034,7 @@ exports[`PreviewCopyJob should render with long subscription and account names 1
</span>
<span
class="css-125"
data-test="source-account-name"
>
this-is-a-very-long-database-account-name-that-might-cause-display-issues
</span>
@@ -1275,6 +1291,7 @@ exports[`PreviewCopyJob should render with long subscription and account names 1
exports[`PreviewCopyJob should render with missing source account information 1`] = `
<div
class="ms-Stack previewCopyJobContainer css-109"
data-test="Panel:PreviewCopyJob"
>
<div
class="ms-Stack flex-row css-110"
@@ -1304,6 +1321,7 @@ exports[`PreviewCopyJob should render with missing source account information 1`
<input
aria-invalid="false"
class="ms-TextField-field field-115"
data-test="job-name-textfield"
id="TextField36"
type="text"
value=""
@@ -1323,6 +1341,7 @@ exports[`PreviewCopyJob should render with missing source account information 1`
</span>
<span
class="css-125"
data-test="source-subscription-name"
>
Test Subscription
</span>
@@ -1588,6 +1607,7 @@ exports[`PreviewCopyJob should render with missing source account information 1`
exports[`PreviewCopyJob should render with missing source subscription information 1`] = `
<div
class="ms-Stack previewCopyJobContainer css-109"
data-test="Panel:PreviewCopyJob"
>
<div
class="ms-Stack flex-row css-110"
@@ -1617,6 +1637,7 @@ exports[`PreviewCopyJob should render with missing source subscription informati
<input
aria-invalid="false"
class="ms-TextField-field field-115"
data-test="job-name-textfield"
id="TextField24"
type="text"
value=""
@@ -1645,6 +1666,7 @@ exports[`PreviewCopyJob should render with missing source subscription informati
</span>
<span
class="css-125"
data-test="source-account-name"
>
test-account
</span>
@@ -1901,6 +1923,7 @@ exports[`PreviewCopyJob should render with missing source subscription informati
exports[`PreviewCopyJob should render with online migration type 1`] = `
<div
class="ms-Stack previewCopyJobContainer css-109"
data-test="Panel:PreviewCopyJob"
>
<div
class="ms-Stack flex-row css-110"
@@ -1930,6 +1953,7 @@ exports[`PreviewCopyJob should render with online migration type 1`] = `
<input
aria-invalid="false"
class="ms-TextField-field field-115"
data-test="job-name-textfield"
id="TextField72"
type="text"
value="online-migration-job"
@@ -1949,6 +1973,7 @@ exports[`PreviewCopyJob should render with online migration type 1`] = `
</span>
<span
class="css-125"
data-test="source-subscription-name"
>
Test Subscription
</span>
@@ -1963,6 +1988,7 @@ exports[`PreviewCopyJob should render with online migration type 1`] = `
</span>
<span
class="css-125"
data-test="source-account-name"
>
test-account
</span>
@@ -2219,6 +2245,7 @@ exports[`PreviewCopyJob should render with online migration type 1`] = `
exports[`PreviewCopyJob should render with pre-filled job name 1`] = `
<div
class="ms-Stack previewCopyJobContainer css-109"
data-test="Panel:PreviewCopyJob"
>
<div
class="ms-Stack flex-row css-110"
@@ -2248,6 +2275,7 @@ exports[`PreviewCopyJob should render with pre-filled job name 1`] = `
<input
aria-invalid="false"
class="ms-TextField-field field-115"
data-test="job-name-textfield"
id="TextField12"
type="text"
value="custom-job-name-123"
@@ -2267,6 +2295,7 @@ exports[`PreviewCopyJob should render with pre-filled job name 1`] = `
</span>
<span
class="css-125"
data-test="source-subscription-name"
>
Test Subscription
</span>
@@ -2281,6 +2310,7 @@ exports[`PreviewCopyJob should render with pre-filled job name 1`] = `
</span>
<span
class="css-125"
data-test="source-account-name"
>
test-account
</span>
@@ -2537,6 +2567,7 @@ exports[`PreviewCopyJob should render with pre-filled job name 1`] = `
exports[`PreviewCopyJob should render with undefined database and container names 1`] = `
<div
class="ms-Stack previewCopyJobContainer css-109"
data-test="Panel:PreviewCopyJob"
>
<div
class="ms-Stack flex-row css-110"
@@ -2566,6 +2597,7 @@ exports[`PreviewCopyJob should render with undefined database and container name
<input
aria-invalid="false"
class="ms-TextField-field field-115"
data-test="job-name-textfield"
id="TextField48"
type="text"
value=""
@@ -2585,6 +2617,7 @@ exports[`PreviewCopyJob should render with undefined database and container name
</span>
<span
class="css-125"
data-test="source-subscription-name"
>
Test Subscription
</span>
@@ -2599,6 +2632,7 @@ exports[`PreviewCopyJob should render with undefined database and container name
</span>
<span
class="css-125"
data-test="source-account-name"
>
test-account
</span>

View File

@@ -9,7 +9,7 @@ import ContainerCopyMessages from "../../../../ContainerCopyMessages";
import { CopyJobContext } from "../../../../Context/CopyJobContext";
import { CopyJobMigrationType } from "../../../../Enums/CopyJobEnums";
import { CopyJobContextProviderType, CopyJobContextState } from "../../../../Types/CopyJobTypes";
import { AccountDropdown } from "./AccountDropdown";
import { AccountDropdown, normalizeAccountId } from "./AccountDropdown";
jest.mock("../../../../../../hooks/useDatabaseAccounts");
jest.mock("../../../../../../UserContext", () => ({
@@ -202,13 +202,16 @@ describe("AccountDropdown", () => {
const stateUpdateFunction = mockSetCopyJobState.mock.calls[0][0];
const newState = stateUpdateFunction(mockCopyJobState);
expect(newState.source.account).toBe(mockDatabaseAccount1);
expect(newState.source.account).toEqual({
...mockDatabaseAccount1,
id: normalizeAccountId(mockDatabaseAccount1.id),
});
});
it("should auto-select predefined account from userContext if available", async () => {
const userContextAccount = {
...mockDatabaseAccount2,
id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/account2",
id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDB/databaseAccounts/account2",
};
(userContext as any).databaseAccount = userContextAccount;
@@ -223,7 +226,10 @@ describe("AccountDropdown", () => {
const stateUpdateFunction = mockSetCopyJobState.mock.calls[0][0];
const newState = stateUpdateFunction(mockCopyJobState);
expect(newState.source.account).toBe(mockDatabaseAccount2);
expect(newState.source.account).toEqual({
...mockDatabaseAccount2,
id: normalizeAccountId(mockDatabaseAccount2.id),
});
});
it("should keep current account if it exists in the filtered list", async () => {
@@ -248,7 +254,16 @@ describe("AccountDropdown", () => {
const stateUpdateFunction = mockSetCopyJobState.mock.calls[0][0];
const newState = stateUpdateFunction(contextWithSelectedAccount.copyJobState);
expect(newState).toBe(contextWithSelectedAccount.copyJobState);
expect(newState).toEqual({
...contextWithSelectedAccount.copyJobState,
source: {
...contextWithSelectedAccount.copyJobState.source,
account: {
...mockDatabaseAccount1,
id: normalizeAccountId(mockDatabaseAccount1.id),
},
},
});
});
it("should handle account change when user selects different account", async () => {
@@ -272,7 +287,7 @@ describe("AccountDropdown", () => {
it("should normalize account ID for Portal platform", () => {
const portalAccount = {
...mockDatabaseAccount1,
id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/account1",
id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDB/databaseAccounts/account1",
};
(configContext as any).platform = Platform.Portal;

View File

@@ -12,7 +12,7 @@ import FieldRow from "../../Components/FieldRow";
interface AccountDropdownProps {}
const normalizeAccountId = (id: string) => {
export const normalizeAccountId = (id: string = "") => {
if (configContext.platform === Platform.Portal) {
return id.replace("/Microsoft.DocumentDb/", "/Microsoft.DocumentDB/");
} else if (configContext.platform === Platform.Hosted) {
@@ -27,7 +27,12 @@ export const AccountDropdown: React.FC<AccountDropdownProps> = () => {
const selectedSubscriptionId = copyJobState?.source?.subscription?.subscriptionId;
const allAccounts: DatabaseAccount[] = useDatabaseAccounts(selectedSubscriptionId);
const sqlApiOnlyAccounts: DatabaseAccount[] = (allAccounts || []).filter((account) => apiType(account) === "SQL");
const sqlApiOnlyAccounts = (allAccounts || [])
.filter((account) => apiType(account) === "SQL")
.map((account) => ({
...account,
id: normalizeAccountId(account.id),
}));
const updateCopyJobState = (newAccount: DatabaseAccount) => {
setCopyJobState((prevState) => {
@@ -47,7 +52,7 @@ export const AccountDropdown: React.FC<AccountDropdownProps> = () => {
useEffect(() => {
if (sqlApiOnlyAccounts && sqlApiOnlyAccounts.length > 0 && selectedSubscriptionId) {
const currentAccountId = copyJobState?.source?.account?.id;
const predefinedAccountId = userContext.databaseAccount?.id;
const predefinedAccountId = normalizeAccountId(userContext.databaseAccount?.id);
const selectedAccountId = currentAccountId || predefinedAccountId;
const targetAccount: DatabaseAccount | null =
@@ -58,7 +63,7 @@ export const AccountDropdown: React.FC<AccountDropdownProps> = () => {
const accountOptions =
sqlApiOnlyAccounts?.map((account) => ({
key: normalizeAccountId(account.id),
key: account.id,
text: account.name,
data: account,
})) || [];

View File

@@ -10,7 +10,7 @@ interface MigrationTypeCheckboxProps {
}
export const MigrationTypeCheckbox: React.FC<MigrationTypeCheckboxProps> = React.memo(({ checked, onChange }) => (
<Stack horizontal horizontalAlign="space-between" className="migrationTypeRow">
<Stack horizontal horizontalAlign="space-between" className="migrationTypeRow" data-test="migration-type-checkbox">
<Checkbox label={ContainerCopyMessages.migrationTypeCheckboxLabel} checked={checked} onChange={onChange} />
</Stack>
));

View File

@@ -3,6 +3,7 @@
exports[`MigrationTypeCheckbox Component Rendering should render in checked state 1`] = `
<div
class="ms-Stack migrationTypeRow css-109"
data-test="migration-type-checkbox"
>
<div
class="ms-Checkbox is-checked is-enabled root-119"
@@ -43,6 +44,7 @@ exports[`MigrationTypeCheckbox Component Rendering should render in checked stat
exports[`MigrationTypeCheckbox Component Rendering should render with default props (unchecked state) 1`] = `
<div
class="ms-Stack migrationTypeRow css-109"
data-test="migration-type-checkbox"
>
<div
class="ms-Checkbox is-enabled root-110"

View File

@@ -47,7 +47,11 @@ const SelectSourceAndTargetContainers = ({ showAddCollectionPanel }: SelectSourc
const onDropdownChange = dropDownChangeHandler(setCopyJobState);
return (
<Stack className="selectSourceAndTargetContainers" tokens={{ childrenGap: 25 }}>
<Stack
data-test="Panel:SelectSourceAndTargetContainers"
className="selectSourceAndTargetContainers"
tokens={{ childrenGap: 25 }}
>
<span>{ContainerCopyMessages.selectSourceAndTargetContainersDescription}</span>
<DatabaseContainerSection
heading={ContainerCopyMessages.sourceContainerSubHeading}
@@ -59,6 +63,7 @@ const SelectSourceAndTargetContainers = ({ showAddCollectionPanel }: SelectSourc
selectedContainer={source?.containerId}
containerDisabled={!source?.databaseId}
containerOnChange={onDropdownChange("sourceContainer")}
sectionType="source"
/>
<DatabaseContainerSection
heading={ContainerCopyMessages.targetContainerSubHeading}
@@ -71,6 +76,7 @@ const SelectSourceAndTargetContainers = ({ showAddCollectionPanel }: SelectSourc
containerDisabled={!target?.databaseId}
containerOnChange={onDropdownChange("targetContainer")}
handleOnDemandCreateContainer={showAddCollectionPanel}
sectionType="target"
/>
</Stack>
);

View File

@@ -32,6 +32,7 @@ describe("DatabaseContainerSection", () => {
selectedContainer: "container1",
containerDisabled: false,
containerOnChange: mockContainerOnChange,
sectionType: "source",
};
beforeEach(() => {
@@ -292,6 +293,7 @@ describe("DatabaseContainerSection", () => {
containerOptions: mockContainerOptions,
selectedContainer: "container1",
containerOnChange: mockContainerOnChange,
sectionType: "source",
};
render(<DatabaseContainerSection {...minimalProps} />);
@@ -393,6 +395,7 @@ describe("DatabaseContainerSection", () => {
containerOptions: [{ key: "c1", text: "Container 1", data: { id: "c1" } }],
selectedContainer: "c1",
containerOnChange: jest.fn(),
sectionType: "source",
};
const { container } = render(<DatabaseContainerSection {...minimalProps} />);
@@ -411,6 +414,7 @@ describe("DatabaseContainerSection", () => {
containerDisabled: false,
containerOnChange: jest.fn(),
handleOnDemandCreateContainer: jest.fn(),
sectionType: "target",
};
const { container } = render(<DatabaseContainerSection {...fullProps} />);
@@ -428,6 +432,7 @@ describe("DatabaseContainerSection", () => {
selectedContainer: "container1",
containerDisabled: true,
containerOnChange: jest.fn(),
sectionType: "target",
};
const { container } = render(<DatabaseContainerSection {...disabledProps} />);
@@ -443,6 +448,7 @@ describe("DatabaseContainerSection", () => {
containerOptions: [],
selectedContainer: "",
containerOnChange: jest.fn(),
sectionType: "target",
};
const { container } = render(<DatabaseContainerSection {...emptyOptionsProps} />);

View File

@@ -15,6 +15,7 @@ export const DatabaseContainerSection = ({
containerDisabled,
containerOnChange,
handleOnDemandCreateContainer,
sectionType,
}: DatabaseContainerSectionProps) => (
<Stack tokens={{ childrenGap: 15 }} className="databaseContainerSection">
<label className="subHeading">{heading}</label>
@@ -27,6 +28,7 @@ export const DatabaseContainerSection = ({
disabled={!!databaseDisabled}
selectedKey={selectedDatabase}
onChange={databaseOnChange}
data-test={`${sectionType}-databaseDropdown`}
/>
</FieldRow>
<FieldRow label={ContainerCopyMessages.containerDropdownLabel}>
@@ -39,6 +41,7 @@ export const DatabaseContainerSection = ({
disabled={!!containerDisabled}
selectedKey={selectedContainer}
onChange={containerOnChange}
data-test={`${sectionType}-containerDropdown`}
/>
{handleOnDemandCreateContainer && (
<ActionButton className="create-container-link-btn" onClick={() => handleOnDemandCreateContainer()}>

View File

@@ -37,6 +37,7 @@ exports[`DatabaseContainerSection Snapshot Testing matches snapshot with all pro
class="ms-Dropdown is-required dropdown-112"
data-is-focusable="true"
data-ktp-target="true"
data-test="target-databaseDropdown"
id="Dropdown98"
role="combobox"
tabindex="0"
@@ -94,6 +95,7 @@ exports[`DatabaseContainerSection Snapshot Testing matches snapshot with all pro
class="ms-Dropdown is-required dropdown-112"
data-is-focusable="true"
data-ktp-target="true"
data-test="target-containerDropdown"
id="Dropdown99"
role="combobox"
tabindex="0"
@@ -182,6 +184,7 @@ exports[`DatabaseContainerSection Snapshot Testing matches snapshot with disable
class="ms-Dropdown is-disabled is-required dropdown-143"
data-is-focusable="false"
data-ktp-target="true"
data-test="target-databaseDropdown"
id="Dropdown103"
role="combobox"
tabindex="-1"
@@ -239,6 +242,7 @@ exports[`DatabaseContainerSection Snapshot Testing matches snapshot with disable
class="ms-Dropdown is-disabled is-required dropdown-143"
data-is-focusable="false"
data-ktp-target="true"
data-test="target-containerDropdown"
id="Dropdown104"
role="combobox"
tabindex="-1"
@@ -306,6 +310,7 @@ exports[`DatabaseContainerSection Snapshot Testing matches snapshot with empty o
class="ms-Dropdown is-required dropdown-112"
data-is-focusable="true"
data-ktp-target="true"
data-test="target-databaseDropdown"
id="Dropdown105"
role="combobox"
tabindex="0"
@@ -363,6 +368,7 @@ exports[`DatabaseContainerSection Snapshot Testing matches snapshot with empty o
class="ms-Dropdown is-required dropdown-112"
data-is-focusable="true"
data-ktp-target="true"
data-test="target-containerDropdown"
id="Dropdown106"
role="combobox"
tabindex="0"
@@ -430,6 +436,7 @@ exports[`DatabaseContainerSection Snapshot Testing matches snapshot with minimal
class="ms-Dropdown is-required dropdown-112"
data-is-focusable="true"
data-ktp-target="true"
data-test="source-databaseDropdown"
id="Dropdown96"
role="combobox"
tabindex="0"
@@ -487,6 +494,7 @@ exports[`DatabaseContainerSection Snapshot Testing matches snapshot with minimal
class="ms-Dropdown is-required dropdown-112"
data-is-focusable="true"
data-ktp-target="true"
data-test="source-containerDropdown"
id="Dropdown97"
role="combobox"
tabindex="0"

View File

@@ -83,6 +83,7 @@ const CopyJobActionMenu: React.FC<CopyJobActionMenuProps> = ({ job, handleClick
return (
<IconButton
data-test={`CopyJobActionMenu/Button:${job.Name}`}
role="button"
iconProps={{ iconName: "More", styles: { root: { fontSize: "20px", fontWeight: "bold" } } }}
menuProps={{ items: getMenuItems() }}

View File

@@ -80,6 +80,7 @@ const CopyJobsList: React.FC<CopyJobsListProps> = ({ jobs, handleActionClick, pa
<Stack.Item verticalFill={true} grow={1} shrink={1} style={styles.stackItem}>
<ScrollablePane scrollbarVisibility={ScrollbarVisibility.auto}>
<ShimmeredDetailsList
className="CopyJobListContainer"
onRenderRow={_onRenderRow}
checkboxVisibility={2}
columns={columns}

View File

@@ -49,6 +49,7 @@ export interface DatabaseContainerSectionProps {
containerDisabled?: boolean;
containerOnChange: (ev: React.FormEvent<HTMLDivElement>, option: DropdownOptionType) => void;
handleOnDemandCreateContainer?: () => void;
sectionType: "source" | "target";
}
export interface CopyJobContextState {

View File

@@ -1,8 +1,5 @@
import { IndexingPolicy } from "@azure/cosmos";
import { act } from "@testing-library/react";
import { AuthType } from "AuthType";
import { shallow } from "enzyme";
import { useIndexingPolicyStore } from "Explorer/Tabs/QueryTab/ResultsView";
import ko from "knockout";
import React from "react";
import { updateCollection } from "../../../Common/dataAccess/updateCollection";
@@ -447,49 +444,3 @@ describe("SettingsComponent", () => {
expect(settingsComponentInstance.isSaveSettingsButtonEnabled()).toBe(true);
});
});
describe("SettingsComponent - indexing policy subscription", () => {
const baseProps: SettingsComponentProps = {
settingsTab: new CollectionSettingsTabV2({
collection: collection,
tabKind: ViewModels.CollectionTabKind.CollectionSettingsV2,
title: "Scale & Settings",
tabPath: "",
node: undefined,
}),
};
it("subscribes to the correct container's indexing policy and updates state on change", async () => {
const containerId = collection.id();
const mockIndexingPolicy: IndexingPolicy = {
automatic: false,
indexingMode: "lazy",
includedPaths: [{ path: "/foo/*" }],
excludedPaths: [{ path: "/bar/*" }],
compositeIndexes: [],
spatialIndexes: [],
vectorIndexes: [],
fullTextIndexes: [],
};
const wrapper = shallow(<SettingsComponent {...baseProps} />);
const instance = wrapper.instance() as SettingsComponent;
await act(async () => {
useIndexingPolicyStore.setState({
indexingPolicies: {
[containerId]: mockIndexingPolicy,
},
});
// Wait for the async refreshCollectionData to complete
await new Promise((resolve) => setTimeout(resolve, 0));
});
wrapper.update();
expect(wrapper.state("indexingPolicyContent")).toEqual(mockIndexingPolicy);
expect(wrapper.state("indexingPolicyContentBaseline")).toEqual(mockIndexingPolicy);
// @ts-expect-error: rawDataModel is intentionally accessed for test validation
expect(instance.collection.rawDataModel.indexingPolicy).toEqual(mockIndexingPolicy);
});
});

View File

@@ -13,7 +13,6 @@ import {
ThroughputBucketsComponent,
ThroughputBucketsComponentProps,
} from "Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputBucketsComponent";
import { useIndexingPolicyStore } from "Explorer/Tabs/QueryTab/ResultsView";
import { useDatabases } from "Explorer/useDatabases";
import { isFabricNative } from "Platform/Fabric/FabricUtil";
import { isCapabilityEnabled, isVectorSearchEnabled } from "Utils/CapabilityUtils";
@@ -74,6 +73,7 @@ import {
parseConflictResolutionMode,
parseConflictResolutionProcedure,
} from "./SettingsUtils";
interface SettingsV2TabInfo {
tab: SettingsV2TabTypes;
content: JSX.Element;
@@ -182,7 +182,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
private totalThroughputUsed: number;
private throughputBucketsEnabled: boolean;
public mongoDBCollectionResource: MongoDBCollectionResource;
private unsubscribe: () => void;
constructor(props: SettingsComponentProps) {
super(props);
@@ -312,13 +312,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
if (this.isCollectionSettingsTab) {
this.refreshIndexTransformationProgress();
this.loadMongoIndexes();
this.unsubscribe = useIndexingPolicyStore.subscribe(
() => {
this.refreshCollectionData();
},
(state) => state.indexingPolicies[this.collection?.id()],
);
this.refreshCollectionData();
}
this.setBaseline();
@@ -326,11 +319,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
useCommandBar.getState().setContextButtons(this.getTabsButtons());
}
}
componentWillUnmount(): void {
if (this.unsubscribe) {
this.unsubscribe();
}
}
componentDidUpdate(): void {
if (this.props.settingsTab.isActive()) {
useCommandBar.getState().setContextButtons(this.getTabsButtons());
@@ -860,6 +849,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
{ name: "name_of_property", query: "query_to_compute_property" },
] as DataModels.ComputedProperties;
}
const throughputBuckets = this.offer?.throughputBuckets;
return {
@@ -1019,31 +1009,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
startKey,
);
};
private refreshCollectionData = async (): Promise<void> => {
const containerId = this.collection.id();
const latestIndexingPolicy = useIndexingPolicyStore.getState().indexingPolicies[containerId];
const rawPolicy = latestIndexingPolicy ?? this.collection.indexingPolicy();
const latestCollection: DataModels.IndexingPolicy = {
automatic: rawPolicy?.automatic ?? true,
indexingMode: rawPolicy?.indexingMode ?? "consistent",
includedPaths: rawPolicy?.includedPaths ?? [],
excludedPaths: rawPolicy?.excludedPaths ?? [],
compositeIndexes: rawPolicy?.compositeIndexes ?? [],
spatialIndexes: rawPolicy?.spatialIndexes ?? [],
vectorIndexes: rawPolicy?.vectorIndexes ?? [],
fullTextIndexes: rawPolicy?.fullTextIndexes ?? [],
};
this.collection.rawDataModel.indexingPolicy = latestCollection;
this.setState({
indexingPolicyContent: latestCollection,
indexingPolicyContentBaseline: latestCollection,
});
};
private saveCollectionSettings = async (startKey: number): Promise<void> => {
const newCollection: DataModels.Collection = { ...this.collection.rawDataModel };
if (
this.state.isSubSettingsSaveable ||
this.state.isContainerPolicyDirty ||
@@ -1283,6 +1252,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
onScaleDiscardableChange: this.onScaleDiscardableChange,
throughputError: this.state.throughputError,
};
if (!this.isCollectionSettingsTab) {
return (
<div className="settingsV2MainContainer">

View File

@@ -1,10 +1,10 @@
import { MessageBar, MessageBarType } from "@fluentui/react";
import * as React from "react";
import { handleError } from "../../../../../Common/ErrorHandlingUtils";
import { MessageBar, MessageBarType } from "@fluentui/react";
import {
mongoIndexTransformationRefreshingMessage,
renderMongoIndexTransformationRefreshMessage,
} from "../../SettingsRenderUtils";
import { handleError } from "../../../../../Common/ErrorHandlingUtils";
import { isIndexTransforming } from "../../SettingsUtils";
export interface IndexingPolicyRefreshComponentProps {

View File

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

View File

@@ -78,6 +78,7 @@ exports[`PartitionKeyComponent renders default component and matches snapshot 1`
</Text>
</Stack>
<Stack
data-test="partition-key-values"
tokens={
{
"childrenGap": 5,
@@ -108,6 +109,7 @@ exports[`PartitionKeyComponent renders default component and matches snapshot 1`
</Stack>
</Stack>
<StyledMessageBar
data-test="partition-key-warning"
messageBarIconProps={
{
"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.
</Text>
<CustomizedPrimaryButton
data-test="change-partition-key-button"
onClick={[Function]}
styles={
{
@@ -237,6 +240,7 @@ exports[`PartitionKeyComponent renders read-only component and matches snapshot
</Text>
</Stack>
<Stack
data-test="partition-key-values"
tokens={
{
"childrenGap": 5,

View File

@@ -153,16 +153,6 @@ exports[`SettingsComponent renders 1`] = `
"partitionKey",
],
"rawDataModel": {
"indexingPolicy": {
"automatic": true,
"compositeIndexes": [],
"excludedPaths": [],
"fullTextIndexes": [],
"includedPaths": [],
"indexingMode": "consistent",
"spatialIndexes": [],
"vectorIndexes": [],
},
"uniqueKeyPolicy": {
"uniqueKeys": [
{
@@ -274,16 +264,6 @@ exports[`SettingsComponent renders 1`] = `
"partitionKey",
],
"rawDataModel": {
"indexingPolicy": {
"automatic": true,
"compositeIndexes": [],
"excludedPaths": [],
"fullTextIndexes": [],
"includedPaths": [],
"indexingMode": "consistent",
"spatialIndexes": [],
"vectorIndexes": [],
},
"uniqueKeyPolicy": {
"uniqueKeys": [
{
@@ -496,16 +476,6 @@ exports[`SettingsComponent renders 1`] = `
"partitionKey",
],
"rawDataModel": {
"indexingPolicy": {
"automatic": true,
"compositeIndexes": [],
"excludedPaths": [],
"fullTextIndexes": [],
"includedPaths": [],
"indexingMode": "consistent",
"spatialIndexes": [],
"vectorIndexes": [],
},
"uniqueKeyPolicy": {
"uniqueKeys": [
{
@@ -683,16 +653,6 @@ exports[`SettingsComponent renders 1`] = `
"partitionKey",
],
"rawDataModel": {
"indexingPolicy": {
"automatic": true,
"compositeIndexes": [],
"excludedPaths": [],
"fullTextIndexes": [],
"includedPaths": [],
"indexingMode": "consistent",
"spatialIndexes": [],
"vectorIndexes": [],
},
"uniqueKeyPolicy": {
"uniqueKeys": [
{

View File

@@ -437,13 +437,14 @@ export default class Explorer {
public onRefreshResourcesClick = async (): Promise<void> => {
if (isFabricMirroredKey()) {
scheduleRefreshFabricToken(true).then(() => this.refreshAllDatabases());
return;
} else {
await (userContext.authType === AuthType.ResourceToken
? this.refreshDatabaseForResourceToken()
: this.refreshAllDatabases());
await this.refreshNotebookList();
}
await (userContext.authType === AuthType.ResourceToken
? this.refreshDatabaseForResourceToken()
: this.refreshAllDatabases());
await this.refreshNotebookList();
logConsoleInfo("Successfully refreshed databases");
};
// Facade

View File

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

View File

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

View File

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

View File

@@ -1,107 +0,0 @@
import { CircleFilled } from "@fluentui/react-icons";
import type { IIndexMetric } from "Explorer/Tabs/QueryTab/ResultsView";
import { useIndexAdvisorStyles } from "Explorer/Tabs/QueryTab/StylesAdvisor";
import * as React from "react";
// SDK response format
export interface IndexMetricsResponse {
UtilizedIndexes?: {
SingleIndexes?: Array<{ IndexSpec: string; IndexImpactScore?: string }>;
CompositeIndexes?: Array<{ IndexSpecs: string[]; IndexImpactScore?: string }>;
};
PotentialIndexes?: {
SingleIndexes?: Array<{ IndexSpec: string; IndexImpactScore?: string }>;
CompositeIndexes?: Array<{ IndexSpecs: string[]; IndexImpactScore?: string }>;
};
}
export function parseIndexMetrics(indexMetrics: IndexMetricsResponse): {
included: IIndexMetric[];
notIncluded: IIndexMetric[];
} {
const included: IIndexMetric[] = [];
const notIncluded: IIndexMetric[] = [];
// Process UtilizedIndexes (Included in Current Policy)
if (indexMetrics.UtilizedIndexes) {
// Single indexes
indexMetrics.UtilizedIndexes.SingleIndexes?.forEach((index) => {
included.push({
index: index.IndexSpec,
impact: index.IndexImpactScore || "Utilized",
section: "Included",
path: index.IndexSpec,
});
});
// Composite indexes
indexMetrics.UtilizedIndexes.CompositeIndexes?.forEach((index) => {
const compositeSpec = index.IndexSpecs.join(", ");
included.push({
index: compositeSpec,
impact: index.IndexImpactScore || "Utilized",
section: "Included",
composite: index.IndexSpecs.map((spec) => {
const [path, order] = spec.trim().split(/\s+/);
return {
path: path.trim(),
order: order?.toLowerCase() === "desc" ? "descending" : "ascending",
};
}),
});
});
}
// Process PotentialIndexes (Not Included in Current Policy)
if (indexMetrics.PotentialIndexes) {
// Single indexes
indexMetrics.PotentialIndexes.SingleIndexes?.forEach((index) => {
notIncluded.push({
index: index.IndexSpec,
impact: index.IndexImpactScore || "Unknown",
section: "Not Included",
path: index.IndexSpec,
});
});
// Composite indexes
indexMetrics.PotentialIndexes.CompositeIndexes?.forEach((index) => {
const compositeSpec = index.IndexSpecs.join(", ");
notIncluded.push({
index: compositeSpec,
impact: index.IndexImpactScore || "Unknown",
section: "Not Included",
composite: index.IndexSpecs.map((spec) => {
const [path, order] = spec.trim().split(/\s+/);
return {
path: path.trim(),
order: order?.toLowerCase() === "desc" ? "descending" : "ascending",
};
}),
});
});
}
return { included, notIncluded };
}
export const renderImpactDots = (impact: string): JSX.Element => {
const style = useIndexAdvisorStyles();
let count = 0;
if (impact === "High") {
count = 3;
} else if (impact === "Medium") {
count = 2;
} else if (impact === "Low") {
count = 1;
}
return (
<div className={style.indexAdvisorImpactDots}>
{Array.from({ length: count }).map((_, i) => (
<CircleFilled key={i} className={style.indexAdvisorImpactDot} />
))}
</div>
);
};

View File

@@ -3,21 +3,18 @@ import QueryError from "Common/QueryError";
import { IndeterminateProgressBar } from "Explorer/Controls/IndeterminateProgressBar";
import { MessageBanner } from "Explorer/Controls/MessageBanner";
import { useQueryTabStyles } from "Explorer/Tabs/QueryTab/Styles";
import useZoomLevel from "hooks/useZoomLevel";
import React from "react";
import { conditionalClass } from "Utils/StyleUtils";
import RunQuery from "../../../../images/RunQuery.png";
import { QueryResults } from "../../../Contracts/ViewModels";
import { ErrorList } from "./ErrorList";
import { ResultsView } from "./ResultsView";
import useZoomLevel from "hooks/useZoomLevel";
import { conditionalClass } from "Utils/StyleUtils";
export interface ResultsViewProps {
isMongoDB: boolean;
queryResults: QueryResults;
executeQueryDocumentsPage: (firstItemIndex: number) => Promise<void>;
queryEditorContent?: string;
databaseId?: string;
containerId?: string;
}
interface QueryResultProps extends ResultsViewProps {
@@ -52,8 +49,6 @@ export const QueryResultSection: React.FC<QueryResultProps> = ({
queryResults,
executeQueryDocumentsPage,
isExecuting,
databaseId,
containerId,
}: QueryResultProps): JSX.Element => {
const styles = useQueryTabStyles();
const maybeSubQuery = queryEditorContent && /.*\(.*SELECT.*\)/i.test(queryEditorContent);
@@ -96,9 +91,6 @@ export const QueryResultSection: React.FC<QueryResultProps> = ({
queryResults={queryResults}
executeQueryDocumentsPage={executeQueryDocumentsPage}
isMongoDB={isMongoDB}
queryEditorContent={queryEditorContent}
databaseId={databaseId}
containerId={containerId}
/>
) : (
<ExecuteQueryCallToAction />

View File

@@ -52,9 +52,8 @@ describe("QueryTabComponent", () => {
copilotVersion: "v3.0",
},
});
const propsMock: Readonly<IQueryTabComponentProps> = {
collection: { databaseId: "CopilotSampleDB", id: () => "CopilotContainer" },
collection: { databaseId: "CopilotSampleDB" },
onTabAccessor: () => jest.fn(),
isExecutionError: false,
tabId: "mockTabId",

View File

@@ -28,7 +28,6 @@ import { useMonacoTheme } from "hooks/useTheme";
import React, { Fragment, createRef } from "react";
import "react-splitter-layout/lib/index.css";
import { format } from "react-string-format";
import create from "zustand";
//TODO: Uncomment next two lines when query copilot is reinstated in DE
// import QueryCommandIcon from "../../../../images/CopilotCommand.svg";
// import LaunchCopilot from "../../../../images/CopilotTabIcon.svg";
@@ -58,20 +57,6 @@ import { SaveQueryPane } from "../../Panes/SaveQueryPane/SaveQueryPane";
import TabsBase from "../TabsBase";
import "./QueryTabComponent.less";
export interface QueryMetadataStore {
userQuery: string;
databaseId: string;
containerId: string;
setMetadata: (query1: string, db: string, container: string) => void;
}
export const useQueryMetadataStore = create<QueryMetadataStore>((set) => ({
userQuery: "",
databaseId: "",
containerId: "",
setMetadata: (query1, db, container) => set({ userQuery: query1, databaseId: db, containerId: container }),
}));
enum ToggleState {
Result,
QueryMetrics,
@@ -279,10 +264,6 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
}
public onExecuteQueryClick = async (): Promise<void> => {
const query1 = this.state.sqlQueryEditorContent;
const db = this.props.collection.databaseId;
const container = this.props.collection.id();
useQueryMetadataStore.getState().setMetadata(query1, db, container);
this._iterator = undefined;
setTimeout(async () => {
@@ -799,8 +780,6 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
errors={this.props.copilotStore?.errors}
isExecuting={this.props.copilotStore?.isExecuting}
queryResults={this.props.copilotStore?.queryResults}
databaseId={this.props.collection.databaseId}
containerId={this.props.collection.id()}
executeQueryDocumentsPage={(firstItemIndex: number) =>
QueryDocumentsPerPage(
firstItemIndex,
@@ -816,8 +795,6 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
errors={this.state.errors}
isExecuting={this.state.isExecuting}
queryResults={this.state.queryResults}
databaseId={this.props.collection.databaseId}
containerId={this.props.collection.id()}
executeQueryDocumentsPage={(firstItemIndex: number) =>
this._executeQueryDocumentsPage(firstItemIndex)
}

View File

@@ -1,170 +0,0 @@
import "@testing-library/jest-dom";
import { render, screen, waitFor } from "@testing-library/react";
import { IndexAdvisorTab } from "Explorer/Tabs/QueryTab/ResultsView";
import React from "react";
const mockReplace = jest.fn();
const mockFetchAll = jest.fn();
const mockRead = jest.fn();
const mockLogConsoleProgress = jest.fn();
const mockHandleError = jest.fn();
const indexMetricsResponse = {
UtilizedIndexes: {
SingleIndexes: [{ IndexSpec: "/foo/?", IndexImpactScore: "High" }],
CompositeIndexes: [{ IndexSpecs: ["/baz/? DESC", "/qux/? ASC"], IndexImpactScore: "Low" }],
},
PotentialIndexes: {
SingleIndexes: [{ IndexSpec: "/bar/?", IndexImpactScore: "Medium" }],
CompositeIndexes: [] as Array<{ IndexSpecs: string[]; IndexImpactScore?: string }>,
},
};
const mockQueryResults = {
documents: [] as unknown[],
hasMoreResults: false,
itemCount: 0,
firstItemIndex: 0,
lastItemIndex: 0,
requestCharge: 0,
activityId: "test-activity-id",
};
mockRead.mockResolvedValue({
resource: {
indexingPolicy: {
automatic: true,
indexingMode: "consistent",
includedPaths: [{ path: "/*" }, { path: "/foo/?" }],
excludedPaths: [],
},
partitionKey: "pk",
},
});
mockReplace.mockResolvedValue({
resource: {
indexingPolicy: {
automatic: true,
indexingMode: "consistent",
includedPaths: [{ path: "/*" }],
excludedPaths: [],
},
},
});
jest.mock("Common/CosmosClient", () => ({
client: () => ({
database: () => ({
container: () => ({
items: {
query: () => ({
fetchAll: mockFetchAll,
}),
},
read: mockRead,
replace: mockReplace,
}),
}),
}),
}));
jest.mock("./StylesAdvisor", () => ({
useIndexAdvisorStyles: () => ({}),
}));
jest.mock("../../../Utils/NotificationConsoleUtils", () => ({
logConsoleProgress: (...args: unknown[]) => {
mockLogConsoleProgress(...args);
return () => {};
},
}));
jest.mock("../../../Common/ErrorHandlingUtils", () => ({
handleError: (...args: unknown[]) => mockHandleError(...args),
}));
beforeEach(() => {
jest.clearAllMocks();
mockFetchAll.mockResolvedValue({ indexMetrics: indexMetricsResponse });
});
describe("IndexAdvisorTab Basic Tests", () => {
test("component renders without crashing", () => {
const { container } = render(
<IndexAdvisorTab queryEditorContent="SELECT * FROM c" databaseId="db1" containerId="col1" />,
);
expect(container).toBeTruthy();
});
test("renders component and handles missing parameters", () => {
const { container } = render(<IndexAdvisorTab />);
expect(container).toBeTruthy();
// Should not crash when parameters are missing
});
test("fetches index metrics with query results", async () => {
render(
<IndexAdvisorTab
queryResults={mockQueryResults}
queryEditorContent="SELECT * FROM c"
databaseId="db1"
containerId="col1"
/>,
);
await waitFor(() => expect(mockFetchAll).toHaveBeenCalled());
});
test("displays content after loading", async () => {
render(
<IndexAdvisorTab
queryResults={mockQueryResults}
queryEditorContent="SELECT * FROM c"
databaseId="db1"
containerId="col1"
/>,
);
// Wait for the component to finish loading
await waitFor(() => expect(mockFetchAll).toHaveBeenCalled());
// Component should have rendered some content
expect(screen.getByText(/Index Advisor/i)).toBeInTheDocument();
});
test("calls log console progress when fetching metrics", async () => {
render(
<IndexAdvisorTab
queryResults={mockQueryResults}
queryEditorContent="SELECT * FROM c"
databaseId="db1"
containerId="col1"
/>,
);
await waitFor(() => expect(mockLogConsoleProgress).toHaveBeenCalled());
});
test("handles error when fetch fails", async () => {
mockFetchAll.mockRejectedValueOnce(new Error("fetch failed"));
render(
<IndexAdvisorTab
queryResults={mockQueryResults}
queryEditorContent="SELECT * FROM c"
databaseId="db1"
containerId="col1"
/>,
);
await waitFor(() => expect(mockHandleError).toHaveBeenCalled(), { timeout: 3000 });
});
test("renders with all required props", () => {
const { container } = render(
<IndexAdvisorTab
queryResults={mockQueryResults}
queryEditorContent="SELECT * FROM c"
databaseId="testDb"
containerId="testContainer"
/>,
);
expect(container).toBeTruthy();
expect(container.firstChild).toBeTruthy();
});
});

View File

@@ -1,8 +1,5 @@
import type { CompositePath, IndexingPolicy } from "@azure/cosmos";
import { FontIcon } from "@fluentui/react";
import {
Button,
Checkbox,
DataGrid,
DataGridBody,
DataGridCell,
@@ -11,45 +8,28 @@ import {
DataGridRow,
SelectTabData,
SelectTabEvent,
Spinner,
Tab,
TabList,
Table,
TableBody,
TableCell,
TableColumnDefinition,
TableHeader,
TableRow,
createTableColumn,
} from "@fluentui/react-components";
import { ArrowDownloadRegular, ChevronDown20Regular, ChevronRight20Regular, CopyRegular } from "@fluentui/react-icons";
import copy from "clipboard-copy";
import { ArrowDownloadRegular, CopyRegular } from "@fluentui/react-icons";
import { HttpHeaders } from "Common/Constants";
import MongoUtility from "Common/MongoUtility";
import { QueryMetrics } from "Contracts/DataModels";
import { QueryResults } from "Contracts/ViewModels";
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
import {
parseIndexMetrics,
renderImpactDots,
type IndexMetricsResponse,
} from "Explorer/Tabs/QueryTab/IndexAdvisorUtils";
import { IDocument } from "Explorer/Tabs/QueryTab/QueryTabComponent";
import { useQueryTabStyles } from "Explorer/Tabs/QueryTab/Styles";
import React, { useCallback, useEffect, useState } from "react";
import { userContext } from "UserContext";
import { logConsoleProgress } from "Utils/NotificationConsoleUtils";
import create from "zustand";
import { client } from "../../../Common/CosmosClient";
import { handleError } from "../../../Common/ErrorHandlingUtils";
import { sampleDataClient } from "../../../Common/SampleDataClient";
import copy from "clipboard-copy";
import React, { useCallback, useState } from "react";
import { ResultsViewProps } from "./QueryResultSection";
import { useIndexAdvisorStyles } from "./StylesAdvisor";
enum ResultsTabs {
Results = "results",
QueryStats = "queryStats",
IndexAdvisor = "indexadv",
}
const ResultsTab: React.FC<ResultsViewProps> = ({ queryResults, isMongoDB, executeQueryDocumentsPage }) => {
const styles = useQueryTabStyles();
/* eslint-disable react/prop-types */
@@ -543,331 +523,14 @@ const QueryStatsTab: React.FC<Pick<ResultsViewProps, "queryResults">> = ({ query
);
};
export interface IIndexMetric {
index: string;
impact: string;
section: "Included" | "Not Included" | "Header";
path?: string;
composite?: { path: string; order: string }[];
}
export const IndexAdvisorTab: React.FC<{
queryResults?: QueryResults;
queryEditorContent?: string;
databaseId?: string;
containerId?: string;
}> = ({ queryResults, queryEditorContent, databaseId, containerId }) => {
const style = useIndexAdvisorStyles();
const [loading, setLoading] = useState(false);
const [indexMetrics, setIndexMetrics] = useState<IndexMetricsResponse | null>(null);
const [showIncluded, setShowIncluded] = useState(true);
const [showNotIncluded, setShowNotIncluded] = useState(true);
const [selectedIndexes, setSelectedIndexes] = useState<IIndexMetric[]>([]);
const [selectAll, setSelectAll] = useState(false);
const [updateMessageShown, setUpdateMessageShown] = useState(false);
const [included, setIncludedIndexes] = useState<IIndexMetric[]>([]);
const [notIncluded, setNotIncludedIndexes] = useState<IIndexMetric[]>([]);
const [isUpdating, setIsUpdating] = useState(false);
const [justUpdatedPolicy, setJustUpdatedPolicy] = useState(false);
const indexingMetricsDocLink = "https://learn.microsoft.com/azure/cosmos-db/nosql/index-metrics";
const fetchIndexMetrics = async () => {
if (!queryEditorContent || !databaseId || !containerId) {
return;
}
setLoading(true);
const clearMessage = logConsoleProgress(`Querying items with IndexMetrics in container ${containerId}`);
try {
const querySpec = {
query: queryEditorContent,
};
// Use sampleDataClient for CopilotSampleDB, regular client for other databases
const cosmosClient = databaseId === "CopilotSampleDB" ? sampleDataClient() : client();
const sdkResponse = await cosmosClient
.database(databaseId)
.container(containerId)
.items.query(querySpec, {
populateIndexMetrics: true,
})
.fetchAll();
const parsedMetrics =
typeof sdkResponse.indexMetrics === "string" ? JSON.parse(sdkResponse.indexMetrics) : sdkResponse.indexMetrics;
setIndexMetrics(parsedMetrics);
} catch (error) {
handleError(error, "queryItemsWithIndexMetrics", `Error querying items from ${containerId}`);
} finally {
clearMessage();
setLoading(false);
}
};
// Fetch index metrics when query results change (i.e., when Execute Query is clicked)
useEffect(() => {
if (queryEditorContent && databaseId && containerId && queryResults) {
fetchIndexMetrics();
}
}, [queryResults]);
useEffect(() => {
if (!indexMetrics) {
return;
}
const { included, notIncluded } = parseIndexMetrics(indexMetrics);
setIncludedIndexes(included);
setNotIncludedIndexes(notIncluded);
if (justUpdatedPolicy) {
setJustUpdatedPolicy(false);
} else {
setUpdateMessageShown(false);
}
}, [indexMetrics]);
useEffect(() => {
const allSelected =
notIncluded.length > 0 && notIncluded.every((item) => selectedIndexes.some((s) => s.index === item.index));
setSelectAll(allSelected);
}, [selectedIndexes, notIncluded]);
const handleCheckboxChange = (indexObj: IIndexMetric, checked: boolean) => {
if (checked) {
setSelectedIndexes((prev) => [...prev, indexObj]);
} else {
setSelectedIndexes((prev) => prev.filter((item) => item.index !== indexObj.index));
}
};
const handleSelectAll = (checked: boolean) => {
setSelectAll(checked);
setSelectedIndexes(checked ? notIncluded : []);
};
const handleUpdatePolicy = async () => {
setIsUpdating(true);
try {
const containerRef = client().database(databaseId).container(containerId);
const { resource: containerDef } = await containerRef.read();
const newIncludedPaths = selectedIndexes
.filter((index) => !index.composite)
.map((index) => {
return {
path: index.path,
};
});
const newCompositeIndexes: CompositePath[][] = selectedIndexes
.filter((index) => Array.isArray(index.composite))
.map(
(index) =>
(index.composite as { path: string; order: string }[]).map((comp) => ({
path: comp.path,
order: comp.order === "descending" ? "descending" : "ascending",
})) as CompositePath[],
);
const updatedPolicy: IndexingPolicy = {
...containerDef.indexingPolicy,
includedPaths: [...(containerDef.indexingPolicy?.includedPaths || []), ...newIncludedPaths],
compositeIndexes: [...(containerDef.indexingPolicy?.compositeIndexes || []), ...newCompositeIndexes],
automatic: containerDef.indexingPolicy?.automatic ?? true,
indexingMode: containerDef.indexingPolicy?.indexingMode ?? "consistent",
excludedPaths: containerDef.indexingPolicy?.excludedPaths ?? [],
};
await containerRef.replace({
id: containerId,
partitionKey: containerDef.partitionKey,
indexingPolicy: updatedPolicy,
});
useIndexingPolicyStore.getState().setIndexingPolicyFor(containerId, updatedPolicy);
const selectedIndexSet = new Set(selectedIndexes.map((s) => s.index));
const updatedNotIncluded: typeof notIncluded = [];
const newlyIncluded: typeof included = [];
for (const item of notIncluded) {
if (selectedIndexSet.has(item.index)) {
newlyIncluded.push(item);
} else {
updatedNotIncluded.push(item);
}
}
const newIncluded = [...included, ...newlyIncluded];
const newNotIncluded = updatedNotIncluded;
setIncludedIndexes(newIncluded);
setNotIncludedIndexes(newNotIncluded);
setSelectedIndexes([]);
setSelectAll(false);
setUpdateMessageShown(true);
setJustUpdatedPolicy(true);
} catch (err) {
console.error("Failed to update indexing policy:", err);
} finally {
setIsUpdating(false);
}
};
const renderRow = (item: IIndexMetric, index: number) => {
const isHeader = item.section === "Header";
const isNotIncluded = item.section === "Not Included";
return (
<TableRow key={index}>
<TableCell colSpan={2}>
<div className={style.indexAdvisorGrid}>
{isNotIncluded ? (
<Checkbox
checked={selectedIndexes.some((selected) => selected.index === item.index)}
onChange={(_, data) => handleCheckboxChange(item, data.checked === true)}
/>
) : isHeader && item.index === "Not Included in Current Policy" && notIncluded.length > 0 ? (
<Checkbox checked={selectAll} onChange={(_, data) => handleSelectAll(data.checked === true)} />
) : (
<div className={style.indexAdvisorCheckboxSpacer}></div>
)}
{isHeader ? (
<span
style={{ cursor: "pointer" }}
onClick={() => {
if (item.index === "Included in Current Policy") {
setShowIncluded(!showIncluded);
} else if (item.index === "Not Included in Current Policy") {
setShowNotIncluded(!showNotIncluded);
}
}}
>
{item.index === "Included in Current Policy" ? (
showIncluded ? (
<ChevronDown20Regular />
) : (
<ChevronRight20Regular />
)
) : showNotIncluded ? (
<ChevronDown20Regular />
) : (
<ChevronRight20Regular />
)}
</span>
) : (
<div className={style.indexAdvisorChevronSpacer}></div>
)}
<div className={isHeader ? style.indexAdvisorRowBold : style.indexAdvisorRowNormal}>{item.index}</div>
<div className={isHeader ? style.indexAdvisorRowImpactHeader : style.indexAdvisorRowImpact}>
{!isHeader && item.impact}
</div>
<div>{!isHeader && renderImpactDots(item.impact)}</div>
</div>
</TableCell>
</TableRow>
);
};
const indexMetricItems = React.useMemo(() => {
const items: IIndexMetric[] = [];
items.push({ index: "Not Included in Current Policy", impact: "", section: "Header" });
if (showNotIncluded) {
notIncluded.forEach((item) => items.push({ ...item, section: "Not Included" }));
}
items.push({ index: "Included in Current Policy", impact: "", section: "Header" });
if (showIncluded) {
included.forEach((item) => items.push({ ...item, section: "Included" }));
}
return items;
}, [included, notIncluded, showIncluded, showNotIncluded]);
if (loading) {
return (
<div>
<Spinner
size="small"
style={
{
"--spinner-size": "16px",
"--spinner-thickness": "2px",
"--spinner-color": "#0078D4",
} as React.CSSProperties
}
/>
</div>
);
}
return (
<div>
<div className={style.indexAdvisorMessage}>
{updateMessageShown ? (
<>
<span className={style.indexAdvisorSuccessIcon}>
<FontIcon iconName="CheckMark" style={{ color: "white", fontSize: 12 }} />
</span>
<span>
Your indexing policy has been updated with the new included paths. You may review the changes in Scale &
Settings.
</span>
</>
) : (
<>
<span>
Index Advisor uses Indexing Metrics to suggest query paths that, when included in your indexing policy,
can improve the performance of this query by reducing RU costs and lowering latency.{" "}
<a href={indexingMetricsDocLink} target="_blank" rel="noopener noreferrer">
Learn more about Indexing Metrics
</a>
.{" "}
</span>
</>
)}
</div>
<div className={style.indexAdvisorTitle}>Indexes analysis</div>
<Table className={style.indexAdvisorTable}>
<TableHeader>
<TableRow>
<TableCell colSpan={2}>
<div className={style.indexAdvisorGrid}>
<div className={style.indexAdvisorCheckboxSpacer}></div>
<div className={style.indexAdvisorChevronSpacer}></div>
<div>Index</div>
<div>
<span style={{ whiteSpace: "nowrap" }}>Estimated Impact</span>
</div>
</div>
</TableCell>
</TableRow>
</TableHeader>
<TableBody>{indexMetricItems.map(renderRow)}</TableBody>
</Table>
{selectedIndexes.length > 0 && (
<div className={style.indexAdvisorButtonBar}>
{isUpdating ? (
<div className={style.indexAdvisorButtonSpinner}>
<Spinner size="tiny" />{" "}
</div>
) : (
<button onClick={handleUpdatePolicy} className={style.indexAdvisorButton}>
Update Indexing Policy with selected index(es)
</button>
)}
</div>
)}
</div>
);
};
export const ResultsView: React.FC<ResultsViewProps> = ({
isMongoDB,
queryResults,
executeQueryDocumentsPage,
queryEditorContent,
databaseId,
containerId,
}) => {
export const ResultsView: React.FC<ResultsViewProps> = ({ isMongoDB, queryResults, executeQueryDocumentsPage }) => {
const styles = useQueryTabStyles();
const [activeTab, setActiveTab] = useState<ResultsTabs>(ResultsTabs.Results);
const onTabSelect = useCallback((event: SelectTabEvent, data: SelectTabData) => {
setActiveTab(data.value as ResultsTabs);
}, []);
return (
<div data-test="QueryTab/ResultsPane/ResultsView" className={styles.queryResultsTabPanel}>
<TabList selectedValue={activeTab} onTabSelect={onTabSelect}>
@@ -885,13 +548,6 @@ export const ResultsView: React.FC<ResultsViewProps> = ({
>
Query Stats
</Tab>
<Tab
data-test="QueryTab/ResultsPane/ResultsView/IndexAdvisorTab"
id={ResultsTabs.IndexAdvisor}
value={ResultsTabs.IndexAdvisor}
>
Index Advisor
</Tab>
</TabList>
<div className={styles.queryResultsTabContentContainer}>
{activeTab === ResultsTabs.Results && (
@@ -902,30 +558,7 @@ export const ResultsView: React.FC<ResultsViewProps> = ({
/>
)}
{activeTab === ResultsTabs.QueryStats && <QueryStatsTab queryResults={queryResults} />}
{activeTab === ResultsTabs.IndexAdvisor && (
<IndexAdvisorTab
queryResults={queryResults}
queryEditorContent={queryEditorContent}
databaseId={databaseId}
containerId={containerId}
/>
)}
</div>
</div>
);
};
export interface IndexingPolicyStore {
indexingPolicies: { [containerId: string]: IndexingPolicy };
setIndexingPolicyFor: (containerId: string, indexingPolicy: IndexingPolicy) => void;
}
export const useIndexingPolicyStore = create<IndexingPolicyStore>((set) => ({
indexingPolicies: {},
setIndexingPolicyFor: (containerId, indexingPolicy) =>
set((state) => ({
indexingPolicies: {
...state.indexingPolicies,
[containerId]: { ...indexingPolicy },
},
})),
}));

View File

@@ -1,95 +0,0 @@
import { makeStyles } from "@fluentui/react-components";
export type IndexAdvisorStyles = ReturnType<typeof useIndexAdvisorStyles>;
export const useIndexAdvisorStyles = makeStyles({
indexAdvisorMessage: {
padding: "1rem",
fontSize: "1.2rem",
display: "flex",
alignItems: "center",
gap: "0.5rem",
},
indexAdvisorSuccessIcon: {
width: "18px",
height: "18px",
borderRadius: "50%",
backgroundColor: "#107C10",
display: "flex",
alignItems: "center",
justifyContent: "center",
},
indexAdvisorTitle: {
padding: "1rem",
fontSize: "1.3rem",
fontWeight: "bold",
},
indexAdvisorTable: {
display: "block",
alignItems: "center",
marginBottom: "7rem",
},
indexAdvisorGrid: {
display: "grid",
gridTemplateColumns: "30px 30px 1fr 50px 120px",
alignItems: "center",
gap: "15px",
fontWeight: "bold",
},
indexAdvisorCheckboxSpacer: {
width: "18px",
height: "18px",
},
indexAdvisorChevronSpacer: {
width: "24px",
},
indexAdvisorRowBold: {
fontWeight: "bold",
},
indexAdvisorRowNormal: {
fontWeight: "normal",
},
indexAdvisorRowImpactHeader: {
fontSize: 0,
},
indexAdvisorRowImpact: {
fontWeight: "normal",
},
indexAdvisorImpactDot: {
color: "#0078D4",
fontSize: "12px",
display: "inline-flex",
},
indexAdvisorImpactDots: {
display: "flex",
alignItems: "center",
gap: "4px",
},
indexAdvisorButtonBar: {
padding: "1rem",
marginTop: "-7rem",
flexWrap: "wrap",
},
indexAdvisorButtonSpinner: {
marginTop: "1rem",
minWidth: "320px",
minHeight: "40px",
display: "flex",
alignItems: "left",
justifyContent: "left",
marginLeft: "10rem",
},
indexAdvisorButton: {
backgroundColor: "#0078D4",
color: "white",
padding: "8px 16px",
border: "none",
borderRadius: "4px",
cursor: "pointer",
marginTop: "1rem",
fontSize: "1rem",
fontWeight: 500,
transition: "background 0.2s",
":hover": {
backgroundColor: "#005a9e",
},
},
});

View File

@@ -1,15 +0,0 @@
import create from "zustand";
interface QueryMetadataStore {
userQuery: string;
databaseId: string;
containerId: string;
setMetadata: (query1: string, db: string, container: string) => void;
}
export const useQueryMetadataStore = create<QueryMetadataStore>((set) => ({
userQuery: "",
databaseId: "",
containerId: "",
setMetadata: (query1, db, container) => set({ userQuery: query1, databaseId: db, containerId: container }),
}));

View File

@@ -598,6 +598,9 @@ export default class Collection implements ViewModels.Collection {
public onSettingsClick = async (): Promise<void> => {
useSelectedNode.getState().setSelectedNode(this);
const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit;
// // Refresh all databases in case they were deleted outside of user session
await this.container.onRefreshResourcesClick();
throughputCap && throughputCap !== -1 ? await useDatabases.getState().loadAllOffers() : await this.loadOffer();
this.selectedSubnodeKind(ViewModels.CollectionTabKind.Settings);
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {

View File

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

View File

@@ -8,7 +8,8 @@ test("Cassandra keyspace and table CRUD", async ({ page }) => {
const explorer = await DataExplorer.open(page, TestAccount.Cassandra);
await explorer.globalCommandButton("New Table").click();
const newTableButton = await explorer.globalCommandButton("New Table");
await newTableButton.click();
await explorer.whilePanelOpen(
"Add Table",
async (panel, okButton) => {

View File

@@ -352,8 +352,9 @@ export class DataExplorer {
*
* There's only a single "primary" button, but we still require you to pass the label to confirm you're selecting the right button.
*/
globalCommandButton(label: string): Locator {
return this.frame.getByTestId("GlobalCommands").getByText(label);
async globalCommandButton(label: string): Promise<Locator> {
await this.frame.getByTestId("GlobalCommands").click();
return this.frame.getByRole("menuitem", { name: label });
}
/** Select the command bar button with the specified label */
@@ -459,6 +460,15 @@ export class DataExplorer {
const containerNode = await this.waitForContainerNode(context.database.id, context.container.id);
await containerNode.expand();
// // refresh tree to remove deleted database
// const consoleMessages = await this.getNotificationConsoleMessages();
// const refreshButton = this.frame.getByTestId("Sidebar/RefreshButton");
// await refreshButton.click();
// await expect(consoleMessages).toContainText("Successfully refreshed databases", {
// timeout: ONE_MINUTE_MS,
// });
// await this.collapseNotificationConsole();
const scaleAndSettingsButton = this.frame.getByTestId(
`TreeNode:${context.database.id}/${context.container.id}/Scale & Settings`,
);
@@ -466,10 +476,44 @@ export class DataExplorer {
}
/** Gets the console message element */
getConsoleMessage(): Locator {
getConsoleHeaderStatus(): Locator {
return this.frame.getByTestId("notification-console/header-status");
}
async expandNotificationConsole(): Promise<void> {
await this.setNotificationConsoleExpanded(true);
}
async collapseNotificationConsole(): Promise<void> {
await this.setNotificationConsoleExpanded(false);
}
async setNotificationConsoleExpanded(expanded: boolean): Promise<void> {
const notificationConsoleToggleButton = this.frame.getByTestId("NotificationConsole/ExpandCollapseButton");
const alt = await notificationConsoleToggleButton.locator("img").getAttribute("alt");
// When expanded, the icon says "Collapse icon"
if (expanded && alt === "Expand icon") {
await notificationConsoleToggleButton.click();
} else if (!expanded && alt === "Collapse icon") {
await notificationConsoleToggleButton.click();
}
}
async getNotificationConsoleMessages(): Promise<Locator> {
await this.setNotificationConsoleExpanded(true);
return this.frame.getByTestId("NotificationConsole/Contents");
}
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 */
static async waitForExplorer(page: Page) {
const iframeElement = await page.getByTestId("DataExplorerFrame").elementHandle();

View File

@@ -9,7 +9,8 @@ test("Gremlin graph CRUD", async ({ page }) => {
const explorer = await DataExplorer.open(page, TestAccount.Gremlin);
// Create new database and graph
await explorer.globalCommandButton("New Graph").click();
const newGraphButton = await explorer.globalCommandButton("New Graph");
await newGraphButton.click();
await explorer.whilePanelOpen(
"New Graph",
async (panel, okButton) => {

View File

@@ -14,7 +14,8 @@ import { DataExplorer, TEST_AUTOSCALE_THROUGHPUT_RU, TestAccount, generateUnique
const explorer = await DataExplorer.open(page, accountType);
await explorer.globalCommandButton("New Collection").click();
const newCollectionButton = await explorer.globalCommandButton("New Collection");
await newCollectionButton.click();
await explorer.whilePanelOpen(
"New Collection",
async (panel, okButton) => {

View File

@@ -8,7 +8,8 @@ test("SQL database and container CRUD", async ({ page }) => {
const explorer = await DataExplorer.open(page, TestAccount.SQL);
await explorer.globalCommandButton("New Container").click();
const newContainerButton = await explorer.globalCommandButton("New Container");
await newContainerButton.click();
await explorer.whilePanelOpen(
"New Container",
async (panel, okButton) => {

View File

@@ -1,148 +0,0 @@
import { expect, test, type Page } from "@playwright/test";
import { DataExplorer, TestAccount } from "../fx";
import { createTestSQLContainer, TestContainerContext } from "../testData";
// Test container context for setup and cleanup
let testContainer: TestContainerContext;
let DATABASE_ID: string;
let CONTAINER_ID: string;
// Set up test database and container with data before all tests
test.beforeAll(async () => {
testContainer = await createTestSQLContainer(true);
DATABASE_ID = testContainer.database.id;
CONTAINER_ID = testContainer.container.id;
});
// Clean up test database after all tests
test.afterAll(async () => {
if (testContainer) {
await testContainer.dispose();
}
});
// Helper function to set up query tab and navigate to Index Advisor
async function setupIndexAdvisorTab(page: Page, customQuery?: string) {
const explorer = await DataExplorer.open(page, TestAccount.SQL);
const databaseNode = await explorer.waitForNode(DATABASE_ID);
await databaseNode.expand();
await page.waitForTimeout(2000);
const containerNode = await explorer.waitForNode(`${DATABASE_ID}/${CONTAINER_ID}`);
await containerNode.openContextMenu();
await containerNode.contextMenuItem("New SQL Query").click();
await page.waitForTimeout(2000);
const queryTab = explorer.queryTab("tab0");
await queryTab.editor().locator.waitFor({ timeout: 30 * 1000 });
await queryTab.editor().locator.click();
if (customQuery) {
// Clear the default query and type the custom query
await page.keyboard.press("Control+A");
await page.waitForTimeout(100);
await page.keyboard.type(customQuery);
await page.waitForTimeout(500);
}
const executeQueryButton = explorer.commandBarButton("Execute Query");
await executeQueryButton.click();
await expect(queryTab.resultsEditor.locator).toBeAttached({ timeout: 60 * 1000 });
const indexAdvisorTab = queryTab.resultsView.getByTestId("QueryTab/ResultsPane/ResultsView/IndexAdvisorTab");
await indexAdvisorTab.click();
await page.waitForTimeout(2000);
return { explorer, queryTab, indexAdvisorTab };
}
test("Index Advisor tab loads without errors", async ({ page }) => {
const { indexAdvisorTab } = await setupIndexAdvisorTab(page);
await expect(indexAdvisorTab).toHaveAttribute("aria-selected", "true");
});
test("Verify UI sections are collapsible", async ({ page }) => {
const { explorer } = await setupIndexAdvisorTab(page);
// Verify both section headers exist
const includedHeader = explorer.frame.getByText("Included in Current Policy", { exact: true });
const notIncludedHeader = explorer.frame.getByText("Not Included in Current Policy", { exact: true });
await expect(includedHeader).toBeVisible();
await expect(notIncludedHeader).toBeVisible();
// Test collapsibility by checking if chevron/arrow icon changes state
// Both sections should be expandable/collapsible regardless of content
await includedHeader.click();
await page.waitForTimeout(300);
await includedHeader.click();
await page.waitForTimeout(300);
await notIncludedHeader.click();
await page.waitForTimeout(300);
await notIncludedHeader.click();
await page.waitForTimeout(300);
});
test("Verify SDK response structure - Case 1: Empty response", async ({ page }) => {
const { explorer } = await setupIndexAdvisorTab(page);
// Verify both section headers still exist even with no data
await expect(explorer.frame.getByText("Included in Current Policy", { exact: true })).toBeVisible();
await expect(explorer.frame.getByText("Not Included in Current Policy", { exact: true })).toBeVisible();
// Verify table headers
const table = explorer.frame.locator("table");
await expect(table.getByText("Index", { exact: true })).toBeVisible();
await expect(table.getByText("Estimated Impact", { exact: true })).toBeVisible();
// Verify "Update Indexing Policy" button is NOT visible when there are no potential indexes
const updateButton = explorer.frame.getByRole("button", { name: /Update Indexing Policy/i });
await expect(updateButton).not.toBeVisible();
});
test("Verify index suggestions and apply potential index", async ({ page }) => {
const customQuery = 'SELECT * FROM c WHERE c.partitionKey = "partition_1" ORDER BY c.randomData';
const { explorer } = await setupIndexAdvisorTab(page, customQuery);
// Wait for Index Advisor to process the query
await page.waitForTimeout(2000);
// Verify "Not Included in Current Policy" section has suggestions
const notIncludedHeader = explorer.frame.getByText("Not Included in Current Policy", { exact: true });
await expect(notIncludedHeader).toBeVisible();
// Find the checkbox for the suggested composite index
// The composite index should be /partitionKey ASC, /randomData ASC
const checkboxes = explorer.frame.locator('input[type="checkbox"]');
const checkboxCount = await checkboxes.count();
// Should have at least one checkbox for the potential index
expect(checkboxCount).toBeGreaterThan(0);
// Select the first checkbox (the high-impact composite index)
await checkboxes.first().check();
await page.waitForTimeout(500);
// Verify "Update Indexing Policy" button becomes visible
const updateButton = explorer.frame.getByRole("button", { name: /Update Indexing Policy/i });
await expect(updateButton).toBeVisible();
// Click the "Update Indexing Policy" button
await updateButton.click();
await page.waitForTimeout(1000);
// Verify success message appears
const successMessage = explorer.frame.getByText(/Your indexing policy has been updated with the new included paths/i);
await expect(successMessage).toBeVisible();
// Verify the message mentions reviewing changes in Scale & Settings
const reviewMessage = explorer.frame.getByText(/You may review the changes in Scale & Settings/i);
await expect(reviewMessage).toBeVisible();
// Verify the checkmark icon is shown
const checkmarkIcon = explorer.frame.locator('[data-icon-name="CheckMark"]');
await expect(checkmarkIcon).toBeVisible();
});

View File

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

View File

@@ -0,0 +1,102 @@
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 expect(PartitionKeyTab).toBeVisible();
await PartitionKeyTab.click();
});
if (!process.env.CI) {
test.afterEach("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: 5 * 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

@@ -9,39 +9,38 @@ import {
} from "../../fx";
import { createTestSQLContainer, TestContainerContext } from "../../testData";
test.describe("Autoscale and Manual throughput", () => {
test.describe("Autoscale 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 }) => {
test.beforeAll("Create Test Database", async ({ browser }) => {
context = await createTestSQLContainer();
const page = await browser.newPage();
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();
await switchManualToAutoscaleThroughput();
});
test.afterAll("Delete Test Database", async () => {
await context?.dispose();
});
if (!process.env.CI) {
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());
await getThroughputInput(explorer, "autopilot").fill(TEST_AUTOSCALE_MAX_THROUGHPUT_RU_2K.toString());
// Save
await explorer.commandBarButton(CommandBarButton.Save).click();
// Read console message
await expect(explorer.getConsoleMessage()).toContainText(
await expect(explorer.getConsoleHeaderStatus()).toContainText(
`Successfully updated offer for collection ${context.container.id}`,
{
timeout: 2 * ONE_MINUTE_MS,
@@ -50,9 +49,6 @@ test.describe("Autoscale and Manual throughput", () => {
});
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")
@@ -60,29 +56,65 @@ test.describe("Autoscale and Manual throughput", () => {
const softAllowedMaxThroughput = Number(softAllowedMaxThroughputString.replace(/,/g, ""));
// Try to set autoscale max throughput above allowed limit
await getThroughputInput("autopilot").fill((softAllowedMaxThroughput * 10).toString());
await getThroughputInput(explorer, "autopilot").fill((softAllowedMaxThroughput * 10).toString());
await expect(explorer.commandBarButton(CommandBarButton.Save)).toBeDisabled();
await expect(getThroughputInputErrorMessage("autopilot")).toContainText(
await expect(getThroughputInputErrorMessage(explorer, "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 getThroughputInput(explorer, "autopilot").fill("1100");
await expect(explorer.commandBarButton(CommandBarButton.Save)).toBeDisabled();
await expect(getThroughputInputErrorMessage("autopilot")).toContainText(
await expect(getThroughputInputErrorMessage(explorer, "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());
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(
await expect(explorer.getConsoleHeaderStatus()).toContainText(
`Successfully updated offer for collection ${context.container.id}`,
{
timeout: ONE_MINUTE_MS,
},
);
};
});
test.describe("Manual throughput", () => {
let context: TestContainerContext = null!;
let explorer: DataExplorer = null!;
test.beforeAll("Create Test Database & Open container settings", async ({ browser }) => {
context = await createTestSQLContainer();
const page = await browser.newPage();
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.beforeEach("Open container settings", async ({ page }) => {
// });
if (!process.env.CI) {
test.afterAll("Delete Test Database", async () => {
await context?.dispose();
});
}
test("Update manual throughput", async () => {
await getThroughputInput(explorer, "manual").fill(TEST_MANUAL_THROUGHPUT_RU_2K.toString());
await explorer.commandBarButton(CommandBarButton.Save).click();
await expect(explorer.getConsoleHeaderStatus()).toContainText(
`Successfully updated offer for collection ${context.container.id}`,
{
timeout: 2 * ONE_MINUTE_MS,
@@ -98,32 +130,19 @@ test.describe("Autoscale and Manual throughput", () => {
const softAllowedMaxThroughput = Number(softAllowedMaxThroughputString.replace(/,/g, ""));
// Try to set manual throughput above allowed limit
await getThroughputInput("manual").fill((softAllowedMaxThroughput * 10).toString());
await getThroughputInput(explorer, "manual").fill((softAllowedMaxThroughput * 10).toString());
await expect(explorer.commandBarButton(CommandBarButton.Save)).toBeDisabled();
await expect(getThroughputInputErrorMessage("manual")).toContainText(
await expect(getThroughputInputErrorMessage(explorer, "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,
},
);
};
});
// Helper methods
const getThroughputInput = (explorer: DataExplorer, type: "manual" | "autopilot"): Locator => {
return explorer.frame.getByTestId(`${type}-throughput-input`);
};
const getThroughputInputErrorMessage = (explorer: DataExplorer, type: "manual" | "autopilot"): Locator => {
return explorer.frame.getByTestId(`${type}-throughput-input-error`);
};

View File

@@ -2,18 +2,14 @@ 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", () => {
test.describe.serial("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 }) => {
test.beforeAll("Create Test Database", async ({ browser }) => {
context = await createTestSQLContainer();
const page = await browser.newPage();
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);
@@ -21,18 +17,35 @@ test.describe("Settings under Scale & Settings", () => {
await settingsTab.click();
});
test.afterAll("Delete Test Database", async () => {
await context?.dispose();
});
// 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 settingsTab = explorer.frame.getByTestId("settings-tab-header/SubSettingsTab");
// await settingsTab.click();
// });
// test.afterEach("Delete Test Database", async () => {
// await context?.dispose();
// });
if (!process.env.CI) {
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,
});
await expect(explorer.getConsoleHeaderStatus()).toContainText(
`Successfully updated container ${context.container.id}`,
{
timeout: ONE_MINUTE_MS,
},
);
});
test("Update TTL to On (with user entry)", async () => {
@@ -44,27 +57,11 @@ test.describe("Settings under Scale & Settings", () => {
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,
});
await expect(explorer.getConsoleHeaderStatus()).toContainText(
`Successfully updated container ${context.container.id}`,
{
timeout: ONE_MINUTE_MS,
},
);
});
});

View File

@@ -7,7 +7,8 @@ test("Tables CRUD", async ({ page }) => {
const explorer = await DataExplorer.open(page, TestAccount.Tables);
await explorer.globalCommandButton("New Table").click();
const newTableButton = explorer.frame.getByTestId("GlobalCommands").getByRole("button", { name: "New Table" });
await newTableButton.click();
await explorer.whilePanelOpen(
"New Table",
async (panel, okButton) => {

View File

@@ -74,8 +74,18 @@ export class TestContainerContext {
}
}
export async function createTestSQLContainer(includeTestData?: boolean) {
const databaseId = generateUniqueName("db");
type createTestSqlContainerConfig = {
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 credentials = getAzureCLICredentials();
const adaptedCredentials = new AzureIdentityCredentialAdapter(credentials);
@@ -104,7 +114,7 @@ export async function createTestSQLContainer(includeTestData?: boolean) {
try {
const { container } = await database.containers.createIfNotExists({
id: containerId,
partitionKey: "/partitionKey",
partitionKey,
});
if (includeTestData) {
const batchCount = TestData.length / 100;