mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-01-08 03:57:31 +00:00
Compare commits
10 Commits
refresh-ar
...
user/bchou
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd32f568e3 | ||
|
|
d35acaa132 | ||
|
|
4ac8cd8fe4 | ||
|
|
53288dec6f | ||
|
|
258a6286e7 | ||
|
|
c8ebca6da4 | ||
|
|
6167f94bc3 | ||
|
|
be89c634f3 | ||
|
|
42e230b88b | ||
|
|
6196ba4722 |
33
.github/workflows/ci.yml
vendored
33
.github/workflows/ci.yml
vendored
@@ -164,8 +164,8 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
shardIndex: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
|
shardIndex: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
|
||||||
shardTotal: [16]
|
shardTotal: [20]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Use Node.js 18.x
|
- name: Use Node.js 18.x
|
||||||
@@ -192,24 +192,29 @@ jobs:
|
|||||||
NOSQL_READONLY_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql-readonly.documents.azure.com/.default" -o tsv --query accessToken)
|
NOSQL_READONLY_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql-readonly.documents.azure.com/.default" -o tsv --query accessToken)
|
||||||
echo "::add-mask::$NOSQL_READONLY_TESTACCOUNT_TOKEN"
|
echo "::add-mask::$NOSQL_READONLY_TESTACCOUNT_TOKEN"
|
||||||
echo NOSQL_READONLY_TESTACCOUNT_TOKEN=$NOSQL_READONLY_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
echo NOSQL_READONLY_TESTACCOUNT_TOKEN=$NOSQL_READONLY_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
||||||
|
NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql-containercopyonly.documents.azure.com/.default" -o tsv --query accessToken)
|
||||||
|
echo "::add-mask::$NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN"
|
||||||
|
echo NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN=$NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
||||||
TABLE_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-tables.documents.azure.com/.default" -o tsv --query accessToken)
|
TABLE_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-tables.documents.azure.com/.default" -o tsv --query accessToken)
|
||||||
echo "::add-mask::$TABLE_TESTACCOUNT_TOKEN"
|
echo "::add-mask::$TABLE_TESTACCOUNT_TOKEN"
|
||||||
echo TABLE_TESTACCOUNT_TOKEN=$TABLE_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
echo TABLE_TESTACCOUNT_TOKEN=$TABLE_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
||||||
GREMLIN_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-gremlin.documents.azure.com/.default" -o tsv --query accessToken)
|
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 "::add-mask::$GREMLIN_TESTACCOUNT_TOKEN"
|
||||||
echo GREMLIN_TESTACCOUNT_TOKEN=$GREMLIN_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
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)
|
# 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 "::add-mask::$CASSANDRA_TESTACCOUNT_TOKEN"
|
||||||
echo CASSANDRA_TESTACCOUNT_TOKEN=$CASSANDRA_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
# 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)
|
# 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 "::add-mask::$MONGO_TESTACCOUNT_TOKEN"
|
||||||
echo MONGO_TESTACCOUNT_TOKEN=$MONGO_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
# 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)
|
# 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 "::add-mask::$MONGO32_TESTACCOUNT_TOKEN"
|
||||||
echo MONGO32_TESTACCOUNT_TOKEN=$MONGO32_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
# 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)
|
# 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 "::add-mask::$MONGO_READONLY_TESTACCOUNT_TOKEN"
|
||||||
echo MONGO_READONLY_TESTACCOUNT_TOKEN=$MONGO_READONLY_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
# echo MONGO_READONLY_TESTACCOUNT_TOKEN=$MONGO_READONLY_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
||||||
|
- name: List test files for shard ${{ matrix['shardIndex'] }} of ${{ matrix['shardTotal']}}
|
||||||
|
run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --list
|
||||||
- name: Run test shard ${{ matrix['shardIndex'] }} of ${{ matrix['shardTotal']}}
|
- name: Run test shard ${{ matrix['shardIndex'] }} of ${{ matrix['shardTotal']}}
|
||||||
run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --workers=3
|
run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --workers=3
|
||||||
- name: Upload blob report to GitHub Actions Artifacts
|
- name: Upload blob report to GitHub Actions Artifacts
|
||||||
|
|||||||
4
.github/workflows/cleanup.yml
vendored
4
.github/workflows/cleanup.yml
vendored
@@ -6,8 +6,8 @@ on:
|
|||||||
# Allows you to run this workflow manually from the Actions tab
|
# Allows you to run this workflow manually from the Actions tab
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
schedule:
|
schedule:
|
||||||
# Once every hour
|
# Once every two hours
|
||||||
- cron: "0 15 * * *"
|
- cron: "0 */2 * * *"
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
id-token: write
|
id-token: write
|
||||||
|
|||||||
@@ -128,7 +128,7 @@
|
|||||||
@provisionDatabaseThroughputInfo: 200px;
|
@provisionDatabaseThroughputInfo: 200px;
|
||||||
|
|
||||||
//tabs container
|
//tabs container
|
||||||
@ActiveTabHeight: 31px;
|
@ActiveTabHeight: 32px;
|
||||||
@ActiveTabWidth: 141px;
|
@ActiveTabWidth: 141px;
|
||||||
@TabsHeight: 30px;
|
@TabsHeight: 30px;
|
||||||
@TabsWidth: 140px;
|
@TabsWidth: 140px;
|
||||||
|
|||||||
@@ -2643,7 +2643,7 @@ a:link {
|
|||||||
|
|
||||||
.tabPanesContainer {
|
.tabPanesContainer {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
overflow-y: scroll;
|
overflow: hidden;
|
||||||
background-color: var(--colorNeutralBackground1);
|
background-color: var(--colorNeutralBackground1);
|
||||||
color: var(--colorNeutralForeground1);
|
color: var(--colorNeutralForeground1);
|
||||||
}
|
}
|
||||||
@@ -2651,6 +2651,7 @@ a:link {
|
|||||||
.tabs-container {
|
.tabs-container {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.paddingspan4 {
|
.paddingspan4 {
|
||||||
@@ -2677,7 +2678,7 @@ a:link {
|
|||||||
width: @ActiveTabWidth;
|
width: @ActiveTabWidth;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-tabs > li.active .contentWrapper {
|
.nav-tabs > li.active .contentWrapper .tabNavText {
|
||||||
border-bottom: 2px solid var(--colorCompoundBrandBackground);
|
border-bottom: 2px solid var(--colorCompoundBrandBackground);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ html {
|
|||||||
body {
|
body {
|
||||||
font-family: @FabricFont;
|
font-family: @FabricFont;
|
||||||
background-color: #f5f5f5;
|
background-color: #f5f5f5;
|
||||||
|
--colorCompoundBrandBackground: @FabricAccentMedium;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
@@ -41,7 +42,7 @@ a:focus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.nav-tabs-margin {
|
.nav-tabs-margin {
|
||||||
padding-top: 5px;
|
padding-top: 0px;
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,17 +69,20 @@ a:focus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.nav-tabs > li > .tabNavContentContainer > .tab_Content:hover {
|
.nav-tabs > li > .tabNavContentContainer > .tab_Content:hover {
|
||||||
border-bottom: 2px solid #e0e0e0;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-tabs > li.active > .tabNavContentContainer > .tab_Content,
|
.nav-tabs > li.active > .tabNavContentContainer > .tab_Content,
|
||||||
.nav-tabs > li.active > .tabNavContentContainer > .tab_Content:hover {
|
.nav-tabs > li.active > .tabNavContentContainer > .tab_Content:hover {
|
||||||
border-bottom: 2px solid @FabricAccentMedium;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-tabs > li.active > .tabNavContentContainer > .tab_Content > .contentWrapper > .tabNavText {
|
.nav-tabs > li.active > .tabNavContentContainer > .tab_Content > .contentWrapper > .tabNavText {
|
||||||
border-bottom: 0px none transparent;
|
border-bottom: 0px none transparent;
|
||||||
}
|
}
|
||||||
|
.nav-tabs > li.active .contentWrapper .tabNavText {
|
||||||
|
border-bottom: 2px solid @FabricAccentMedium;
|
||||||
|
}
|
||||||
|
|
||||||
.tabNavContentContainer {
|
.tabNavContentContainer {
|
||||||
padding: @SmallSpace 0px @SmallSpace 0px;
|
padding: @SmallSpace 0px @SmallSpace 0px;
|
||||||
@@ -214,6 +218,7 @@ a:focus {
|
|||||||
|
|
||||||
.tabPanesContainer {
|
.tabPanesContainer {
|
||||||
overflow: auto !important;
|
overflow: auto !important;
|
||||||
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabs-container {
|
.tabs-container {
|
||||||
|
|||||||
2
package-lock.json
generated
2
package-lock.json
generated
@@ -116,8 +116,8 @@
|
|||||||
"tinykeys": "2.1.0",
|
"tinykeys": "2.1.0",
|
||||||
"underscore": "1.12.1",
|
"underscore": "1.12.1",
|
||||||
"utility-types": "3.10.0",
|
"utility-types": "3.10.0",
|
||||||
"web-vitals": "4.2.4",
|
|
||||||
"uuid": "9.0.0",
|
"uuid": "9.0.0",
|
||||||
|
"web-vitals": "4.2.4",
|
||||||
"zustand": "3.5.0"
|
"zustand": "3.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ export default defineConfig({
|
|||||||
reporter: process.env.CI ? "blob" : "html",
|
reporter: process.env.CI ? "blob" : "html",
|
||||||
timeout: 10 * 60 * 1000,
|
timeout: 10 * 60 * 1000,
|
||||||
use: {
|
use: {
|
||||||
trace: "off",
|
trace: "retain-on-failure",
|
||||||
video: "off",
|
video: "retain-on-failure",
|
||||||
screenshot: "on",
|
screenshot: "on",
|
||||||
testIdAttribute: "data-test",
|
testIdAttribute: "data-test",
|
||||||
contextOptions: {
|
contextOptions: {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const LoadingOverlay: React.FC<LoadingOverlayProps> = ({ isLoading, label }) =>
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Overlay
|
<Overlay
|
||||||
|
data-test="loading-overlay"
|
||||||
styles={{
|
styles={{
|
||||||
root: {
|
root: {
|
||||||
backgroundColor: "rgba(255,255,255,0.9)",
|
backgroundColor: "rgba(255,255,255,0.9)",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
exports[`LoadingOverlay should handle long labels properly 1`] = `
|
exports[`LoadingOverlay should handle long labels properly 1`] = `
|
||||||
<div
|
<div
|
||||||
class="ms-Overlay root-109"
|
class="ms-Overlay root-109"
|
||||||
|
data-test="loading-overlay"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Spinner root-111"
|
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`] = `
|
exports[`LoadingOverlay should render loading overlay when isLoading is true 1`] = `
|
||||||
<div
|
<div
|
||||||
class="ms-Overlay root-109"
|
class="ms-Overlay root-109"
|
||||||
|
data-test="loading-overlay"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Spinner root-111"
|
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`] = `
|
exports[`LoadingOverlay should render loading overlay with custom label 1`] = `
|
||||||
<div
|
<div
|
||||||
class="ms-Overlay root-109"
|
class="ms-Overlay root-109"
|
||||||
|
data-test="loading-overlay"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Spinner root-111"
|
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`] = `
|
exports[`LoadingOverlay should render loading overlay with empty label 1`] = `
|
||||||
<div
|
<div
|
||||||
class="ms-Overlay root-109"
|
class="ms-Overlay root-109"
|
||||||
|
data-test="loading-overlay"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Spinner root-111"
|
class="ms-Spinner root-111"
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { configContext } from "ConfigContext";
|
||||||
import { ApiType, userContext } from "UserContext";
|
import { ApiType, userContext } from "UserContext";
|
||||||
import * as NotificationConsoleUtils from "Utils/NotificationConsoleUtils";
|
import * as NotificationConsoleUtils from "Utils/NotificationConsoleUtils";
|
||||||
import {
|
import {
|
||||||
@@ -14,9 +15,12 @@ import {
|
|||||||
DataTransferJobFeedResults,
|
DataTransferJobFeedResults,
|
||||||
DataTransferJobGetResults,
|
DataTransferJobGetResults,
|
||||||
} from "Utils/arm/generatedClients/dataTransferService/types";
|
} from "Utils/arm/generatedClients/dataTransferService/types";
|
||||||
|
import { armRequest } from "Utils/arm/request";
|
||||||
import { addToPolling, removeFromPolling, updateDataTransferJob, useDataTransferJobs } from "hooks/useDataTransferJobs";
|
import { addToPolling, removeFromPolling, updateDataTransferJob, useDataTransferJobs } from "hooks/useDataTransferJobs";
|
||||||
import promiseRetry, { AbortError, FailedAttemptError } from "p-retry";
|
import promiseRetry, { AbortError, FailedAttemptError } from "p-retry";
|
||||||
|
|
||||||
|
export const DATA_TRANSFER_JOB_API_VERSION = "2025-05-01-preview";
|
||||||
|
|
||||||
export interface DataTransferParams {
|
export interface DataTransferParams {
|
||||||
jobName: string;
|
jobName: string;
|
||||||
apiType: ApiType;
|
apiType: ApiType;
|
||||||
@@ -33,26 +37,34 @@ export const getDataTransferJobs = async (
|
|||||||
subscriptionId: string,
|
subscriptionId: string,
|
||||||
resourceGroup: string,
|
resourceGroup: string,
|
||||||
accountName: string,
|
accountName: string,
|
||||||
|
signal?: AbortSignal,
|
||||||
): Promise<DataTransferJobGetResults[]> => {
|
): Promise<DataTransferJobGetResults[]> => {
|
||||||
let dataTransferJobs: DataTransferJobGetResults[] = [];
|
let dataTransferJobs: DataTransferJobGetResults[] = [];
|
||||||
let dataTransferFeeds: DataTransferJobFeedResults = await listByDatabaseAccount(
|
let dataTransferFeeds: DataTransferJobFeedResults = await listByDatabaseAccount(
|
||||||
subscriptionId,
|
subscriptionId,
|
||||||
resourceGroup,
|
resourceGroup,
|
||||||
accountName,
|
accountName,
|
||||||
|
signal,
|
||||||
);
|
);
|
||||||
dataTransferJobs = [...dataTransferJobs, ...(dataTransferFeeds?.value || [])];
|
dataTransferJobs = [...dataTransferJobs, ...(dataTransferFeeds?.value || [])];
|
||||||
while (dataTransferFeeds?.nextLink) {
|
while (dataTransferFeeds?.nextLink) {
|
||||||
const nextResponse = await window.fetch(dataTransferFeeds.nextLink, {
|
/**
|
||||||
headers: {
|
* The `nextLink` URL returned by the Cosmos DB SQL API pointed to an incorrect endpoint, causing timeouts.
|
||||||
Authorization: userContext.authorizationToken,
|
* (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) {
|
dataTransferJobs.push(...(dataTransferFeeds?.value || []));
|
||||||
dataTransferFeeds = await nextResponse.json();
|
|
||||||
dataTransferJobs = [...dataTransferJobs, ...(dataTransferFeeds?.value || [])];
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return dataTransferJobs;
|
return dataTransferJobs;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ const AddManagedIdentity: React.FC<AddManagedIdentityProps> = () => {
|
|||||||
<InfoTooltip content={managedIdentityTooltip} />
|
<InfoTooltip content={managedIdentityTooltip} />
|
||||||
</Text>
|
</Text>
|
||||||
<Toggle
|
<Toggle
|
||||||
|
data-test="btn-toggle"
|
||||||
checked={systemAssigned}
|
checked={systemAssigned}
|
||||||
onText={ContainerCopyMessages.toggleBtn.onText}
|
onText={ContainerCopyMessages.toggleBtn.onText}
|
||||||
offText={ContainerCopyMessages.toggleBtn.offText}
|
offText={ContainerCopyMessages.toggleBtn.offText}
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ const AddReadPermissionToDefaultIdentity: React.FC<AddReadPermissionToDefaultIde
|
|||||||
<InfoTooltip content={TooltipContent} />
|
<InfoTooltip content={TooltipContent} />
|
||||||
</Text>
|
</Text>
|
||||||
<Toggle
|
<Toggle
|
||||||
|
data-test="btn-toggle"
|
||||||
checked={readPermissionAssigned}
|
checked={readPermissionAssigned}
|
||||||
onText={ContainerCopyMessages.toggleBtn.onText}
|
onText={ContainerCopyMessages.toggleBtn.onText}
|
||||||
offText={ContainerCopyMessages.toggleBtn.offText}
|
offText={ContainerCopyMessages.toggleBtn.offText}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { useCopyJobPrerequisitesCache } from "../../Utils/useCopyJobPrerequisite
|
|||||||
import usePermissionSections, { PermissionGroupConfig, PermissionSectionConfig } from "./hooks/usePermissionsSection";
|
import usePermissionSections, { PermissionGroupConfig, PermissionSectionConfig } from "./hooks/usePermissionsSection";
|
||||||
|
|
||||||
const PermissionSection: React.FC<PermissionSectionConfig> = ({ id, title, Component, completed, disabled }) => (
|
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">
|
<AccordionHeader className="accordionHeader">
|
||||||
<Text className="accordionHeaderText" variant="medium">
|
<Text className="accordionHeaderText" variant="medium">
|
||||||
{title}
|
{title}
|
||||||
@@ -25,13 +25,13 @@ const PermissionSection: React.FC<PermissionSectionConfig> = ({ id, title, Compo
|
|||||||
height={completed ? 20 : 24}
|
height={completed ? 20 : 24}
|
||||||
/>
|
/>
|
||||||
</AccordionHeader>
|
</AccordionHeader>
|
||||||
<AccordionPanel aria-disabled={disabled} className="accordionPanel">
|
<AccordionPanel aria-disabled={disabled} className="accordionPanel" data-test="accordion-panel">
|
||||||
<Component />
|
<Component />
|
||||||
</AccordionPanel>
|
</AccordionPanel>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
);
|
);
|
||||||
|
|
||||||
const PermissionGroup: React.FC<PermissionGroupConfig> = ({ title, description, sections }) => {
|
const PermissionGroup: React.FC<PermissionGroupConfig> = ({ id, title, description, sections }) => {
|
||||||
const [openItems, setOpenItems] = React.useState<string[]>([]);
|
const [openItems, setOpenItems] = React.useState<string[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -44,6 +44,7 @@ const PermissionGroup: React.FC<PermissionGroupConfig> = ({ title, description,
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack
|
<Stack
|
||||||
|
data-test={`permission-group-container-${id}`}
|
||||||
tokens={{ childrenGap: 15 }}
|
tokens={{ childrenGap: 15 }}
|
||||||
styles={{
|
styles={{
|
||||||
root: {
|
root: {
|
||||||
@@ -99,7 +100,11 @@ const AssignPermissions = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack className="assignPermissionsContainer" tokens={{ childrenGap: 20 }}>
|
<Stack
|
||||||
|
data-test="Panel:AssignPermissionsContainer"
|
||||||
|
className="assignPermissionsContainer"
|
||||||
|
tokens={{ childrenGap: 20 }}
|
||||||
|
>
|
||||||
<Text variant="medium">
|
<Text variant="medium">
|
||||||
{isSameAccount && copyJobState.migrationType === CopyJobMigrationType.Online
|
{isSameAccount && copyJobState.migrationType === CopyJobMigrationType.Online
|
||||||
? ContainerCopyMessages.assignPermissions.intraAccountOnlineDescription(
|
? ContainerCopyMessages.assignPermissions.intraAccountOnlineDescription(
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ const DefaultManagedIdentity: React.FC<AddManagedIdentityProps> = () => {
|
|||||||
<InfoTooltip content={managedIdentityTooltip} />
|
<InfoTooltip content={managedIdentityTooltip} />
|
||||||
</div>
|
</div>
|
||||||
<Toggle
|
<Toggle
|
||||||
|
data-test="btn-toggle"
|
||||||
checked={defaultSystemAssigned}
|
checked={defaultSystemAssigned}
|
||||||
onText={ContainerCopyMessages.toggleBtn.onText}
|
onText={ContainerCopyMessages.toggleBtn.onText}
|
||||||
offText={ContainerCopyMessages.toggleBtn.offText}
|
offText={ContainerCopyMessages.toggleBtn.offText}
|
||||||
|
|||||||
@@ -127,6 +127,7 @@ const PointInTimeRestore: React.FC = () => {
|
|||||||
<Stack.Item>
|
<Stack.Item>
|
||||||
{showRefreshButton ? (
|
{showRefreshButton ? (
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
|
data-test="pointInTimeRestore:RefreshBtn"
|
||||||
className="fullWidth"
|
className="fullWidth"
|
||||||
text={ContainerCopyMessages.refreshButtonLabel}
|
text={ContainerCopyMessages.refreshButtonLabel}
|
||||||
iconProps={{ iconName: "Refresh" }}
|
iconProps={{ iconName: "Refresh" }}
|
||||||
@@ -134,6 +135,7 @@ const PointInTimeRestore: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
|
data-test="pointInTimeRestore:PrimaryBtn"
|
||||||
className="fullWidth"
|
className="fullWidth"
|
||||||
text={loading ? "" : ContainerCopyMessages.pointInTimeRestore.buttonText}
|
text={loading ? "" : ContainerCopyMessages.pointInTimeRestore.buttonText}
|
||||||
{...(loading ? { iconProps: { iconName: "SyncStatusSolid" } } : {})}
|
{...(loading ? { iconProps: { iconName: "SyncStatusSolid" } } : {})}
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ exports[`AddManagedIdentity Snapshot Tests renders initial state correctly 1`] =
|
|||||||
class="ms-Toggle-background pill-117"
|
class="ms-Toggle-background pill-117"
|
||||||
data-is-focusable="true"
|
data-is-focusable="true"
|
||||||
data-ktp-target="true"
|
data-ktp-target="true"
|
||||||
|
data-test="btn-toggle"
|
||||||
id="Toggle1"
|
id="Toggle1"
|
||||||
role="switch"
|
role="switch"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -154,6 +155,7 @@ exports[`AddManagedIdentity Snapshot Tests renders loading state 1`] = `
|
|||||||
class="ms-Toggle-background pill-121"
|
class="ms-Toggle-background pill-121"
|
||||||
data-is-focusable="true"
|
data-is-focusable="true"
|
||||||
data-ktp-target="true"
|
data-ktp-target="true"
|
||||||
|
data-test="btn-toggle"
|
||||||
id="Toggle11"
|
id="Toggle11"
|
||||||
role="switch"
|
role="switch"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -173,10 +175,12 @@ exports[`AddManagedIdentity Snapshot Tests renders loading state 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack popover-container foreground loading css-123"
|
class="ms-Stack popover-container foreground loading css-123"
|
||||||
|
data-test="popover-container"
|
||||||
style="max-width: 450px;"
|
style="max-width: 450px;"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Overlay root-135"
|
class="ms-Overlay root-135"
|
||||||
|
data-test="loading-overlay"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Spinner root-137"
|
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"
|
class="ms-Toggle-background pill-121"
|
||||||
data-is-focusable="true"
|
data-is-focusable="true"
|
||||||
data-ktp-target="true"
|
data-ktp-target="true"
|
||||||
|
data-test="btn-toggle"
|
||||||
id="Toggle3"
|
id="Toggle3"
|
||||||
role="switch"
|
role="switch"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -342,6 +347,7 @@ exports[`AddManagedIdentity Snapshot Tests renders with toggle on and popover vi
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack popover-container foreground css-123"
|
class="ms-Stack popover-container foreground css-123"
|
||||||
|
data-test="popover-container"
|
||||||
style="max-width: 450px;"
|
style="max-width: 450px;"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Edge Cases should handle m
|
|||||||
class="ms-Toggle-background pill-115"
|
class="ms-Toggle-background pill-115"
|
||||||
data-is-focusable="true"
|
data-is-focusable="true"
|
||||||
data-ktp-target="true"
|
data-ktp-target="true"
|
||||||
|
data-test="btn-toggle"
|
||||||
id="Toggle17"
|
id="Toggle17"
|
||||||
role="switch"
|
role="switch"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -103,6 +104,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Edge Cases should handle m
|
|||||||
class="ms-Toggle-background pill-115"
|
class="ms-Toggle-background pill-115"
|
||||||
data-is-focusable="true"
|
data-is-focusable="true"
|
||||||
data-ktp-target="true"
|
data-ktp-target="true"
|
||||||
|
data-test="btn-toggle"
|
||||||
id="Toggle16"
|
id="Toggle16"
|
||||||
role="switch"
|
role="switch"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -165,6 +167,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
|
|||||||
class="ms-Toggle-background pill-115"
|
class="ms-Toggle-background pill-115"
|
||||||
data-is-focusable="true"
|
data-is-focusable="true"
|
||||||
data-ktp-target="true"
|
data-ktp-target="true"
|
||||||
|
data-test="btn-toggle"
|
||||||
id="Toggle3"
|
id="Toggle3"
|
||||||
role="switch"
|
role="switch"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -227,6 +230,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
|
|||||||
class="ms-Toggle-background pill-119"
|
class="ms-Toggle-background pill-119"
|
||||||
data-is-focusable="true"
|
data-is-focusable="true"
|
||||||
data-ktp-target="true"
|
data-ktp-target="true"
|
||||||
|
data-test="btn-toggle"
|
||||||
id="Toggle1"
|
id="Toggle1"
|
||||||
role="switch"
|
role="switch"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -314,6 +318,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
|
|||||||
class="ms-Toggle-background pill-115"
|
class="ms-Toggle-background pill-115"
|
||||||
data-is-focusable="true"
|
data-is-focusable="true"
|
||||||
data-ktp-target="true"
|
data-ktp-target="true"
|
||||||
|
data-test="btn-toggle"
|
||||||
id="Toggle0"
|
id="Toggle0"
|
||||||
role="switch"
|
role="switch"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -376,6 +381,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
|
|||||||
class="ms-Toggle-background pill-115"
|
class="ms-Toggle-background pill-115"
|
||||||
data-is-focusable="true"
|
data-is-focusable="true"
|
||||||
data-ktp-target="true"
|
data-ktp-target="true"
|
||||||
|
data-test="btn-toggle"
|
||||||
id="Toggle2"
|
id="Toggle2"
|
||||||
role="switch"
|
role="switch"
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ exports[`AssignPermissions Component Accordion Behavior should render accordion
|
|||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack assignPermissionsContainer css-109"
|
class="ms-Stack assignPermissionsContainer css-109"
|
||||||
|
data-test="Panel:AssignPermissionsContainer"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="css-110"
|
class="css-110"
|
||||||
@@ -15,6 +16,7 @@ exports[`AssignPermissions Component Accordion Behavior should render accordion
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack css-112"
|
class="ms-Stack css-112"
|
||||||
|
data-test="permission-group-container-testGroup"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack css-113"
|
class="ms-Stack css-113"
|
||||||
@@ -36,6 +38,7 @@ exports[`AssignPermissions Component Accordion Behavior should render accordion
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionItem"
|
class="fui-AccordionItem"
|
||||||
|
data-test="accordion-item"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
||||||
@@ -85,6 +88,7 @@ exports[`AssignPermissions Component Accordion Behavior should render accordion
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionItem"
|
class="fui-AccordionItem"
|
||||||
|
data-test="accordion-item"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
||||||
@@ -134,6 +138,7 @@ exports[`AssignPermissions Component Accordion Behavior should render accordion
|
|||||||
<div
|
<div
|
||||||
aria-disabled="false"
|
aria-disabled="false"
|
||||||
class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu"
|
class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu"
|
||||||
|
data-test="accordion-panel"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
Incomplete Component
|
Incomplete Component
|
||||||
@@ -142,6 +147,7 @@ exports[`AssignPermissions Component Accordion Behavior should render accordion
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionItem"
|
class="fui-AccordionItem"
|
||||||
|
data-test="accordion-item"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionHeader accordionHeader ___lyghz50_53x5ri0 f1s2aq7o f1c21dwh f1s184ao ft85np5 fwrgwhw"
|
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>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack assignPermissionsContainer css-109"
|
class="ms-Stack assignPermissionsContainer css-109"
|
||||||
|
data-test="Panel:AssignPermissionsContainer"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="css-110"
|
class="css-110"
|
||||||
@@ -212,6 +219,7 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack css-112"
|
class="ms-Stack css-112"
|
||||||
|
data-test="permission-group-container-testGroup"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack css-113"
|
class="ms-Stack css-113"
|
||||||
@@ -233,6 +241,7 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionItem"
|
class="fui-AccordionItem"
|
||||||
|
data-test="accordion-item"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
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>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionItem"
|
class="fui-AccordionItem"
|
||||||
|
data-test="accordion-item"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
||||||
@@ -331,6 +341,7 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
|
|||||||
<div
|
<div
|
||||||
aria-disabled="false"
|
aria-disabled="false"
|
||||||
class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu"
|
class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu"
|
||||||
|
data-test="accordion-panel"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
Incomplete Component
|
Incomplete Component
|
||||||
@@ -339,6 +350,7 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionItem"
|
class="fui-AccordionItem"
|
||||||
|
data-test="accordion-item"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionHeader accordionHeader ___lyghz50_53x5ri0 f1s2aq7o f1c21dwh f1s184ao ft85np5 fwrgwhw"
|
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>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack assignPermissionsContainer css-109"
|
class="ms-Stack assignPermissionsContainer css-109"
|
||||||
|
data-test="Panel:AssignPermissionsContainer"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="css-110"
|
class="css-110"
|
||||||
@@ -409,6 +422,7 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack css-112"
|
class="ms-Stack css-112"
|
||||||
|
data-test="permission-group-container-testGroup"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack css-113"
|
class="ms-Stack css-113"
|
||||||
@@ -430,6 +444,7 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionItem"
|
class="fui-AccordionItem"
|
||||||
|
data-test="accordion-item"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
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>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionItem"
|
class="fui-AccordionItem"
|
||||||
|
data-test="accordion-item"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
||||||
@@ -528,6 +544,7 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
|
|||||||
<div
|
<div
|
||||||
aria-disabled="false"
|
aria-disabled="false"
|
||||||
class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu"
|
class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu"
|
||||||
|
data-test="accordion-panel"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
Incomplete Component
|
Incomplete Component
|
||||||
@@ -536,6 +553,7 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionItem"
|
class="fui-AccordionItem"
|
||||||
|
data-test="accordion-item"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionHeader accordionHeader ___lyghz50_53x5ri0 f1s2aq7o f1c21dwh f1s184ao ft85np5 fwrgwhw"
|
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>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack assignPermissionsContainer css-109"
|
class="ms-Stack assignPermissionsContainer css-109"
|
||||||
|
data-test="Panel:AssignPermissionsContainer"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="css-110"
|
class="css-110"
|
||||||
@@ -606,6 +625,7 @@ exports[`AssignPermissions Component Edge Cases should handle missing account na
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack css-112"
|
class="ms-Stack css-112"
|
||||||
|
data-test="permission-group-container-testGroup"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack css-113"
|
class="ms-Stack css-113"
|
||||||
@@ -627,6 +647,7 @@ exports[`AssignPermissions Component Edge Cases should handle missing account na
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionItem"
|
class="fui-AccordionItem"
|
||||||
|
data-test="accordion-item"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
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>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionItem"
|
class="fui-AccordionItem"
|
||||||
|
data-test="accordion-item"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
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
|
<div
|
||||||
aria-disabled="false"
|
aria-disabled="false"
|
||||||
class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu"
|
class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu"
|
||||||
|
data-test="accordion-panel"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
Incomplete Component
|
Incomplete Component
|
||||||
@@ -733,6 +756,7 @@ exports[`AssignPermissions Component Edge Cases should handle missing account na
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionItem"
|
class="fui-AccordionItem"
|
||||||
|
data-test="accordion-item"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionHeader accordionHeader ___lyghz50_53x5ri0 f1s2aq7o f1c21dwh f1s184ao ft85np5 fwrgwhw"
|
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>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack assignPermissionsContainer css-109"
|
class="ms-Stack assignPermissionsContainer css-109"
|
||||||
|
data-test="Panel:AssignPermissionsContainer"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="css-110"
|
class="css-110"
|
||||||
@@ -803,6 +828,7 @@ exports[`AssignPermissions Component Permission Groups should render multiple pe
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack css-112"
|
class="ms-Stack css-112"
|
||||||
|
data-test="permission-group-container-crossAccountConfigs"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack css-113"
|
class="ms-Stack css-113"
|
||||||
@@ -824,6 +850,7 @@ exports[`AssignPermissions Component Permission Groups should render multiple pe
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionItem"
|
class="fui-AccordionItem"
|
||||||
|
data-test="accordion-item"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
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>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack css-112"
|
class="ms-Stack css-112"
|
||||||
|
data-test="permission-group-container-onlineConfigs"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack css-113"
|
class="ms-Stack css-113"
|
||||||
@@ -896,6 +924,7 @@ exports[`AssignPermissions Component Permission Groups should render multiple pe
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionItem"
|
class="fui-AccordionItem"
|
||||||
|
data-test="accordion-item"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
||||||
@@ -945,6 +974,7 @@ exports[`AssignPermissions Component Permission Groups should render multiple pe
|
|||||||
<div
|
<div
|
||||||
aria-disabled="false"
|
aria-disabled="false"
|
||||||
class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu"
|
class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu"
|
||||||
|
data-test="accordion-panel"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
data-testid="online-copy-enabled"
|
data-testid="online-copy-enabled"
|
||||||
@@ -964,6 +994,7 @@ exports[`AssignPermissions Component Permission Groups should render online migr
|
|||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack assignPermissionsContainer css-109"
|
class="ms-Stack assignPermissionsContainer css-109"
|
||||||
|
data-test="Panel:AssignPermissionsContainer"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="css-110"
|
class="css-110"
|
||||||
@@ -975,6 +1006,7 @@ exports[`AssignPermissions Component Permission Groups should render online migr
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack css-112"
|
class="ms-Stack css-112"
|
||||||
|
data-test="permission-group-container-onlineConfigs"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack css-113"
|
class="ms-Stack css-113"
|
||||||
@@ -996,6 +1028,7 @@ exports[`AssignPermissions Component Permission Groups should render online migr
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionItem"
|
class="fui-AccordionItem"
|
||||||
|
data-test="accordion-item"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
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>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionItem"
|
class="fui-AccordionItem"
|
||||||
|
data-test="accordion-item"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
||||||
@@ -1094,6 +1128,7 @@ exports[`AssignPermissions Component Permission Groups should render online migr
|
|||||||
<div
|
<div
|
||||||
aria-disabled="false"
|
aria-disabled="false"
|
||||||
class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu"
|
class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu"
|
||||||
|
data-test="accordion-panel"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
data-testid="online-copy-enabled"
|
data-testid="online-copy-enabled"
|
||||||
@@ -1113,6 +1148,7 @@ exports[`AssignPermissions Component Permission Groups should render permission
|
|||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack assignPermissionsContainer css-109"
|
class="ms-Stack assignPermissionsContainer css-109"
|
||||||
|
data-test="Panel:AssignPermissionsContainer"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="css-110"
|
class="css-110"
|
||||||
@@ -1124,6 +1160,7 @@ exports[`AssignPermissions Component Permission Groups should render permission
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack css-112"
|
class="ms-Stack css-112"
|
||||||
|
data-test="permission-group-container-crossAccountConfigs"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack css-113"
|
class="ms-Stack css-113"
|
||||||
@@ -1145,6 +1182,7 @@ exports[`AssignPermissions Component Permission Groups should render permission
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionItem"
|
class="fui-AccordionItem"
|
||||||
|
data-test="accordion-item"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
||||||
@@ -1194,6 +1232,7 @@ exports[`AssignPermissions Component Permission Groups should render permission
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionItem"
|
class="fui-AccordionItem"
|
||||||
|
data-test="accordion-item"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
||||||
@@ -1243,6 +1282,7 @@ exports[`AssignPermissions Component Permission Groups should render permission
|
|||||||
<div
|
<div
|
||||||
aria-disabled="false"
|
aria-disabled="false"
|
||||||
class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu"
|
class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu"
|
||||||
|
data-test="accordion-panel"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
data-testid="add-read-permission"
|
data-testid="add-read-permission"
|
||||||
@@ -1262,6 +1302,7 @@ exports[`AssignPermissions Component Rendering should render without crashing wi
|
|||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack assignPermissionsContainer css-109"
|
class="ms-Stack assignPermissionsContainer css-109"
|
||||||
|
data-test="Panel:AssignPermissionsContainer"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="css-110"
|
class="css-110"
|
||||||
@@ -1283,6 +1324,7 @@ exports[`AssignPermissions Component Rendering should render without crashing wi
|
|||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack assignPermissionsContainer css-109"
|
class="ms-Stack assignPermissionsContainer css-109"
|
||||||
|
data-test="Panel:AssignPermissionsContainer"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="css-110"
|
class="css-110"
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ exports[`DefaultManagedIdentity Edge Cases should handle missing account name gr
|
|||||||
class="ms-Toggle-background pill-115"
|
class="ms-Toggle-background pill-115"
|
||||||
data-is-focusable="true"
|
data-is-focusable="true"
|
||||||
data-ktp-target="true"
|
data-ktp-target="true"
|
||||||
|
data-test="btn-toggle"
|
||||||
id="Toggle14"
|
id="Toggle14"
|
||||||
role="switch"
|
role="switch"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -103,6 +104,7 @@ exports[`DefaultManagedIdentity Edge Cases should handle null account 1`] = `
|
|||||||
class="ms-Toggle-background pill-115"
|
class="ms-Toggle-background pill-115"
|
||||||
data-is-focusable="true"
|
data-is-focusable="true"
|
||||||
data-ktp-target="true"
|
data-ktp-target="true"
|
||||||
|
data-test="btn-toggle"
|
||||||
id="Toggle15"
|
id="Toggle15"
|
||||||
role="switch"
|
role="switch"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -165,6 +167,7 @@ exports[`DefaultManagedIdentity Loading States should render loading state snaps
|
|||||||
class="ms-Toggle-background pill-119"
|
class="ms-Toggle-background pill-119"
|
||||||
data-is-focusable="true"
|
data-is-focusable="true"
|
||||||
data-ktp-target="true"
|
data-ktp-target="true"
|
||||||
|
data-test="btn-toggle"
|
||||||
id="Toggle10"
|
id="Toggle10"
|
||||||
role="switch"
|
role="switch"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -256,6 +259,7 @@ exports[`DefaultManagedIdentity Rendering should render correctly with default s
|
|||||||
class="ms-Toggle-background pill-115"
|
class="ms-Toggle-background pill-115"
|
||||||
data-is-focusable="true"
|
data-is-focusable="true"
|
||||||
data-ktp-target="true"
|
data-ktp-target="true"
|
||||||
|
data-test="btn-toggle"
|
||||||
id="Toggle0"
|
id="Toggle0"
|
||||||
role="switch"
|
role="switch"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -318,6 +322,7 @@ exports[`DefaultManagedIdentity Toggle Interactions should render toggle with ch
|
|||||||
class="ms-Toggle-background pill-119"
|
class="ms-Toggle-background pill-119"
|
||||||
data-is-focusable="true"
|
data-is-focusable="true"
|
||||||
data-ktp-target="true"
|
data-ktp-target="true"
|
||||||
|
data-test="btn-toggle"
|
||||||
id="Toggle7"
|
id="Toggle7"
|
||||||
role="switch"
|
role="switch"
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ exports[`PointInTimeRestore Initial Render should render correctly with default
|
|||||||
<button
|
<button
|
||||||
class="ms-Button ms-Button--primary fullWidth root-115"
|
class="ms-Button ms-Button--primary fullWidth root-115"
|
||||||
data-is-focusable="true"
|
data-is-focusable="true"
|
||||||
|
data-test="pointInTimeRestore:PrimaryBtn"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@@ -131,6 +132,7 @@ exports[`PointInTimeRestore Initial Render should render with empty account name
|
|||||||
<button
|
<button
|
||||||
class="ms-Button ms-Button--primary fullWidth root-115"
|
class="ms-Button ms-Button--primary fullWidth root-115"
|
||||||
data-is-focusable="true"
|
data-is-focusable="true"
|
||||||
|
data-test="pointInTimeRestore:PrimaryBtn"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@@ -161,6 +163,7 @@ exports[`PointInTimeRestore Snapshots should match snapshot in loading state 1`]
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Overlay root-123"
|
class="ms-Overlay root-123"
|
||||||
|
data-test="loading-overlay"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Spinner root-125"
|
class="ms-Spinner root-125"
|
||||||
@@ -223,6 +226,7 @@ exports[`PointInTimeRestore Snapshots should match snapshot in loading state 1`]
|
|||||||
aria-disabled="true"
|
aria-disabled="true"
|
||||||
class="ms-Button ms-Button--primary is-disabled fullWidth root-128"
|
class="ms-Button ms-Button--primary is-disabled fullWidth root-128"
|
||||||
data-is-focusable="false"
|
data-is-focusable="false"
|
||||||
|
data-test="pointInTimeRestore:PrimaryBtn"
|
||||||
disabled=""
|
disabled=""
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
@@ -301,6 +305,7 @@ exports[`PointInTimeRestore Snapshots should match snapshot with refresh button
|
|||||||
<button
|
<button
|
||||||
class="ms-Button ms-Button--primary fullWidth root-115"
|
class="ms-Button ms-Button--primary fullWidth root-115"
|
||||||
data-is-focusable="true"
|
data-is-focusable="true"
|
||||||
|
data-test="pointInTimeRestore:RefreshBtn"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
|
|||||||
@@ -19,9 +19,21 @@ const NavigationControls: React.FC<NavigationControlsProps> = ({
|
|||||||
isPreviousDisabled,
|
isPreviousDisabled,
|
||||||
}) => (
|
}) => (
|
||||||
<Stack horizontal tokens={{ childrenGap: 20 }}>
|
<Stack horizontal tokens={{ childrenGap: 20 }}>
|
||||||
<PrimaryButton text={primaryBtnText} onClick={onPrimary} allowDisabledFocus disabled={isPrimaryDisabled} />
|
<PrimaryButton
|
||||||
<DefaultButton text="Previous" onClick={onPrevious} allowDisabledFocus disabled={isPreviousDisabled} />
|
data-test="copy-job-primary"
|
||||||
<DefaultButton text="Cancel" onClick={onCancel} />
|
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>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ const PopoverContainer: React.FC<PopoverContainerProps> = React.memo(
|
|||||||
({ isLoading = false, title, children, onPrimary, onCancel }) => {
|
({ isLoading = false, title, children, onPrimary, onCancel }) => {
|
||||||
return (
|
return (
|
||||||
<Stack
|
<Stack
|
||||||
|
data-test="popover-container"
|
||||||
className={`popover-container foreground ${isLoading ? "loading" : ""}`}
|
className={`popover-container foreground ${isLoading ? "loading" : ""}`}
|
||||||
tokens={{ childrenGap: 20 }}
|
tokens={{ childrenGap: 20 }}
|
||||||
style={{ maxWidth: 450 }}
|
style={{ maxWidth: 450 }}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ exports[`PopoverMessage Component Edge Cases should handle empty string title 1`
|
|||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack popover-container foreground css-109"
|
class="ms-Stack popover-container foreground css-109"
|
||||||
|
data-test="popover-container"
|
||||||
style="max-width: 450px;"
|
style="max-width: 450px;"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@@ -71,6 +72,7 @@ exports[`PopoverMessage Component Edge Cases should handle null children 1`] = `
|
|||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack popover-container foreground css-109"
|
class="ms-Stack popover-container foreground css-109"
|
||||||
|
data-test="popover-container"
|
||||||
style="max-width: 450px;"
|
style="max-width: 450px;"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@@ -133,6 +135,7 @@ exports[`PopoverMessage Component Edge Cases should handle undefined children 1`
|
|||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack popover-container foreground css-109"
|
class="ms-Stack popover-container foreground css-109"
|
||||||
|
data-test="popover-container"
|
||||||
style="max-width: 450px;"
|
style="max-width: 450px;"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@@ -195,6 +198,7 @@ exports[`PopoverMessage Component Edge Cases should handle very long title 1`] =
|
|||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack popover-container foreground css-109"
|
class="ms-Stack popover-container foreground css-109"
|
||||||
|
data-test="popover-container"
|
||||||
style="max-width: 450px;"
|
style="max-width: 450px;"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@@ -266,6 +270,7 @@ exports[`PopoverMessage Component Rendering should render correctly when visible
|
|||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack popover-container foreground css-109"
|
class="ms-Stack popover-container foreground css-109"
|
||||||
|
data-test="popover-container"
|
||||||
style="max-width: 450px;"
|
style="max-width: 450px;"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@@ -335,6 +340,7 @@ exports[`PopoverMessage Component Rendering should render correctly with differe
|
|||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack popover-container foreground css-109"
|
class="ms-Stack popover-container foreground css-109"
|
||||||
|
data-test="popover-container"
|
||||||
style="max-width: 450px;"
|
style="max-width: 450px;"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@@ -409,6 +415,7 @@ exports[`PopoverMessage Component Rendering should render correctly with differe
|
|||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack popover-container foreground css-109"
|
class="ms-Stack popover-container foreground css-109"
|
||||||
|
data-test="popover-container"
|
||||||
style="max-width: 450px;"
|
style="max-width: 450px;"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@@ -478,6 +485,7 @@ exports[`PopoverMessage Component Rendering should render correctly with loading
|
|||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack popover-container foreground loading css-109"
|
class="ms-Stack popover-container foreground loading css-109"
|
||||||
|
data-test="popover-container"
|
||||||
style="max-width: 450px;"
|
style="max-width: 450px;"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ const CreateCopyJobScreens: React.FC = () => {
|
|||||||
<Stack.Item className="createCopyJobScreensContent">
|
<Stack.Item className="createCopyJobScreensContent">
|
||||||
{contextError && (
|
{contextError && (
|
||||||
<MessageBar
|
<MessageBar
|
||||||
|
data-test="Panel:ErrorContainer"
|
||||||
className="createCopyJobErrorMessageBar"
|
className="createCopyJobErrorMessageBar"
|
||||||
messageBarType={MessageBarType.blocked}
|
messageBarType={MessageBarType.blocked}
|
||||||
isMultiline={false}
|
isMultiline={false}
|
||||||
|
|||||||
@@ -31,17 +31,17 @@ const PreviewCopyJob: React.FC = () => {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<Stack tokens={{ childrenGap: 20 }} className="previewCopyJobContainer">
|
<Stack tokens={{ childrenGap: 20 }} className="previewCopyJobContainer" data-test="Panel:PreviewCopyJob">
|
||||||
<FieldRow label={ContainerCopyMessages.jobNameLabel}>
|
<FieldRow label={ContainerCopyMessages.jobNameLabel}>
|
||||||
<TextField value={jobName} onChange={onJobNameChange} />
|
<TextField data-test="job-name-textfield" value={jobName} onChange={onJobNameChange} />
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
<Stack>
|
<Stack>
|
||||||
<Text className="bold">{ContainerCopyMessages.sourceSubscriptionLabel}</Text>
|
<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>
|
||||||
<Stack>
|
<Stack>
|
||||||
<Text className="bold">{ContainerCopyMessages.sourceAccountLabel}</Text>
|
<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>
|
||||||
<Stack>
|
<Stack>
|
||||||
<DetailsList
|
<DetailsList
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
exports[`PreviewCopyJob should handle special characters in database and container names 1`] = `
|
exports[`PreviewCopyJob should handle special characters in database and container names 1`] = `
|
||||||
<div
|
<div
|
||||||
class="ms-Stack previewCopyJobContainer css-109"
|
class="ms-Stack previewCopyJobContainer css-109"
|
||||||
|
data-test="Panel:PreviewCopyJob"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack flex-row css-110"
|
class="ms-Stack flex-row css-110"
|
||||||
@@ -32,6 +33,7 @@ exports[`PreviewCopyJob should handle special characters in database and contain
|
|||||||
<input
|
<input
|
||||||
aria-invalid="false"
|
aria-invalid="false"
|
||||||
class="ms-TextField-field field-115"
|
class="ms-TextField-field field-115"
|
||||||
|
data-test="job-name-textfield"
|
||||||
id="TextField84"
|
id="TextField84"
|
||||||
type="text"
|
type="text"
|
||||||
value="job-with@special#chars_123"
|
value="job-with@special#chars_123"
|
||||||
@@ -51,6 +53,7 @@ exports[`PreviewCopyJob should handle special characters in database and contain
|
|||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-125"
|
class="css-125"
|
||||||
|
data-test="source-subscription-name"
|
||||||
>
|
>
|
||||||
Test Subscription
|
Test Subscription
|
||||||
</span>
|
</span>
|
||||||
@@ -65,6 +68,7 @@ exports[`PreviewCopyJob should handle special characters in database and contain
|
|||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-125"
|
class="css-125"
|
||||||
|
data-test="source-account-name"
|
||||||
>
|
>
|
||||||
test-account
|
test-account
|
||||||
</span>
|
</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`] = `
|
exports[`PreviewCopyJob should render component with cross-subscription setup 1`] = `
|
||||||
<div
|
<div
|
||||||
class="ms-Stack previewCopyJobContainer css-109"
|
class="ms-Stack previewCopyJobContainer css-109"
|
||||||
|
data-test="Panel:PreviewCopyJob"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack flex-row css-110"
|
class="ms-Stack flex-row css-110"
|
||||||
@@ -350,6 +355,7 @@ exports[`PreviewCopyJob should render component with cross-subscription setup 1`
|
|||||||
<input
|
<input
|
||||||
aria-invalid="false"
|
aria-invalid="false"
|
||||||
class="ms-TextField-field field-115"
|
class="ms-TextField-field field-115"
|
||||||
|
data-test="job-name-textfield"
|
||||||
id="TextField96"
|
id="TextField96"
|
||||||
type="text"
|
type="text"
|
||||||
value=""
|
value=""
|
||||||
@@ -369,6 +375,7 @@ exports[`PreviewCopyJob should render component with cross-subscription setup 1`
|
|||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-125"
|
class="css-125"
|
||||||
|
data-test="source-subscription-name"
|
||||||
>
|
>
|
||||||
Test Subscription
|
Test Subscription
|
||||||
</span>
|
</span>
|
||||||
@@ -383,6 +390,7 @@ exports[`PreviewCopyJob should render component with cross-subscription setup 1`
|
|||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-125"
|
class="css-125"
|
||||||
|
data-test="source-account-name"
|
||||||
>
|
>
|
||||||
test-account
|
test-account
|
||||||
</span>
|
</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`] = `
|
exports[`PreviewCopyJob should render with default state and empty job name 1`] = `
|
||||||
<div
|
<div
|
||||||
class="ms-Stack previewCopyJobContainer css-109"
|
class="ms-Stack previewCopyJobContainer css-109"
|
||||||
|
data-test="Panel:PreviewCopyJob"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack flex-row css-110"
|
class="ms-Stack flex-row css-110"
|
||||||
@@ -668,6 +677,7 @@ exports[`PreviewCopyJob should render with default state and empty job name 1`]
|
|||||||
<input
|
<input
|
||||||
aria-invalid="false"
|
aria-invalid="false"
|
||||||
class="ms-TextField-field field-115"
|
class="ms-TextField-field field-115"
|
||||||
|
data-test="job-name-textfield"
|
||||||
id="TextField0"
|
id="TextField0"
|
||||||
type="text"
|
type="text"
|
||||||
value=""
|
value=""
|
||||||
@@ -687,6 +697,7 @@ exports[`PreviewCopyJob should render with default state and empty job name 1`]
|
|||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-125"
|
class="css-125"
|
||||||
|
data-test="source-subscription-name"
|
||||||
>
|
>
|
||||||
Test Subscription
|
Test Subscription
|
||||||
</span>
|
</span>
|
||||||
@@ -701,6 +712,7 @@ exports[`PreviewCopyJob should render with default state and empty job name 1`]
|
|||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-125"
|
class="css-125"
|
||||||
|
data-test="source-account-name"
|
||||||
>
|
>
|
||||||
test-account
|
test-account
|
||||||
</span>
|
</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`] = `
|
exports[`PreviewCopyJob should render with long subscription and account names 1`] = `
|
||||||
<div
|
<div
|
||||||
class="ms-Stack previewCopyJobContainer css-109"
|
class="ms-Stack previewCopyJobContainer css-109"
|
||||||
|
data-test="Panel:PreviewCopyJob"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack flex-row css-110"
|
class="ms-Stack flex-row css-110"
|
||||||
@@ -986,6 +999,7 @@ exports[`PreviewCopyJob should render with long subscription and account names 1
|
|||||||
<input
|
<input
|
||||||
aria-invalid="false"
|
aria-invalid="false"
|
||||||
class="ms-TextField-field field-115"
|
class="ms-TextField-field field-115"
|
||||||
|
data-test="job-name-textfield"
|
||||||
id="TextField60"
|
id="TextField60"
|
||||||
type="text"
|
type="text"
|
||||||
value=""
|
value=""
|
||||||
@@ -1005,6 +1019,7 @@ exports[`PreviewCopyJob should render with long subscription and account names 1
|
|||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-125"
|
class="css-125"
|
||||||
|
data-test="source-subscription-name"
|
||||||
>
|
>
|
||||||
This is a very long subscription name that might cause display issues if not handled properly
|
This is a very long subscription name that might cause display issues if not handled properly
|
||||||
</span>
|
</span>
|
||||||
@@ -1019,6 +1034,7 @@ exports[`PreviewCopyJob should render with long subscription and account names 1
|
|||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-125"
|
class="css-125"
|
||||||
|
data-test="source-account-name"
|
||||||
>
|
>
|
||||||
this-is-a-very-long-database-account-name-that-might-cause-display-issues
|
this-is-a-very-long-database-account-name-that-might-cause-display-issues
|
||||||
</span>
|
</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`] = `
|
exports[`PreviewCopyJob should render with missing source account information 1`] = `
|
||||||
<div
|
<div
|
||||||
class="ms-Stack previewCopyJobContainer css-109"
|
class="ms-Stack previewCopyJobContainer css-109"
|
||||||
|
data-test="Panel:PreviewCopyJob"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack flex-row css-110"
|
class="ms-Stack flex-row css-110"
|
||||||
@@ -1304,6 +1321,7 @@ exports[`PreviewCopyJob should render with missing source account information 1`
|
|||||||
<input
|
<input
|
||||||
aria-invalid="false"
|
aria-invalid="false"
|
||||||
class="ms-TextField-field field-115"
|
class="ms-TextField-field field-115"
|
||||||
|
data-test="job-name-textfield"
|
||||||
id="TextField36"
|
id="TextField36"
|
||||||
type="text"
|
type="text"
|
||||||
value=""
|
value=""
|
||||||
@@ -1323,6 +1341,7 @@ exports[`PreviewCopyJob should render with missing source account information 1`
|
|||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-125"
|
class="css-125"
|
||||||
|
data-test="source-subscription-name"
|
||||||
>
|
>
|
||||||
Test Subscription
|
Test Subscription
|
||||||
</span>
|
</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`] = `
|
exports[`PreviewCopyJob should render with missing source subscription information 1`] = `
|
||||||
<div
|
<div
|
||||||
class="ms-Stack previewCopyJobContainer css-109"
|
class="ms-Stack previewCopyJobContainer css-109"
|
||||||
|
data-test="Panel:PreviewCopyJob"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack flex-row css-110"
|
class="ms-Stack flex-row css-110"
|
||||||
@@ -1617,6 +1637,7 @@ exports[`PreviewCopyJob should render with missing source subscription informati
|
|||||||
<input
|
<input
|
||||||
aria-invalid="false"
|
aria-invalid="false"
|
||||||
class="ms-TextField-field field-115"
|
class="ms-TextField-field field-115"
|
||||||
|
data-test="job-name-textfield"
|
||||||
id="TextField24"
|
id="TextField24"
|
||||||
type="text"
|
type="text"
|
||||||
value=""
|
value=""
|
||||||
@@ -1645,6 +1666,7 @@ exports[`PreviewCopyJob should render with missing source subscription informati
|
|||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-125"
|
class="css-125"
|
||||||
|
data-test="source-account-name"
|
||||||
>
|
>
|
||||||
test-account
|
test-account
|
||||||
</span>
|
</span>
|
||||||
@@ -1901,6 +1923,7 @@ exports[`PreviewCopyJob should render with missing source subscription informati
|
|||||||
exports[`PreviewCopyJob should render with online migration type 1`] = `
|
exports[`PreviewCopyJob should render with online migration type 1`] = `
|
||||||
<div
|
<div
|
||||||
class="ms-Stack previewCopyJobContainer css-109"
|
class="ms-Stack previewCopyJobContainer css-109"
|
||||||
|
data-test="Panel:PreviewCopyJob"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack flex-row css-110"
|
class="ms-Stack flex-row css-110"
|
||||||
@@ -1930,6 +1953,7 @@ exports[`PreviewCopyJob should render with online migration type 1`] = `
|
|||||||
<input
|
<input
|
||||||
aria-invalid="false"
|
aria-invalid="false"
|
||||||
class="ms-TextField-field field-115"
|
class="ms-TextField-field field-115"
|
||||||
|
data-test="job-name-textfield"
|
||||||
id="TextField72"
|
id="TextField72"
|
||||||
type="text"
|
type="text"
|
||||||
value="online-migration-job"
|
value="online-migration-job"
|
||||||
@@ -1949,6 +1973,7 @@ exports[`PreviewCopyJob should render with online migration type 1`] = `
|
|||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-125"
|
class="css-125"
|
||||||
|
data-test="source-subscription-name"
|
||||||
>
|
>
|
||||||
Test Subscription
|
Test Subscription
|
||||||
</span>
|
</span>
|
||||||
@@ -1963,6 +1988,7 @@ exports[`PreviewCopyJob should render with online migration type 1`] = `
|
|||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-125"
|
class="css-125"
|
||||||
|
data-test="source-account-name"
|
||||||
>
|
>
|
||||||
test-account
|
test-account
|
||||||
</span>
|
</span>
|
||||||
@@ -2219,6 +2245,7 @@ exports[`PreviewCopyJob should render with online migration type 1`] = `
|
|||||||
exports[`PreviewCopyJob should render with pre-filled job name 1`] = `
|
exports[`PreviewCopyJob should render with pre-filled job name 1`] = `
|
||||||
<div
|
<div
|
||||||
class="ms-Stack previewCopyJobContainer css-109"
|
class="ms-Stack previewCopyJobContainer css-109"
|
||||||
|
data-test="Panel:PreviewCopyJob"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack flex-row css-110"
|
class="ms-Stack flex-row css-110"
|
||||||
@@ -2248,6 +2275,7 @@ exports[`PreviewCopyJob should render with pre-filled job name 1`] = `
|
|||||||
<input
|
<input
|
||||||
aria-invalid="false"
|
aria-invalid="false"
|
||||||
class="ms-TextField-field field-115"
|
class="ms-TextField-field field-115"
|
||||||
|
data-test="job-name-textfield"
|
||||||
id="TextField12"
|
id="TextField12"
|
||||||
type="text"
|
type="text"
|
||||||
value="custom-job-name-123"
|
value="custom-job-name-123"
|
||||||
@@ -2267,6 +2295,7 @@ exports[`PreviewCopyJob should render with pre-filled job name 1`] = `
|
|||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-125"
|
class="css-125"
|
||||||
|
data-test="source-subscription-name"
|
||||||
>
|
>
|
||||||
Test Subscription
|
Test Subscription
|
||||||
</span>
|
</span>
|
||||||
@@ -2281,6 +2310,7 @@ exports[`PreviewCopyJob should render with pre-filled job name 1`] = `
|
|||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-125"
|
class="css-125"
|
||||||
|
data-test="source-account-name"
|
||||||
>
|
>
|
||||||
test-account
|
test-account
|
||||||
</span>
|
</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`] = `
|
exports[`PreviewCopyJob should render with undefined database and container names 1`] = `
|
||||||
<div
|
<div
|
||||||
class="ms-Stack previewCopyJobContainer css-109"
|
class="ms-Stack previewCopyJobContainer css-109"
|
||||||
|
data-test="Panel:PreviewCopyJob"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack flex-row css-110"
|
class="ms-Stack flex-row css-110"
|
||||||
@@ -2566,6 +2597,7 @@ exports[`PreviewCopyJob should render with undefined database and container name
|
|||||||
<input
|
<input
|
||||||
aria-invalid="false"
|
aria-invalid="false"
|
||||||
class="ms-TextField-field field-115"
|
class="ms-TextField-field field-115"
|
||||||
|
data-test="job-name-textfield"
|
||||||
id="TextField48"
|
id="TextField48"
|
||||||
type="text"
|
type="text"
|
||||||
value=""
|
value=""
|
||||||
@@ -2585,6 +2617,7 @@ exports[`PreviewCopyJob should render with undefined database and container name
|
|||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-125"
|
class="css-125"
|
||||||
|
data-test="source-subscription-name"
|
||||||
>
|
>
|
||||||
Test Subscription
|
Test Subscription
|
||||||
</span>
|
</span>
|
||||||
@@ -2599,6 +2632,7 @@ exports[`PreviewCopyJob should render with undefined database and container name
|
|||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-125"
|
class="css-125"
|
||||||
|
data-test="source-account-name"
|
||||||
>
|
>
|
||||||
test-account
|
test-account
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import ContainerCopyMessages from "../../../../ContainerCopyMessages";
|
|||||||
import { CopyJobContext } from "../../../../Context/CopyJobContext";
|
import { CopyJobContext } from "../../../../Context/CopyJobContext";
|
||||||
import { CopyJobMigrationType } from "../../../../Enums/CopyJobEnums";
|
import { CopyJobMigrationType } from "../../../../Enums/CopyJobEnums";
|
||||||
import { CopyJobContextProviderType, CopyJobContextState } from "../../../../Types/CopyJobTypes";
|
import { CopyJobContextProviderType, CopyJobContextState } from "../../../../Types/CopyJobTypes";
|
||||||
import { AccountDropdown } from "./AccountDropdown";
|
import { AccountDropdown, normalizeAccountId } from "./AccountDropdown";
|
||||||
|
|
||||||
jest.mock("../../../../../../hooks/useDatabaseAccounts");
|
jest.mock("../../../../../../hooks/useDatabaseAccounts");
|
||||||
jest.mock("../../../../../../UserContext", () => ({
|
jest.mock("../../../../../../UserContext", () => ({
|
||||||
@@ -202,13 +202,16 @@ describe("AccountDropdown", () => {
|
|||||||
|
|
||||||
const stateUpdateFunction = mockSetCopyJobState.mock.calls[0][0];
|
const stateUpdateFunction = mockSetCopyJobState.mock.calls[0][0];
|
||||||
const newState = stateUpdateFunction(mockCopyJobState);
|
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 () => {
|
it("should auto-select predefined account from userContext if available", async () => {
|
||||||
const userContextAccount = {
|
const userContextAccount = {
|
||||||
...mockDatabaseAccount2,
|
...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;
|
(userContext as any).databaseAccount = userContextAccount;
|
||||||
@@ -223,7 +226,10 @@ describe("AccountDropdown", () => {
|
|||||||
|
|
||||||
const stateUpdateFunction = mockSetCopyJobState.mock.calls[0][0];
|
const stateUpdateFunction = mockSetCopyJobState.mock.calls[0][0];
|
||||||
const newState = stateUpdateFunction(mockCopyJobState);
|
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 () => {
|
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 stateUpdateFunction = mockSetCopyJobState.mock.calls[0][0];
|
||||||
const newState = stateUpdateFunction(contextWithSelectedAccount.copyJobState);
|
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 () => {
|
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", () => {
|
it("should normalize account ID for Portal platform", () => {
|
||||||
const portalAccount = {
|
const portalAccount = {
|
||||||
...mockDatabaseAccount1,
|
...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;
|
(configContext as any).platform = Platform.Portal;
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import FieldRow from "../../Components/FieldRow";
|
|||||||
|
|
||||||
interface AccountDropdownProps {}
|
interface AccountDropdownProps {}
|
||||||
|
|
||||||
const normalizeAccountId = (id: string) => {
|
export const normalizeAccountId = (id: string = "") => {
|
||||||
if (configContext.platform === Platform.Portal) {
|
if (configContext.platform === Platform.Portal) {
|
||||||
return id.replace("/Microsoft.DocumentDb/", "/Microsoft.DocumentDB/");
|
return id.replace("/Microsoft.DocumentDb/", "/Microsoft.DocumentDB/");
|
||||||
} else if (configContext.platform === Platform.Hosted) {
|
} else if (configContext.platform === Platform.Hosted) {
|
||||||
@@ -27,7 +27,12 @@ export const AccountDropdown: React.FC<AccountDropdownProps> = () => {
|
|||||||
|
|
||||||
const selectedSubscriptionId = copyJobState?.source?.subscription?.subscriptionId;
|
const selectedSubscriptionId = copyJobState?.source?.subscription?.subscriptionId;
|
||||||
const allAccounts: DatabaseAccount[] = useDatabaseAccounts(selectedSubscriptionId);
|
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) => {
|
const updateCopyJobState = (newAccount: DatabaseAccount) => {
|
||||||
setCopyJobState((prevState) => {
|
setCopyJobState((prevState) => {
|
||||||
@@ -47,7 +52,7 @@ export const AccountDropdown: React.FC<AccountDropdownProps> = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (sqlApiOnlyAccounts && sqlApiOnlyAccounts.length > 0 && selectedSubscriptionId) {
|
if (sqlApiOnlyAccounts && sqlApiOnlyAccounts.length > 0 && selectedSubscriptionId) {
|
||||||
const currentAccountId = copyJobState?.source?.account?.id;
|
const currentAccountId = copyJobState?.source?.account?.id;
|
||||||
const predefinedAccountId = userContext.databaseAccount?.id;
|
const predefinedAccountId = normalizeAccountId(userContext.databaseAccount?.id);
|
||||||
const selectedAccountId = currentAccountId || predefinedAccountId;
|
const selectedAccountId = currentAccountId || predefinedAccountId;
|
||||||
|
|
||||||
const targetAccount: DatabaseAccount | null =
|
const targetAccount: DatabaseAccount | null =
|
||||||
@@ -58,7 +63,7 @@ export const AccountDropdown: React.FC<AccountDropdownProps> = () => {
|
|||||||
|
|
||||||
const accountOptions =
|
const accountOptions =
|
||||||
sqlApiOnlyAccounts?.map((account) => ({
|
sqlApiOnlyAccounts?.map((account) => ({
|
||||||
key: normalizeAccountId(account.id),
|
key: account.id,
|
||||||
text: account.name,
|
text: account.name,
|
||||||
data: account,
|
data: account,
|
||||||
})) || [];
|
})) || [];
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ interface MigrationTypeCheckboxProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const MigrationTypeCheckbox: React.FC<MigrationTypeCheckboxProps> = React.memo(({ checked, onChange }) => (
|
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} />
|
<Checkbox label={ContainerCopyMessages.migrationTypeCheckboxLabel} checked={checked} onChange={onChange} />
|
||||||
</Stack>
|
</Stack>
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
exports[`MigrationTypeCheckbox Component Rendering should render in checked state 1`] = `
|
exports[`MigrationTypeCheckbox Component Rendering should render in checked state 1`] = `
|
||||||
<div
|
<div
|
||||||
class="ms-Stack migrationTypeRow css-109"
|
class="ms-Stack migrationTypeRow css-109"
|
||||||
|
data-test="migration-type-checkbox"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Checkbox is-checked is-enabled root-119"
|
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`] = `
|
exports[`MigrationTypeCheckbox Component Rendering should render with default props (unchecked state) 1`] = `
|
||||||
<div
|
<div
|
||||||
class="ms-Stack migrationTypeRow css-109"
|
class="ms-Stack migrationTypeRow css-109"
|
||||||
|
data-test="migration-type-checkbox"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Checkbox is-enabled root-110"
|
class="ms-Checkbox is-enabled root-110"
|
||||||
|
|||||||
@@ -47,7 +47,11 @@ const SelectSourceAndTargetContainers = ({ showAddCollectionPanel }: SelectSourc
|
|||||||
const onDropdownChange = dropDownChangeHandler(setCopyJobState);
|
const onDropdownChange = dropDownChangeHandler(setCopyJobState);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack className="selectSourceAndTargetContainers" tokens={{ childrenGap: 25 }}>
|
<Stack
|
||||||
|
data-test="Panel:SelectSourceAndTargetContainers"
|
||||||
|
className="selectSourceAndTargetContainers"
|
||||||
|
tokens={{ childrenGap: 25 }}
|
||||||
|
>
|
||||||
<span>{ContainerCopyMessages.selectSourceAndTargetContainersDescription}</span>
|
<span>{ContainerCopyMessages.selectSourceAndTargetContainersDescription}</span>
|
||||||
<DatabaseContainerSection
|
<DatabaseContainerSection
|
||||||
heading={ContainerCopyMessages.sourceContainerSubHeading}
|
heading={ContainerCopyMessages.sourceContainerSubHeading}
|
||||||
@@ -59,6 +63,7 @@ const SelectSourceAndTargetContainers = ({ showAddCollectionPanel }: SelectSourc
|
|||||||
selectedContainer={source?.containerId}
|
selectedContainer={source?.containerId}
|
||||||
containerDisabled={!source?.databaseId}
|
containerDisabled={!source?.databaseId}
|
||||||
containerOnChange={onDropdownChange("sourceContainer")}
|
containerOnChange={onDropdownChange("sourceContainer")}
|
||||||
|
sectionType="source"
|
||||||
/>
|
/>
|
||||||
<DatabaseContainerSection
|
<DatabaseContainerSection
|
||||||
heading={ContainerCopyMessages.targetContainerSubHeading}
|
heading={ContainerCopyMessages.targetContainerSubHeading}
|
||||||
@@ -71,6 +76,7 @@ const SelectSourceAndTargetContainers = ({ showAddCollectionPanel }: SelectSourc
|
|||||||
containerDisabled={!target?.databaseId}
|
containerDisabled={!target?.databaseId}
|
||||||
containerOnChange={onDropdownChange("targetContainer")}
|
containerOnChange={onDropdownChange("targetContainer")}
|
||||||
handleOnDemandCreateContainer={showAddCollectionPanel}
|
handleOnDemandCreateContainer={showAddCollectionPanel}
|
||||||
|
sectionType="target"
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ describe("DatabaseContainerSection", () => {
|
|||||||
selectedContainer: "container1",
|
selectedContainer: "container1",
|
||||||
containerDisabled: false,
|
containerDisabled: false,
|
||||||
containerOnChange: mockContainerOnChange,
|
containerOnChange: mockContainerOnChange,
|
||||||
|
sectionType: "source",
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -292,6 +293,7 @@ describe("DatabaseContainerSection", () => {
|
|||||||
containerOptions: mockContainerOptions,
|
containerOptions: mockContainerOptions,
|
||||||
selectedContainer: "container1",
|
selectedContainer: "container1",
|
||||||
containerOnChange: mockContainerOnChange,
|
containerOnChange: mockContainerOnChange,
|
||||||
|
sectionType: "source",
|
||||||
};
|
};
|
||||||
|
|
||||||
render(<DatabaseContainerSection {...minimalProps} />);
|
render(<DatabaseContainerSection {...minimalProps} />);
|
||||||
@@ -393,6 +395,7 @@ describe("DatabaseContainerSection", () => {
|
|||||||
containerOptions: [{ key: "c1", text: "Container 1", data: { id: "c1" } }],
|
containerOptions: [{ key: "c1", text: "Container 1", data: { id: "c1" } }],
|
||||||
selectedContainer: "c1",
|
selectedContainer: "c1",
|
||||||
containerOnChange: jest.fn(),
|
containerOnChange: jest.fn(),
|
||||||
|
sectionType: "source",
|
||||||
};
|
};
|
||||||
|
|
||||||
const { container } = render(<DatabaseContainerSection {...minimalProps} />);
|
const { container } = render(<DatabaseContainerSection {...minimalProps} />);
|
||||||
@@ -411,6 +414,7 @@ describe("DatabaseContainerSection", () => {
|
|||||||
containerDisabled: false,
|
containerDisabled: false,
|
||||||
containerOnChange: jest.fn(),
|
containerOnChange: jest.fn(),
|
||||||
handleOnDemandCreateContainer: jest.fn(),
|
handleOnDemandCreateContainer: jest.fn(),
|
||||||
|
sectionType: "target",
|
||||||
};
|
};
|
||||||
|
|
||||||
const { container } = render(<DatabaseContainerSection {...fullProps} />);
|
const { container } = render(<DatabaseContainerSection {...fullProps} />);
|
||||||
@@ -428,6 +432,7 @@ describe("DatabaseContainerSection", () => {
|
|||||||
selectedContainer: "container1",
|
selectedContainer: "container1",
|
||||||
containerDisabled: true,
|
containerDisabled: true,
|
||||||
containerOnChange: jest.fn(),
|
containerOnChange: jest.fn(),
|
||||||
|
sectionType: "target",
|
||||||
};
|
};
|
||||||
|
|
||||||
const { container } = render(<DatabaseContainerSection {...disabledProps} />);
|
const { container } = render(<DatabaseContainerSection {...disabledProps} />);
|
||||||
@@ -443,6 +448,7 @@ describe("DatabaseContainerSection", () => {
|
|||||||
containerOptions: [],
|
containerOptions: [],
|
||||||
selectedContainer: "",
|
selectedContainer: "",
|
||||||
containerOnChange: jest.fn(),
|
containerOnChange: jest.fn(),
|
||||||
|
sectionType: "target",
|
||||||
};
|
};
|
||||||
|
|
||||||
const { container } = render(<DatabaseContainerSection {...emptyOptionsProps} />);
|
const { container } = render(<DatabaseContainerSection {...emptyOptionsProps} />);
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export const DatabaseContainerSection = ({
|
|||||||
containerDisabled,
|
containerDisabled,
|
||||||
containerOnChange,
|
containerOnChange,
|
||||||
handleOnDemandCreateContainer,
|
handleOnDemandCreateContainer,
|
||||||
|
sectionType,
|
||||||
}: DatabaseContainerSectionProps) => (
|
}: DatabaseContainerSectionProps) => (
|
||||||
<Stack tokens={{ childrenGap: 15 }} className="databaseContainerSection">
|
<Stack tokens={{ childrenGap: 15 }} className="databaseContainerSection">
|
||||||
<label className="subHeading">{heading}</label>
|
<label className="subHeading">{heading}</label>
|
||||||
@@ -27,6 +28,7 @@ export const DatabaseContainerSection = ({
|
|||||||
disabled={!!databaseDisabled}
|
disabled={!!databaseDisabled}
|
||||||
selectedKey={selectedDatabase}
|
selectedKey={selectedDatabase}
|
||||||
onChange={databaseOnChange}
|
onChange={databaseOnChange}
|
||||||
|
data-test={`${sectionType}-databaseDropdown`}
|
||||||
/>
|
/>
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
<FieldRow label={ContainerCopyMessages.containerDropdownLabel}>
|
<FieldRow label={ContainerCopyMessages.containerDropdownLabel}>
|
||||||
@@ -39,6 +41,7 @@ export const DatabaseContainerSection = ({
|
|||||||
disabled={!!containerDisabled}
|
disabled={!!containerDisabled}
|
||||||
selectedKey={selectedContainer}
|
selectedKey={selectedContainer}
|
||||||
onChange={containerOnChange}
|
onChange={containerOnChange}
|
||||||
|
data-test={`${sectionType}-containerDropdown`}
|
||||||
/>
|
/>
|
||||||
{handleOnDemandCreateContainer && (
|
{handleOnDemandCreateContainer && (
|
||||||
<ActionButton className="create-container-link-btn" onClick={() => handleOnDemandCreateContainer()}>
|
<ActionButton className="create-container-link-btn" onClick={() => handleOnDemandCreateContainer()}>
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ exports[`DatabaseContainerSection Snapshot Testing matches snapshot with all pro
|
|||||||
class="ms-Dropdown is-required dropdown-112"
|
class="ms-Dropdown is-required dropdown-112"
|
||||||
data-is-focusable="true"
|
data-is-focusable="true"
|
||||||
data-ktp-target="true"
|
data-ktp-target="true"
|
||||||
|
data-test="target-databaseDropdown"
|
||||||
id="Dropdown98"
|
id="Dropdown98"
|
||||||
role="combobox"
|
role="combobox"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@@ -94,6 +95,7 @@ exports[`DatabaseContainerSection Snapshot Testing matches snapshot with all pro
|
|||||||
class="ms-Dropdown is-required dropdown-112"
|
class="ms-Dropdown is-required dropdown-112"
|
||||||
data-is-focusable="true"
|
data-is-focusable="true"
|
||||||
data-ktp-target="true"
|
data-ktp-target="true"
|
||||||
|
data-test="target-containerDropdown"
|
||||||
id="Dropdown99"
|
id="Dropdown99"
|
||||||
role="combobox"
|
role="combobox"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@@ -182,6 +184,7 @@ exports[`DatabaseContainerSection Snapshot Testing matches snapshot with disable
|
|||||||
class="ms-Dropdown is-disabled is-required dropdown-143"
|
class="ms-Dropdown is-disabled is-required dropdown-143"
|
||||||
data-is-focusable="false"
|
data-is-focusable="false"
|
||||||
data-ktp-target="true"
|
data-ktp-target="true"
|
||||||
|
data-test="target-databaseDropdown"
|
||||||
id="Dropdown103"
|
id="Dropdown103"
|
||||||
role="combobox"
|
role="combobox"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
@@ -239,6 +242,7 @@ exports[`DatabaseContainerSection Snapshot Testing matches snapshot with disable
|
|||||||
class="ms-Dropdown is-disabled is-required dropdown-143"
|
class="ms-Dropdown is-disabled is-required dropdown-143"
|
||||||
data-is-focusable="false"
|
data-is-focusable="false"
|
||||||
data-ktp-target="true"
|
data-ktp-target="true"
|
||||||
|
data-test="target-containerDropdown"
|
||||||
id="Dropdown104"
|
id="Dropdown104"
|
||||||
role="combobox"
|
role="combobox"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
@@ -306,6 +310,7 @@ exports[`DatabaseContainerSection Snapshot Testing matches snapshot with empty o
|
|||||||
class="ms-Dropdown is-required dropdown-112"
|
class="ms-Dropdown is-required dropdown-112"
|
||||||
data-is-focusable="true"
|
data-is-focusable="true"
|
||||||
data-ktp-target="true"
|
data-ktp-target="true"
|
||||||
|
data-test="target-databaseDropdown"
|
||||||
id="Dropdown105"
|
id="Dropdown105"
|
||||||
role="combobox"
|
role="combobox"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@@ -363,6 +368,7 @@ exports[`DatabaseContainerSection Snapshot Testing matches snapshot with empty o
|
|||||||
class="ms-Dropdown is-required dropdown-112"
|
class="ms-Dropdown is-required dropdown-112"
|
||||||
data-is-focusable="true"
|
data-is-focusable="true"
|
||||||
data-ktp-target="true"
|
data-ktp-target="true"
|
||||||
|
data-test="target-containerDropdown"
|
||||||
id="Dropdown106"
|
id="Dropdown106"
|
||||||
role="combobox"
|
role="combobox"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@@ -430,6 +436,7 @@ exports[`DatabaseContainerSection Snapshot Testing matches snapshot with minimal
|
|||||||
class="ms-Dropdown is-required dropdown-112"
|
class="ms-Dropdown is-required dropdown-112"
|
||||||
data-is-focusable="true"
|
data-is-focusable="true"
|
||||||
data-ktp-target="true"
|
data-ktp-target="true"
|
||||||
|
data-test="source-databaseDropdown"
|
||||||
id="Dropdown96"
|
id="Dropdown96"
|
||||||
role="combobox"
|
role="combobox"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@@ -487,6 +494,7 @@ exports[`DatabaseContainerSection Snapshot Testing matches snapshot with minimal
|
|||||||
class="ms-Dropdown is-required dropdown-112"
|
class="ms-Dropdown is-required dropdown-112"
|
||||||
data-is-focusable="true"
|
data-is-focusable="true"
|
||||||
data-ktp-target="true"
|
data-ktp-target="true"
|
||||||
|
data-test="source-containerDropdown"
|
||||||
id="Dropdown97"
|
id="Dropdown97"
|
||||||
role="combobox"
|
role="combobox"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ const CopyJobActionMenu: React.FC<CopyJobActionMenuProps> = ({ job, handleClick
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<IconButton
|
<IconButton
|
||||||
|
data-test={`CopyJobActionMenu/Button:${job.Name}`}
|
||||||
role="button"
|
role="button"
|
||||||
iconProps={{ iconName: "More", styles: { root: { fontSize: "20px", fontWeight: "bold" } } }}
|
iconProps={{ iconName: "More", styles: { root: { fontSize: "20px", fontWeight: "bold" } } }}
|
||||||
menuProps={{ items: getMenuItems() }}
|
menuProps={{ items: getMenuItems() }}
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ const CopyJobsList: React.FC<CopyJobsListProps> = ({ jobs, handleActionClick, pa
|
|||||||
<Stack.Item verticalFill={true} grow={1} shrink={1} style={styles.stackItem}>
|
<Stack.Item verticalFill={true} grow={1} shrink={1} style={styles.stackItem}>
|
||||||
<ScrollablePane scrollbarVisibility={ScrollbarVisibility.auto}>
|
<ScrollablePane scrollbarVisibility={ScrollbarVisibility.auto}>
|
||||||
<ShimmeredDetailsList
|
<ShimmeredDetailsList
|
||||||
|
className="CopyJobListContainer"
|
||||||
onRenderRow={_onRenderRow}
|
onRenderRow={_onRenderRow}
|
||||||
checkboxVisibility={2}
|
checkboxVisibility={2}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ export interface DatabaseContainerSectionProps {
|
|||||||
containerDisabled?: boolean;
|
containerDisabled?: boolean;
|
||||||
containerOnChange: (ev: React.FormEvent<HTMLDivElement>, option: DropdownOptionType) => void;
|
containerOnChange: (ev: React.FormEvent<HTMLDivElement>, option: DropdownOptionType) => void;
|
||||||
handleOnDemandCreateContainer?: () => void;
|
handleOnDemandCreateContainer?: () => void;
|
||||||
|
sectionType: "source" | "target";
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CopyJobContextState {
|
export interface CopyJobContextState {
|
||||||
|
|||||||
@@ -187,7 +187,7 @@ export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({
|
|||||||
<Text styles={textSubHeadingStyle}>Current {partitionKeyName.toLowerCase()}</Text>
|
<Text styles={textSubHeadingStyle}>Current {partitionKeyName.toLowerCase()}</Text>
|
||||||
<Text styles={textSubHeadingStyle}>Partitioning</Text>
|
<Text styles={textSubHeadingStyle}>Partitioning</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack tokens={{ childrenGap: 5 }}>
|
<Stack tokens={{ childrenGap: 5 }} data-test="partition-key-values">
|
||||||
<Text styles={textSubHeadingStyle1}>{partitionKeyValue}</Text>
|
<Text styles={textSubHeadingStyle1}>{partitionKeyValue}</Text>
|
||||||
<Text styles={textSubHeadingStyle1}>
|
<Text styles={textSubHeadingStyle1}>
|
||||||
{isHierarchicalPartitionedContainer() ? "Hierarchical" : "Non-hierarchical"}
|
{isHierarchicalPartitionedContainer() ? "Hierarchical" : "Non-hierarchical"}
|
||||||
@@ -199,6 +199,7 @@ export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({
|
|||||||
{!isReadOnly && (
|
{!isReadOnly && (
|
||||||
<>
|
<>
|
||||||
<MessageBar
|
<MessageBar
|
||||||
|
data-test="partition-key-warning"
|
||||||
messageBarType={MessageBarType.warning}
|
messageBarType={MessageBarType.warning}
|
||||||
messageBarIconProps={{ iconName: "WarningSolid", className: "messageBarWarningIcon" }}
|
messageBarIconProps={{ iconName: "WarningSolid", className: "messageBarWarningIcon" }}
|
||||||
styles={darkThemeMessageBarStyles}
|
styles={darkThemeMessageBarStyles}
|
||||||
@@ -220,6 +221,7 @@ export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({
|
|||||||
</Text>
|
</Text>
|
||||||
{configContext.platform !== Platform.Emulator && (
|
{configContext.platform !== Platform.Emulator && (
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
|
data-test="change-partition-key-button"
|
||||||
styles={{ root: { width: "fit-content" } }}
|
styles={{ root: { width: "fit-content" } }}
|
||||||
text="Change"
|
text="Change"
|
||||||
onClick={startPartitionkeyChangeWorkflow}
|
onClick={startPartitionkeyChangeWorkflow}
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ exports[`PartitionKeyComponent renders default component and matches snapshot 1`
|
|||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack
|
<Stack
|
||||||
|
data-test="partition-key-values"
|
||||||
tokens={
|
tokens={
|
||||||
{
|
{
|
||||||
"childrenGap": 5,
|
"childrenGap": 5,
|
||||||
@@ -108,6 +109,7 @@ exports[`PartitionKeyComponent renders default component and matches snapshot 1`
|
|||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
<StyledMessageBar
|
<StyledMessageBar
|
||||||
|
data-test="partition-key-warning"
|
||||||
messageBarIconProps={
|
messageBarIconProps={
|
||||||
{
|
{
|
||||||
"className": "messageBarWarningIcon",
|
"className": "messageBarWarningIcon",
|
||||||
@@ -160,6 +162,7 @@ exports[`PartitionKeyComponent renders default component and matches snapshot 1`
|
|||||||
To change the partition key, a new destination container must be created or an existing destination container selected. Data will then be copied to the destination container.
|
To change the partition key, a new destination container must be created or an existing destination container selected. Data will then be copied to the destination container.
|
||||||
</Text>
|
</Text>
|
||||||
<CustomizedPrimaryButton
|
<CustomizedPrimaryButton
|
||||||
|
data-test="change-partition-key-button"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
styles={
|
styles={
|
||||||
{
|
{
|
||||||
@@ -237,6 +240,7 @@ exports[`PartitionKeyComponent renders read-only component and matches snapshot
|
|||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack
|
<Stack
|
||||||
|
data-test="partition-key-values"
|
||||||
tokens={
|
tokens={
|
||||||
{
|
{
|
||||||
"childrenGap": 5,
|
"childrenGap": 5,
|
||||||
|
|||||||
@@ -437,13 +437,14 @@ export default class Explorer {
|
|||||||
public onRefreshResourcesClick = async (): Promise<void> => {
|
public onRefreshResourcesClick = async (): Promise<void> => {
|
||||||
if (isFabricMirroredKey()) {
|
if (isFabricMirroredKey()) {
|
||||||
scheduleRefreshFabricToken(true).then(() => this.refreshAllDatabases());
|
scheduleRefreshFabricToken(true).then(() => this.refreshAllDatabases());
|
||||||
return;
|
} else {
|
||||||
}
|
|
||||||
|
|
||||||
await (userContext.authType === AuthType.ResourceToken
|
await (userContext.authType === AuthType.ResourceToken
|
||||||
? this.refreshDatabaseForResourceToken()
|
? this.refreshDatabaseForResourceToken()
|
||||||
: this.refreshAllDatabases());
|
: this.refreshAllDatabases());
|
||||||
await this.refreshNotebookList();
|
await this.refreshNotebookList();
|
||||||
|
}
|
||||||
|
|
||||||
|
logConsoleInfo("Successfully refreshed databases");
|
||||||
};
|
};
|
||||||
|
|
||||||
// Facade
|
// Facade
|
||||||
|
|||||||
@@ -208,7 +208,7 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</Stack>
|
</Stack>
|
||||||
{createNewContainer ? (
|
{createNewContainer ? (
|
||||||
<Stack>
|
<Stack data-test="create-new-container-form">
|
||||||
<MessageBar>All configurations except for unique keys will be copied from the source container</MessageBar>
|
<MessageBar>All configurations except for unique keys will be copied from the source container</MessageBar>
|
||||||
<Stack className="panelGroupSpacing">
|
<Stack className="panelGroupSpacing">
|
||||||
<Stack horizontal>
|
<Stack horizontal>
|
||||||
@@ -230,6 +230,7 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
|
|||||||
</TooltipHost>
|
</TooltipHost>
|
||||||
</Stack>
|
</Stack>
|
||||||
<input
|
<input
|
||||||
|
data-test="new-container-id-input"
|
||||||
name="collectionId"
|
name="collectionId"
|
||||||
id="collectionId"
|
id="collectionId"
|
||||||
type="text"
|
type="text"
|
||||||
@@ -271,6 +272,7 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
|
|||||||
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
data-test="new-container-partition-key-input"
|
||||||
id="addCollection-partitionKeyValue"
|
id="addCollection-partitionKeyValue"
|
||||||
aria-required
|
aria-required
|
||||||
required
|
required
|
||||||
@@ -304,6 +306,7 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
|
|||||||
type="text"
|
type="text"
|
||||||
id="addCollection-partitionKeyValue"
|
id="addCollection-partitionKeyValue"
|
||||||
key={`addCollection-partitionKeyValue_${index}`}
|
key={`addCollection-partitionKeyValue_${index}`}
|
||||||
|
data-test={`new-container-sub-partition-key-input-${index}`}
|
||||||
aria-required
|
aria-required
|
||||||
required
|
required
|
||||||
size={40}
|
size={40}
|
||||||
@@ -327,6 +330,8 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
data-test={`remove-sub-partition-key-button-${index}`}
|
||||||
|
ariaLabel="Remove hierarchical partition key"
|
||||||
iconProps={{ iconName: "Delete" }}
|
iconProps={{ iconName: "Delete" }}
|
||||||
style={{ height: 27 }}
|
style={{ height: 27 }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -339,6 +344,7 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
|
|||||||
})}
|
})}
|
||||||
<Stack className="panelGroupSpacing">
|
<Stack className="panelGroupSpacing">
|
||||||
<DefaultButton
|
<DefaultButton
|
||||||
|
data-test="add-sub-partition-key-button"
|
||||||
styles={{ root: { padding: 0, width: 200, height: 30 }, label: { fontSize: 12 } }}
|
styles={{ root: { padding: 0, width: 200, height: 30 }, label: { fontSize: 12 } }}
|
||||||
disabled={subPartitionKeys.length >= Constants.BackendDefaults.maxNumMultiHashPartition}
|
disabled={subPartitionKeys.length >= Constants.BackendDefaults.maxNumMultiHashPartition}
|
||||||
onClick={() => setSubPartitionKeys([...subPartitionKeys, ""])}
|
onClick={() => setSubPartitionKeys([...subPartitionKeys, ""])}
|
||||||
@@ -346,7 +352,11 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
|
|||||||
Add hierarchical partition key
|
Add hierarchical partition key
|
||||||
</DefaultButton>
|
</DefaultButton>
|
||||||
{subPartitionKeys.length > 0 && (
|
{subPartitionKeys.length > 0 && (
|
||||||
<Text variant="small" style={{ color: "var(--colorNeutralForeground1)" }}>
|
<Text
|
||||||
|
data-test="hierarchical-partitioning-info-text"
|
||||||
|
variant="small"
|
||||||
|
style={{ color: "var(--colorNeutralForeground1)" }}
|
||||||
|
>
|
||||||
<Icon iconName="InfoSolid" className="removeIcon" tabIndex={0} /> This feature allows you to
|
<Icon iconName="InfoSolid" className="removeIcon" tabIndex={0} /> This feature allows you to
|
||||||
partition your data with up to three levels of keys for better data distribution. Requires .NET V3,
|
partition your data with up to three levels of keys for better data distribution. Requires .NET V3,
|
||||||
Java V4 SDK, or preview JavaScript V3 SDK.{" "}
|
Java V4 SDK, or preview JavaScript V3 SDK.{" "}
|
||||||
@@ -359,7 +369,7 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
|
|||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
) : (
|
) : (
|
||||||
<Stack>
|
<Stack data-test="use-existing-container-form">
|
||||||
<Stack horizontal>
|
<Stack horizontal>
|
||||||
<span className="mandatoryStar">* </span>
|
<span className="mandatoryStar">* </span>
|
||||||
<Text className="panelTextBold" variant="small">
|
<Text className="panelTextBold" variant="small">
|
||||||
@@ -390,6 +400,7 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
|
|||||||
}}
|
}}
|
||||||
defaultSelectedKey={targetCollectionId}
|
defaultSelectedKey={targetCollectionId}
|
||||||
responsiveMode={999}
|
responsiveMode={999}
|
||||||
|
ariaLabel="Existing Containers"
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -61,7 +61,8 @@ const useStyles = makeStyles({
|
|||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
minHeight: "100vh",
|
height: "100%",
|
||||||
|
overflowY: "auto",
|
||||||
backgroundColor: "var(--colorNeutralBackground1)",
|
backgroundColor: "var(--colorNeutralBackground1)",
|
||||||
color: "var(--colorNeutralForeground1)",
|
color: "var(--colorNeutralForeground1)",
|
||||||
},
|
},
|
||||||
@@ -73,20 +74,19 @@ const useStyles = makeStyles({
|
|||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
fontSize: "48px",
|
fontSize: "48px",
|
||||||
fontWeight: "500",
|
fontWeight: "400",
|
||||||
margin: "16px auto",
|
margin: "16px auto",
|
||||||
color: "var(--colorNeutralForeground1)",
|
color: "var(--colorNeutralForeground1)",
|
||||||
},
|
},
|
||||||
subtitle: {
|
subtitle: {
|
||||||
fontSize: "18px",
|
fontSize: "18px",
|
||||||
marginBottom: "40px",
|
|
||||||
color: "var(--colorNeutralForeground2)",
|
color: "var(--colorNeutralForeground2)",
|
||||||
},
|
},
|
||||||
cardContainer: {
|
cardContainer: {
|
||||||
display: "grid",
|
display: "grid",
|
||||||
gridTemplateColumns: "repeat(2, 1fr)",
|
gridTemplateColumns: "repeat(2, 1fr)",
|
||||||
gap: "16px",
|
gap: "16px",
|
||||||
width: "66%",
|
width: "60%",
|
||||||
margin: "0 auto",
|
margin: "0 auto",
|
||||||
backgroundColor: "var(--colorNeutralBackground1)",
|
backgroundColor: "var(--colorNeutralBackground1)",
|
||||||
color: "var(--colorNeutralForeground1)",
|
color: "var(--colorNeutralForeground1)",
|
||||||
@@ -100,7 +100,7 @@ const useStyles = makeStyles({
|
|||||||
color: "var(--colorNeutralForeground1)",
|
color: "var(--colorNeutralForeground1)",
|
||||||
border: "1px solid var(--colorNeutralStroke1)",
|
border: "1px solid var(--colorNeutralStroke1)",
|
||||||
borderRadius: "4px",
|
borderRadius: "4px",
|
||||||
boxShadow: "var(--shadow4)",
|
boxShadow: "rgba(0, 0, 0, 0.25) 0px 4px 4px",
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
minHeight: "150px",
|
minHeight: "150px",
|
||||||
"&:hover": {
|
"&:hover": {
|
||||||
@@ -128,11 +128,10 @@ const useStyles = makeStyles({
|
|||||||
textAlign: "left",
|
textAlign: "left",
|
||||||
},
|
},
|
||||||
moreStuffContainer: {
|
moreStuffContainer: {
|
||||||
display: "grid",
|
display: "flex",
|
||||||
gridTemplateColumns: "repeat(3, 1fr)",
|
justifyContent: "space-between",
|
||||||
gap: "32px",
|
gap: "32px",
|
||||||
width: "66%",
|
width: "90%",
|
||||||
margin: "40px auto",
|
|
||||||
},
|
},
|
||||||
moreStuffColumn: {
|
moreStuffColumn: {
|
||||||
display: "flex",
|
display: "flex",
|
||||||
@@ -227,7 +226,7 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
|
|||||||
return (
|
return (
|
||||||
<Stack
|
<Stack
|
||||||
className="splashStackContainer"
|
className="splashStackContainer"
|
||||||
style={{ width: "66%", cursor: "pointer", margin: "40px auto" }}
|
style={{ width: "60%", cursor: "pointer", margin: "40px auto" }}
|
||||||
tokens={{ childrenGap: 16 }}
|
tokens={{ childrenGap: 16 }}
|
||||||
>
|
>
|
||||||
<Stack className="splashStackRow" horizontal>
|
<Stack className="splashStackRow" horizontal>
|
||||||
@@ -903,9 +902,9 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
|
|||||||
return (
|
return (
|
||||||
<div className={styles.splashScreenContainer}>
|
<div className={styles.splashScreenContainer}>
|
||||||
<div className={styles.splashScreen}>
|
<div className={styles.splashScreen}>
|
||||||
<h1 className={styles.title} role="heading" aria-label="Welcome to Azure Cosmos DB">
|
<h2 className={styles.title} role="heading" aria-label="Welcome to Azure Cosmos DB">
|
||||||
Welcome to Azure Cosmos DB<span className="activePatch"></span>
|
Welcome to Azure Cosmos DB<span className="activePatch"></span>
|
||||||
</h1>
|
</h2>
|
||||||
<div className={styles.subtitle}>Globally distributed, multi-model database service for any scale</div>
|
<div className={styles.subtitle}>Globally distributed, multi-model database service for any scale</div>
|
||||||
{getSplashScreenButtons()}
|
{getSplashScreenButtons()}
|
||||||
{useCarousel.getState().showCoachMark && (
|
{useCarousel.getState().showCoachMark && (
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const useStyles = makeStyles({
|
|||||||
button: {
|
button: {
|
||||||
border: "1px solid var(--colorNeutralStroke1)",
|
border: "1px solid var(--colorNeutralStroke1)",
|
||||||
boxSizing: "border-box",
|
boxSizing: "border-box",
|
||||||
boxShadow: "var(--shadow4)",
|
boxShadow: "rgba(0, 0, 0, 0.25) 0px 4px 4px",
|
||||||
borderRadius: "4px",
|
borderRadius: "4px",
|
||||||
padding: "32px 16px",
|
padding: "32px 16px",
|
||||||
backgroundColor: "var(--colorNeutralBackground1)",
|
backgroundColor: "var(--colorNeutralBackground1)",
|
||||||
|
|||||||
@@ -125,7 +125,10 @@ const App = (): JSX.Element => {
|
|||||||
<KeyboardShortcutRoot>
|
<KeyboardShortcutRoot>
|
||||||
<div className="flexContainer" aria-hidden="false">
|
<div className="flexContainer" aria-hidden="false">
|
||||||
{userContext.features.enableContainerCopy && userContext.apiType === "SQL" ? (
|
{userContext.features.enableContainerCopy && userContext.apiType === "SQL" ? (
|
||||||
|
<>
|
||||||
<ContainerCopyPanel explorer={explorer} />
|
<ContainerCopyPanel explorer={explorer} />
|
||||||
|
<SidePanel />
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<DivExplorer explorer={explorer} />
|
<DivExplorer explorer={explorer} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -164,6 +164,9 @@ $ENV:NOSQL_TESTACCOUNT_TOKEN=az account get-access-token --scope "https://<accou
|
|||||||
# NoSQL API (Readonly)
|
# NoSQL API (Readonly)
|
||||||
$ENV:NOSQL_READONLY_TESTACCOUNT_TOKEN=az account get-access-token --scope "https://<account name>.documents.azure.com/.default" -o tsv --query accessToken
|
$ENV:NOSQL_READONLY_TESTACCOUNT_TOKEN=az account get-access-token --scope "https://<account name>.documents.azure.com/.default" -o tsv --query accessToken
|
||||||
|
|
||||||
|
# NoSQL API (Container Copy)
|
||||||
|
$ENV:NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN=az account get-access-token --scope "https://<account name>.documents.azure.com/.default" -o tsv --query accessToken
|
||||||
|
|
||||||
# Tables API
|
# Tables API
|
||||||
$ENV:TABLE_TESTACCOUNT_TOKEN=az account get-access-token --scope "https://<account name>.documents.azure.com/.default" -o tsv --query accessToken
|
$ENV:TABLE_TESTACCOUNT_TOKEN=az account get-access-token --scope "https://<account name>.documents.azure.com/.default" -o tsv --query accessToken
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ test("Cassandra keyspace and table CRUD", async ({ page }) => {
|
|||||||
|
|
||||||
const explorer = await DataExplorer.open(page, TestAccount.Cassandra);
|
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(
|
await explorer.whilePanelOpen(
|
||||||
"Add Table",
|
"Add Table",
|
||||||
async (panel, okButton) => {
|
async (panel, okButton) => {
|
||||||
|
|||||||
222
test/fx.ts
222
test/fx.ts
@@ -11,7 +11,7 @@ export interface TestNameOptions {
|
|||||||
prefixed?: boolean;
|
prefixed?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function generateUniqueName(baseName, options?: TestNameOptions): string {
|
export function generateUniqueName(baseName: string, options?: TestNameOptions): string {
|
||||||
const length = options?.length ?? 1;
|
const length = options?.length ?? 1;
|
||||||
const timestamp = options?.timestampped === undefined ? true : options.timestampped;
|
const timestamp = options?.timestampped === undefined ? true : options.timestampped;
|
||||||
const prefixed = options?.prefixed === undefined ? true : options.prefixed;
|
const prefixed = options?.prefixed === undefined ? true : options.prefixed;
|
||||||
@@ -40,6 +40,7 @@ export enum TestAccount {
|
|||||||
Mongo32 = "Mongo32",
|
Mongo32 = "Mongo32",
|
||||||
SQL = "SQL",
|
SQL = "SQL",
|
||||||
SQLReadOnly = "SQLReadOnly",
|
SQLReadOnly = "SQLReadOnly",
|
||||||
|
SQLContainerCopyOnly = "SQLContainerCopyOnly",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultAccounts: Record<TestAccount, string> = {
|
export const defaultAccounts: Record<TestAccount, string> = {
|
||||||
@@ -51,6 +52,7 @@ export const defaultAccounts: Record<TestAccount, string> = {
|
|||||||
[TestAccount.Mongo32]: "github-e2etests-mongo32",
|
[TestAccount.Mongo32]: "github-e2etests-mongo32",
|
||||||
[TestAccount.SQL]: "github-e2etests-sql",
|
[TestAccount.SQL]: "github-e2etests-sql",
|
||||||
[TestAccount.SQLReadOnly]: "github-e2etests-sql-readonly",
|
[TestAccount.SQLReadOnly]: "github-e2etests-sql-readonly",
|
||||||
|
[TestAccount.SQLContainerCopyOnly]: "github-e2etests-sql-containercopyonly",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const resourceGroupName = process.env.DE_TEST_RESOURCE_GROUP ?? "de-e2e-tests";
|
export const resourceGroupName = process.env.DE_TEST_RESOURCE_GROUP ?? "de-e2e-tests";
|
||||||
@@ -77,7 +79,14 @@ export function getAccountName(accountType: TestAccount) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getTestExplorerUrl(accountType: TestAccount, iframeSrc?: string): Promise<string> {
|
type TestExplorerUrlOptions = {
|
||||||
|
iframeSrc?: string;
|
||||||
|
enablecontainercopy?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function getTestExplorerUrl(accountType: TestAccount, options?: TestExplorerUrlOptions): Promise<string> {
|
||||||
|
const { iframeSrc, enablecontainercopy } = options ?? {};
|
||||||
|
|
||||||
// We can't retrieve AZ CLI credentials from the browser so we get them here.
|
// We can't retrieve AZ CLI credentials from the browser so we get them here.
|
||||||
const token = await getAzureCLICredentialsToken();
|
const token = await getAzureCLICredentialsToken();
|
||||||
const accountName = getAccountName(accountType);
|
const accountName = getAccountName(accountType);
|
||||||
@@ -93,6 +102,7 @@ export async function getTestExplorerUrl(accountType: TestAccount, iframeSrc?: s
|
|||||||
|
|
||||||
const nosqlRbacToken = process.env.NOSQL_TESTACCOUNT_TOKEN;
|
const nosqlRbacToken = process.env.NOSQL_TESTACCOUNT_TOKEN;
|
||||||
const nosqlReadOnlyRbacToken = process.env.NOSQL_READONLY_TESTACCOUNT_TOKEN;
|
const nosqlReadOnlyRbacToken = process.env.NOSQL_READONLY_TESTACCOUNT_TOKEN;
|
||||||
|
const nosqlContainerCopyRbacToken = process.env.NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN;
|
||||||
const tableRbacToken = process.env.TABLE_TESTACCOUNT_TOKEN;
|
const tableRbacToken = process.env.TABLE_TESTACCOUNT_TOKEN;
|
||||||
const gremlinRbacToken = process.env.GREMLIN_TESTACCOUNT_TOKEN;
|
const gremlinRbacToken = process.env.GREMLIN_TESTACCOUNT_TOKEN;
|
||||||
const cassandraRbacToken = process.env.CASSANDRA_TESTACCOUNT_TOKEN;
|
const cassandraRbacToken = process.env.CASSANDRA_TESTACCOUNT_TOKEN;
|
||||||
@@ -108,6 +118,16 @@ export async function getTestExplorerUrl(accountType: TestAccount, iframeSrc?: s
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case TestAccount.SQLContainerCopyOnly:
|
||||||
|
if (nosqlContainerCopyRbacToken) {
|
||||||
|
params.set("nosqlRbacToken", nosqlContainerCopyRbacToken);
|
||||||
|
params.set("enableaaddataplane", "true");
|
||||||
|
}
|
||||||
|
if (enablecontainercopy) {
|
||||||
|
params.set("enablecontainercopy", "true");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
case TestAccount.SQLReadOnly:
|
case TestAccount.SQLReadOnly:
|
||||||
if (nosqlReadOnlyRbacToken) {
|
if (nosqlReadOnlyRbacToken) {
|
||||||
params.set("nosqlReadOnlyRbacToken", nosqlReadOnlyRbacToken);
|
params.set("nosqlReadOnlyRbacToken", nosqlReadOnlyRbacToken);
|
||||||
@@ -165,6 +185,39 @@ export async function getTestExplorerUrl(accountType: TestAccount, iframeSrc?: s
|
|||||||
return `https://localhost:1234/testExplorer.html?${params.toString()}`;
|
return `https://localhost:1234/testExplorer.html?${params.toString()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DropdownItemExpectations = {
|
||||||
|
ariaLabel?: string;
|
||||||
|
itemCount?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type DropdownItemMatcher = {
|
||||||
|
name?: string;
|
||||||
|
position?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function getDropdownItemByNameOrPosition(
|
||||||
|
frame: Frame,
|
||||||
|
matcher?: DropdownItemMatcher,
|
||||||
|
expectedOptions?: DropdownItemExpectations,
|
||||||
|
): Promise<Locator> {
|
||||||
|
const dropdownItemsWrapper = frame.locator("div.ms-Dropdown-items");
|
||||||
|
if (expectedOptions?.ariaLabel) {
|
||||||
|
expect(await dropdownItemsWrapper.getAttribute("aria-label")).toEqual(expectedOptions.ariaLabel);
|
||||||
|
}
|
||||||
|
if (expectedOptions?.itemCount) {
|
||||||
|
const items = dropdownItemsWrapper.locator("button.ms-Dropdown-item[role='option']");
|
||||||
|
await expect(items).toHaveCount(expectedOptions.itemCount);
|
||||||
|
}
|
||||||
|
const containerDropdownItems = dropdownItemsWrapper.locator("button.ms-Dropdown-item[role='option']");
|
||||||
|
if (matcher?.name) {
|
||||||
|
return containerDropdownItems.filter({ hasText: matcher.name });
|
||||||
|
} else if (matcher?.position !== undefined) {
|
||||||
|
return containerDropdownItems.nth(matcher.position);
|
||||||
|
}
|
||||||
|
// Return first item if no matcher is provided
|
||||||
|
return containerDropdownItems.first();
|
||||||
|
}
|
||||||
|
|
||||||
/** Helper class that provides locator methods for TreeNode elements, on top of a Locator */
|
/** Helper class that provides locator methods for TreeNode elements, on top of a Locator */
|
||||||
class TreeNode {
|
class TreeNode {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -352,8 +405,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.
|
* 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 {
|
async globalCommandButton(label: string): Promise<Locator> {
|
||||||
return this.frame.getByTestId("GlobalCommands").getByText(label);
|
await this.frame.getByTestId("GlobalCommands").click();
|
||||||
|
return this.frame.getByRole("menuitem", { name: label });
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Select the command bar button with the specified label */
|
/** Select the command bar button with the specified label */
|
||||||
@@ -459,6 +513,15 @@ export class DataExplorer {
|
|||||||
const containerNode = await this.waitForContainerNode(context.database.id, context.container.id);
|
const containerNode = await this.waitForContainerNode(context.database.id, context.container.id);
|
||||||
await containerNode.expand();
|
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(
|
const scaleAndSettingsButton = this.frame.getByTestId(
|
||||||
`TreeNode:${context.database.id}/${context.container.id}/Scale & Settings`,
|
`TreeNode:${context.database.id}/${context.container.id}/Scale & Settings`,
|
||||||
);
|
);
|
||||||
@@ -466,12 +529,46 @@ export class DataExplorer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Gets the console message element */
|
/** Gets the console message element */
|
||||||
getConsoleMessage(): Locator {
|
getConsoleHeaderStatus(): Locator {
|
||||||
return this.frame.getByTestId("notification-console/header-status");
|
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 */
|
/** Waits for the Data Explorer app to load */
|
||||||
static async waitForExplorer(page: Page) {
|
static async waitForExplorer(page: Page, options?: TestExplorerUrlOptions): Promise<DataExplorer> {
|
||||||
const iframeElement = await page.getByTestId("DataExplorerFrame").elementHandle();
|
const iframeElement = await page.getByTestId("DataExplorerFrame").elementHandle();
|
||||||
if (iframeElement === null) {
|
if (iframeElement === null) {
|
||||||
throw new Error("Explorer iframe not found");
|
throw new Error("Explorer iframe not found");
|
||||||
@@ -483,15 +580,126 @@ export class DataExplorer {
|
|||||||
throw new Error("Explorer frame not found");
|
throw new Error("Explorer frame not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!options?.enablecontainercopy) {
|
||||||
await explorerFrame?.getByTestId("DataExplorerRoot").waitFor();
|
await explorerFrame?.getByTestId("DataExplorerRoot").waitFor();
|
||||||
|
}
|
||||||
|
|
||||||
return new DataExplorer(explorerFrame);
|
return new DataExplorer(explorerFrame);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Opens the Data Explorer app using the specified test account (and optionally, the provided IFRAME src url). */
|
/** Opens the Data Explorer app using the specified test account (and optionally, the provided IFRAME src url). */
|
||||||
static async open(page: Page, testAccount: TestAccount, iframeSrc?: string): Promise<DataExplorer> {
|
static async open(page: Page, testAccount: TestAccount, iframeSrc?: string): Promise<DataExplorer> {
|
||||||
const url = await getTestExplorerUrl(testAccount, iframeSrc);
|
const url = await getTestExplorerUrl(testAccount, { iframeSrc });
|
||||||
await page.goto(url);
|
await page.goto(url);
|
||||||
return DataExplorer.waitForExplorer(page);
|
return DataExplorer.waitForExplorer(page);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function waitForApiResponse(
|
||||||
|
page: Page,
|
||||||
|
urlPattern: string,
|
||||||
|
method?: string,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
payloadValidator?: (payload: any) => boolean,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
// Check if page is still valid before waiting
|
||||||
|
if (page.isClosed()) {
|
||||||
|
throw new Error(`Page is closed, cannot wait for API response: ${urlPattern}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return page.waitForResponse(
|
||||||
|
async (response) => {
|
||||||
|
const request = response.request();
|
||||||
|
|
||||||
|
if (!request.url().includes(urlPattern)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (method && request.method() !== method) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payloadValidator && (request.method() === "POST" || request.method() === "PUT")) {
|
||||||
|
const postData = request.postData();
|
||||||
|
if (postData) {
|
||||||
|
try {
|
||||||
|
const payload = JSON.parse(postData);
|
||||||
|
return payloadValidator(payload);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{ timeout: 60 * 1000 },
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error && error.message.includes("Target page, context or browser has been closed")) {
|
||||||
|
console.warn("Page was closed while waiting for API response:", urlPattern);
|
||||||
|
throw new Error(`Page closed while waiting for API response: ${urlPattern}`);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export async function interceptAndInspectApiRequest(
|
||||||
|
page: Page,
|
||||||
|
urlPattern: string,
|
||||||
|
method: string = "POST",
|
||||||
|
error: Error,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
errorValidator: (url?: string, payload?: any) => boolean,
|
||||||
|
): Promise<void> {
|
||||||
|
await page.route(
|
||||||
|
(url) => url.pathname.includes(urlPattern),
|
||||||
|
async (route, request) => {
|
||||||
|
if (request.method() !== method) {
|
||||||
|
await route.continue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const postData = request.postData();
|
||||||
|
if (postData) {
|
||||||
|
try {
|
||||||
|
const payload = JSON.parse(postData);
|
||||||
|
if (errorValidator && errorValidator(request.url(), payload)) {
|
||||||
|
await route.fulfill({
|
||||||
|
status: 409,
|
||||||
|
contentType: "application/json",
|
||||||
|
body: JSON.stringify({
|
||||||
|
code: "Conflict",
|
||||||
|
message: error.message,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof Error && err.message.includes("not allowed")) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await route.continue();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ContainerCopy {
|
||||||
|
constructor(
|
||||||
|
public frame: Frame,
|
||||||
|
public wrapper: Locator,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
static async waitForContainerCopy(page: Page): Promise<ContainerCopy> {
|
||||||
|
const explorerFrame = await DataExplorer.waitForExplorer(page, { enablecontainercopy: true });
|
||||||
|
const containerCopyWrapper = explorerFrame.frame.locator("div#containerCopyWrapper");
|
||||||
|
return new ContainerCopy(explorerFrame.frame, containerCopyWrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async open(page: Page, testAccount: TestAccount, iframeSrc?: string): Promise<ContainerCopy> {
|
||||||
|
const url = await getTestExplorerUrl(testAccount, { iframeSrc, enablecontainercopy: true });
|
||||||
|
await page.goto(url);
|
||||||
|
return ContainerCopy.waitForContainerCopy(page);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ test("Gremlin graph CRUD", async ({ page }) => {
|
|||||||
const explorer = await DataExplorer.open(page, TestAccount.Gremlin);
|
const explorer = await DataExplorer.open(page, TestAccount.Gremlin);
|
||||||
|
|
||||||
// Create new database and graph
|
// Create new database and graph
|
||||||
await explorer.globalCommandButton("New Graph").click();
|
const newGraphButton = await explorer.globalCommandButton("New Graph");
|
||||||
|
await newGraphButton.click();
|
||||||
await explorer.whilePanelOpen(
|
await explorer.whilePanelOpen(
|
||||||
"New Graph",
|
"New Graph",
|
||||||
async (panel, okButton) => {
|
async (panel, okButton) => {
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ import { DataExplorer, TEST_AUTOSCALE_THROUGHPUT_RU, TestAccount, generateUnique
|
|||||||
|
|
||||||
const explorer = await DataExplorer.open(page, accountType);
|
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(
|
await explorer.whilePanelOpen(
|
||||||
"New Collection",
|
"New Collection",
|
||||||
async (panel, okButton) => {
|
async (panel, okButton) => {
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ test("SQL database and container CRUD", async ({ page }) => {
|
|||||||
|
|
||||||
const explorer = await DataExplorer.open(page, TestAccount.SQL);
|
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(
|
await explorer.whilePanelOpen(
|
||||||
"New Container",
|
"New Container",
|
||||||
async (panel, okButton) => {
|
async (panel, okButton) => {
|
||||||
|
|||||||
651
test/sql/containercopy.spec.ts
Normal file
651
test/sql/containercopy.spec.ts
Normal file
@@ -0,0 +1,651 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import { expect, Frame, Locator, Page, test } from "@playwright/test";
|
||||||
|
import { set } from "lodash";
|
||||||
|
import { truncateName } from "../../src/Explorer/ContainerCopy/CopyJobUtils";
|
||||||
|
import {
|
||||||
|
ContainerCopy,
|
||||||
|
getAccountName,
|
||||||
|
getDropdownItemByNameOrPosition,
|
||||||
|
interceptAndInspectApiRequest,
|
||||||
|
TestAccount,
|
||||||
|
waitForApiResponse,
|
||||||
|
} from "../fx";
|
||||||
|
import { createMultipleTestContainers } from "../testData";
|
||||||
|
|
||||||
|
let page: Page;
|
||||||
|
let wrapper: Locator = null!;
|
||||||
|
let panel: Locator = null!;
|
||||||
|
let frame: Frame = null!;
|
||||||
|
let expectedCopyJobNameInitial: string = null!;
|
||||||
|
let expectedJobName: string = "";
|
||||||
|
let targetAccountName: string = "";
|
||||||
|
let expectedSourceAccountName: string = "";
|
||||||
|
let expectedSubscriptionName: string = "";
|
||||||
|
|
||||||
|
test.beforeAll("Container Copy - Before All", async ({ browser }) => {
|
||||||
|
await createMultipleTestContainers({ accountType: TestAccount.SQLContainerCopyOnly, containerCount: 3 });
|
||||||
|
|
||||||
|
page = await browser.newPage();
|
||||||
|
({ wrapper, frame } = await ContainerCopy.open(page, TestAccount.SQLContainerCopyOnly));
|
||||||
|
expectedJobName = `test_job_${Date.now()}`;
|
||||||
|
targetAccountName = getAccountName(TestAccount.SQLContainerCopyOnly);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.afterEach("Container Copy - After Each", async () => {
|
||||||
|
await page.unroute(/.*/, (route) => route.continue());
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Loading and verifying the content of the page", async () => {
|
||||||
|
expect(wrapper).not.toBeNull();
|
||||||
|
await expect(wrapper.getByTestId("CommandBar/Button:Create Copy Job")).toBeVisible();
|
||||||
|
await expect(wrapper.getByTestId("CommandBar/Button:Refresh")).toBeVisible();
|
||||||
|
await expect(wrapper.getByTestId("CommandBar/Button:Feedback")).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Successfully create a copy job for offline migration", async () => {
|
||||||
|
expect(wrapper).not.toBeNull();
|
||||||
|
// Loading and verifying subscription & account dropdown
|
||||||
|
|
||||||
|
const createCopyJobButton = wrapper.getByTestId("CommandBar/Button:Create Copy Job");
|
||||||
|
await createCopyJobButton.click();
|
||||||
|
panel = frame.getByTestId("Panel:Create copy job");
|
||||||
|
await expect(panel).toBeVisible();
|
||||||
|
|
||||||
|
await page.waitForTimeout(10 * 1000);
|
||||||
|
|
||||||
|
const subscriptionDropdown = panel.getByTestId("subscription-dropdown");
|
||||||
|
|
||||||
|
const expectedAccountName = targetAccountName;
|
||||||
|
expectedSubscriptionName = await subscriptionDropdown.locator("span.ms-Dropdown-title").innerText();
|
||||||
|
|
||||||
|
await subscriptionDropdown.click();
|
||||||
|
const subscriptionItem = await getDropdownItemByNameOrPosition(
|
||||||
|
frame,
|
||||||
|
{ name: expectedSubscriptionName },
|
||||||
|
{ ariaLabel: "Subscription" },
|
||||||
|
);
|
||||||
|
await subscriptionItem.click();
|
||||||
|
|
||||||
|
// Load account dropdown based on selected subscription
|
||||||
|
|
||||||
|
const accountDropdown = panel.getByTestId("account-dropdown");
|
||||||
|
await expect(accountDropdown).toHaveText(new RegExp(expectedAccountName));
|
||||||
|
await accountDropdown.click();
|
||||||
|
|
||||||
|
const accountItem = await getDropdownItemByNameOrPosition(
|
||||||
|
frame,
|
||||||
|
{ name: expectedAccountName },
|
||||||
|
{ ariaLabel: "Account" },
|
||||||
|
);
|
||||||
|
await accountItem.click();
|
||||||
|
|
||||||
|
// Verifying online or offline checkbox functionality
|
||||||
|
/**
|
||||||
|
* This test verifies the functionality of the migration type checkbox that toggles between
|
||||||
|
* online and offline container copy modes. It ensures that:
|
||||||
|
* 1. When online mode is selected, the user is directed to a permissions screen
|
||||||
|
* 2. When offline mode is selected, the user bypasses the permissions screen
|
||||||
|
* 3. The UI correctly reflects the selected migration type throughout the workflow
|
||||||
|
*/
|
||||||
|
const fluentUiCheckboxContainer = panel.getByTestId("migration-type-checkbox").locator("div.ms-Checkbox");
|
||||||
|
await fluentUiCheckboxContainer.click();
|
||||||
|
await panel.getByRole("button", { name: "Next" }).click();
|
||||||
|
await expect(panel.getByTestId("Panel:AssignPermissionsContainer")).toBeVisible();
|
||||||
|
await expect(panel.getByText("Online container copy", { exact: true })).toBeVisible();
|
||||||
|
await panel.getByRole("button", { name: "Previous" }).click();
|
||||||
|
await fluentUiCheckboxContainer.click();
|
||||||
|
await panel.getByRole("button", { name: "Next" }).click();
|
||||||
|
await expect(panel.getByTestId("Panel:SelectSourceAndTargetContainers")).toBeVisible();
|
||||||
|
await expect(panel.getByTestId("Panel:AssignPermissionsContainer")).not.toBeVisible();
|
||||||
|
|
||||||
|
// Verifying source and target container selection
|
||||||
|
|
||||||
|
const sourceContainerDropdown = panel.getByTestId("source-containerDropdown");
|
||||||
|
expect(sourceContainerDropdown).toBeVisible();
|
||||||
|
await expect(sourceContainerDropdown).toHaveClass(/(^|\s)is-disabled(\s|$)/);
|
||||||
|
|
||||||
|
const sourceDatabaseDropdown = panel.getByTestId("source-databaseDropdown");
|
||||||
|
await sourceDatabaseDropdown.click();
|
||||||
|
|
||||||
|
const sourceDbDropdownItem = await getDropdownItemByNameOrPosition(frame, { position: 0 }, { ariaLabel: "Database" });
|
||||||
|
await sourceDbDropdownItem.click();
|
||||||
|
|
||||||
|
await expect(sourceContainerDropdown).not.toHaveClass(/(^|\s)is-disabled(\s|$)/);
|
||||||
|
await sourceContainerDropdown.click();
|
||||||
|
const sourceContainerDropdownItem = await getDropdownItemByNameOrPosition(
|
||||||
|
frame,
|
||||||
|
{ position: 0 },
|
||||||
|
{ ariaLabel: "Container" },
|
||||||
|
);
|
||||||
|
await sourceContainerDropdownItem.click();
|
||||||
|
|
||||||
|
const targetContainerDropdown = panel.getByTestId("target-containerDropdown");
|
||||||
|
expect(targetContainerDropdown).toBeVisible();
|
||||||
|
await expect(targetContainerDropdown).toHaveClass(/(^|\s)is-disabled(\s|$)/);
|
||||||
|
|
||||||
|
const targetDatabaseDropdown = panel.getByTestId("target-databaseDropdown");
|
||||||
|
await targetDatabaseDropdown.click();
|
||||||
|
const targetDbDropdownItem = await getDropdownItemByNameOrPosition(frame, { position: 0 }, { ariaLabel: "Database" });
|
||||||
|
await targetDbDropdownItem.click();
|
||||||
|
|
||||||
|
await expect(targetContainerDropdown).not.toHaveClass(/(^|\s)is-disabled(\s|$)/);
|
||||||
|
await targetContainerDropdown.click();
|
||||||
|
const targetContainerDropdownItem1 = await getDropdownItemByNameOrPosition(
|
||||||
|
frame,
|
||||||
|
{ position: 0 },
|
||||||
|
{ ariaLabel: "Container" },
|
||||||
|
);
|
||||||
|
await targetContainerDropdownItem1.click();
|
||||||
|
|
||||||
|
await panel.getByRole("button", { name: "Next" }).click();
|
||||||
|
|
||||||
|
const errorContainer = panel.getByTestId("Panel:ErrorContainer");
|
||||||
|
await expect(errorContainer).toBeVisible();
|
||||||
|
await expect(errorContainer).toHaveText(/Source and destination containers cannot be the same/i);
|
||||||
|
|
||||||
|
// Reselect target container to be different from source container
|
||||||
|
await targetContainerDropdown.click();
|
||||||
|
const targetContainerDropdownItem2 = await getDropdownItemByNameOrPosition(
|
||||||
|
frame,
|
||||||
|
{ position: 1 },
|
||||||
|
{ ariaLabel: "Container" },
|
||||||
|
);
|
||||||
|
await targetContainerDropdownItem2.click();
|
||||||
|
|
||||||
|
const selectedSourceDatabase = await sourceDatabaseDropdown.innerText();
|
||||||
|
const selectedSourceContainer = await sourceContainerDropdown.innerText();
|
||||||
|
const selectedTargetDatabase = await targetDatabaseDropdown.innerText();
|
||||||
|
const selectedTargetContainer = await targetContainerDropdown.innerText();
|
||||||
|
expectedCopyJobNameInitial = `${truncateName(selectedSourceDatabase)}.${truncateName(
|
||||||
|
selectedSourceContainer,
|
||||||
|
)}_${truncateName(selectedTargetDatabase)}.${truncateName(selectedTargetContainer)}`;
|
||||||
|
|
||||||
|
await panel.getByRole("button", { name: "Next" }).click();
|
||||||
|
|
||||||
|
await expect(errorContainer).not.toBeVisible();
|
||||||
|
await expect(panel.getByTestId("Panel:PreviewCopyJob")).toBeVisible();
|
||||||
|
|
||||||
|
// Verifying the preview of the copy job
|
||||||
|
const previewContainer = panel.getByTestId("Panel:PreviewCopyJob");
|
||||||
|
await expect(previewContainer).toBeVisible();
|
||||||
|
await expect(previewContainer.getByTestId("source-subscription-name")).toHaveText(expectedSubscriptionName);
|
||||||
|
await expect(previewContainer.getByTestId("source-account-name")).toHaveText(expectedAccountName);
|
||||||
|
const jobNameInput = previewContainer.getByTestId("job-name-textfield");
|
||||||
|
await expect(jobNameInput).toHaveValue(new RegExp(expectedCopyJobNameInitial));
|
||||||
|
const primaryBtn = panel.getByRole("button", { name: "Copy", exact: true });
|
||||||
|
await expect(primaryBtn).not.toHaveClass(/(^|\s)is-disabled(\s|$)/);
|
||||||
|
|
||||||
|
await jobNameInput.fill("test job name");
|
||||||
|
await expect(primaryBtn).toHaveClass(/(^|\s)is-disabled(\s|$)/);
|
||||||
|
|
||||||
|
// Testing API request interception with duplicate job name
|
||||||
|
const duplicateJobName = "test-job-name-1";
|
||||||
|
await jobNameInput.fill(duplicateJobName);
|
||||||
|
|
||||||
|
const copyButton = panel.getByRole("button", { name: "Copy", exact: true });
|
||||||
|
const expectedErrorMessage = `Duplicate job name '${duplicateJobName}'`;
|
||||||
|
await interceptAndInspectApiRequest(
|
||||||
|
page,
|
||||||
|
`${expectedAccountName}/dataTransferJobs/${duplicateJobName}`,
|
||||||
|
"PUT",
|
||||||
|
new Error(expectedErrorMessage),
|
||||||
|
(url?: string) => url?.includes(duplicateJobName) ?? false,
|
||||||
|
);
|
||||||
|
|
||||||
|
let errorThrown = false;
|
||||||
|
try {
|
||||||
|
await copyButton.click();
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
} catch (error: any) {
|
||||||
|
errorThrown = true;
|
||||||
|
expect(error.message).toContain("not allowed");
|
||||||
|
}
|
||||||
|
if (!errorThrown) {
|
||||||
|
const errorContainer = panel.getByTestId("Panel:ErrorContainer");
|
||||||
|
await expect(errorContainer).toBeVisible();
|
||||||
|
await expect(errorContainer).toHaveText(new RegExp(expectedErrorMessage, "i"));
|
||||||
|
}
|
||||||
|
|
||||||
|
await expect(panel).toBeVisible();
|
||||||
|
|
||||||
|
// Testing API request success with valid job name and verifying copy job creation
|
||||||
|
|
||||||
|
const validJobName = expectedJobName;
|
||||||
|
|
||||||
|
const copyJobCreationPromise = waitForApiResponse(
|
||||||
|
page,
|
||||||
|
`${expectedAccountName}/dataTransferJobs/${validJobName}`,
|
||||||
|
"PUT",
|
||||||
|
);
|
||||||
|
|
||||||
|
await jobNameInput.fill(validJobName);
|
||||||
|
await expect(copyButton).not.toHaveClass(/(^|\s)is-disabled(\s|$)/);
|
||||||
|
|
||||||
|
await copyButton.click();
|
||||||
|
|
||||||
|
const response = await copyJobCreationPromise;
|
||||||
|
expect(response.ok()).toBe(true);
|
||||||
|
|
||||||
|
await expect(panel).not.toBeVisible({ timeout: 10000 });
|
||||||
|
|
||||||
|
const jobsListContainer = wrapper.locator(".CopyJobListContainer .ms-DetailsList-contentWrapper .ms-List-page");
|
||||||
|
await jobsListContainer.waitFor({ state: "visible" });
|
||||||
|
|
||||||
|
const jobItem = jobsListContainer.getByText(validJobName);
|
||||||
|
await jobItem.waitFor({ state: "visible" });
|
||||||
|
await expect(jobItem).toBeVisible();
|
||||||
|
|
||||||
|
/* // Cancel the created job to clean up
|
||||||
|
|
||||||
|
// Rapid polling to catch the job in running state
|
||||||
|
let attempts = 0;
|
||||||
|
const maxAttempts = 50; // Try for ~5 seconds
|
||||||
|
let jobCancelled = false;
|
||||||
|
|
||||||
|
while (attempts < maxAttempts && !jobCancelled) {
|
||||||
|
try {
|
||||||
|
// Look for the job row
|
||||||
|
const jobRow = jobsListContainer.locator(".ms-DetailsRow", { hasText: validJobName });
|
||||||
|
|
||||||
|
if (await jobRow.isVisible({ timeout: 100 })) {
|
||||||
|
const statusCell = jobRow.locator("[data-automationid='DetailsRowCell'][data-automation-key='CopyJobStatus']");
|
||||||
|
const statusText = await statusCell.textContent({ timeout: 100 });
|
||||||
|
|
||||||
|
// If job is still running/queued, try to cancel it
|
||||||
|
if (statusText && /running|queued|pending/i.test(statusText)) {
|
||||||
|
const actionMenuButton = wrapper.getByTestId(`CopyJobActionMenu/Button:${validJobName}`);
|
||||||
|
await actionMenuButton.click({ timeout: 1000 });
|
||||||
|
|
||||||
|
const cancelAction = frame.locator(".ms-ContextualMenu-list button:has-text('Cancel')");
|
||||||
|
if (await cancelAction.isVisible({ timeout: 1000 })) {
|
||||||
|
await cancelAction.click();
|
||||||
|
|
||||||
|
// Verify cancellation
|
||||||
|
await expect(statusCell).toContainText(/cancelled|canceled|failed/i, { timeout: 5000 });
|
||||||
|
jobCancelled = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else if (statusText && /completed|succeeded|finished/i.test(statusText)) {
|
||||||
|
// Job completed too fast, skip the test
|
||||||
|
// console.log(`Job ${validJobName} completed too quickly to test cancellation`);
|
||||||
|
test.skip(true, "Job completed too quickly for cancellation test");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh the job list
|
||||||
|
const refreshButton = wrapper.getByTestId("CommandBar/Button:Refresh");
|
||||||
|
if (await refreshButton.isVisible({ timeout: 100 })) {
|
||||||
|
await refreshButton.click();
|
||||||
|
await page.waitForTimeout(100); // Small delay between attempts
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Continue trying if there's any error
|
||||||
|
}
|
||||||
|
|
||||||
|
attempts++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!jobCancelled) {
|
||||||
|
// If we couldn't cancel in time, at least verify the job was created
|
||||||
|
const jobRow = jobsListContainer.locator(".ms-DetailsRow", { hasText: validJobName });
|
||||||
|
await expect(jobRow).toBeVisible({ timeout: 5000 });
|
||||||
|
test.skip(true, "Could not catch job in running state for cancellation test");
|
||||||
|
} */
|
||||||
|
});
|
||||||
|
|
||||||
|
/* test.skip("Pause a running copy job", async () => {
|
||||||
|
const jobsListContainer = wrapper.locator(".CopyJobListContainer .ms-DetailsList-contentWrapper .ms-List-page");
|
||||||
|
await jobsListContainer.waitFor({ state: "visible" });
|
||||||
|
|
||||||
|
const firstJobRow = jobsListContainer.locator(".ms-DetailsRow", { hasText: expectedJobName });
|
||||||
|
await firstJobRow.waitFor({ state: "visible" });
|
||||||
|
|
||||||
|
const actionMenuButton = wrapper.getByTestId(`CopyJobActionMenu/Button:${expectedJobName}`);
|
||||||
|
await actionMenuButton.waitFor({ state: "visible" });
|
||||||
|
await actionMenuButton.click();
|
||||||
|
|
||||||
|
const pauseAction = frame.locator(".ms-ContextualMenu-list button:has-text('Pause')");
|
||||||
|
await pauseAction.waitFor({ state: "visible" });
|
||||||
|
await pauseAction.click();
|
||||||
|
|
||||||
|
const updatedJobRow = jobsListContainer.locator(".ms-DetailsRow").filter({ hasText: expectedJobName });
|
||||||
|
const statusCell = updatedJobRow.locator("[data-automationid='DetailsRowCell'][data-automation-key='CopyJobStatus']");
|
||||||
|
await expect(statusCell).toContainText(/paused/i, { timeout: 10000 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test.skip("Resume a paused copy job", async () => {
|
||||||
|
const jobsListContainer = wrapper.locator(".CopyJobListContainer .ms-DetailsList-contentWrapper .ms-List-page");
|
||||||
|
await jobsListContainer.waitFor({ state: "visible" });
|
||||||
|
|
||||||
|
const pausedJobRow = jobsListContainer.locator(".ms-DetailsRow", { hasText: expectedJobName });
|
||||||
|
await pausedJobRow.waitFor({ state: "visible" });
|
||||||
|
|
||||||
|
const statusCell = pausedJobRow.locator("[data-automationid='DetailsRowCell'][data-automation-key='CopyJobStatus']");
|
||||||
|
await expect(statusCell).toContainText(/paused/i);
|
||||||
|
|
||||||
|
const actionMenuButton = wrapper.getByTestId(`CopyJobActionMenu/Button:${expectedJobName}`);
|
||||||
|
await actionMenuButton.waitFor({ state: "visible" });
|
||||||
|
await actionMenuButton.click();
|
||||||
|
|
||||||
|
const resumeAction = frame.locator(".ms-ContextualMenu-list button:has-text('Resume')");
|
||||||
|
await resumeAction.waitFor({ state: "visible" });
|
||||||
|
await resumeAction.click();
|
||||||
|
|
||||||
|
await expect(statusCell).toContainText(/running|queued/i);
|
||||||
|
}); */
|
||||||
|
|
||||||
|
test("Create and Cancel a copy job", async () => {
|
||||||
|
expect(wrapper).not.toBeNull();
|
||||||
|
// Create a new job specifically for cancellation testing
|
||||||
|
const cancelJobName = `cancel_test_job_${Date.now()}`;
|
||||||
|
|
||||||
|
// Navigate to create job panel
|
||||||
|
const createCopyJobButton = wrapper.getByTestId("CommandBar/Button:Create Copy Job");
|
||||||
|
await createCopyJobButton.click();
|
||||||
|
panel = frame.getByTestId("Panel:Create copy job");
|
||||||
|
|
||||||
|
// Skip to container selection (offline mode for faster creation)
|
||||||
|
await panel.getByRole("button", { name: "Next" }).click();
|
||||||
|
|
||||||
|
// Select source containers quickly
|
||||||
|
const sourceDatabaseDropdown = panel.getByTestId("source-databaseDropdown");
|
||||||
|
await sourceDatabaseDropdown.click();
|
||||||
|
const sourceDatabaseDropdownItem = await getDropdownItemByNameOrPosition(
|
||||||
|
frame,
|
||||||
|
{ position: 0 },
|
||||||
|
{ ariaLabel: "Database" },
|
||||||
|
);
|
||||||
|
await sourceDatabaseDropdownItem.click();
|
||||||
|
|
||||||
|
const sourceContainerDropdown = panel.getByTestId("source-containerDropdown");
|
||||||
|
await sourceContainerDropdown.click();
|
||||||
|
const sourceContainerDropdownItem = await getDropdownItemByNameOrPosition(
|
||||||
|
frame,
|
||||||
|
{ position: 0 },
|
||||||
|
{ ariaLabel: "Container" },
|
||||||
|
);
|
||||||
|
await sourceContainerDropdownItem.click();
|
||||||
|
|
||||||
|
// Select target containers
|
||||||
|
const targetDatabaseDropdown = panel.getByTestId("target-databaseDropdown");
|
||||||
|
await targetDatabaseDropdown.click();
|
||||||
|
const targetDatabaseDropdownItem = await getDropdownItemByNameOrPosition(
|
||||||
|
frame,
|
||||||
|
{ position: 0 },
|
||||||
|
{ ariaLabel: "Database" },
|
||||||
|
);
|
||||||
|
await targetDatabaseDropdownItem.click();
|
||||||
|
|
||||||
|
const targetContainerDropdown = panel.getByTestId("target-containerDropdown");
|
||||||
|
await targetContainerDropdown.click();
|
||||||
|
const targetContainerDropdownItem = await getDropdownItemByNameOrPosition(
|
||||||
|
frame,
|
||||||
|
{ position: 1 },
|
||||||
|
{ ariaLabel: "Container" },
|
||||||
|
);
|
||||||
|
await targetContainerDropdownItem.click();
|
||||||
|
|
||||||
|
await panel.getByRole("button", { name: "Next" }).click();
|
||||||
|
|
||||||
|
// Set job name and create
|
||||||
|
const jobNameInput = panel.getByTestId("job-name-textfield");
|
||||||
|
await jobNameInput.fill(cancelJobName);
|
||||||
|
|
||||||
|
const copyButton = panel.getByRole("button", { name: "Copy", exact: true });
|
||||||
|
|
||||||
|
// Create job and immediately start polling for it
|
||||||
|
await copyButton.click();
|
||||||
|
|
||||||
|
// Wait for panel to close and job list to refresh
|
||||||
|
await expect(panel).not.toBeVisible({ timeout: 10000 });
|
||||||
|
|
||||||
|
const jobsListContainer = wrapper.locator(".CopyJobListContainer .ms-DetailsList-contentWrapper .ms-List-page");
|
||||||
|
await jobsListContainer.waitFor({ state: "visible" });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Verify Online or Offline Container Copy Permissions Panel", async () => {
|
||||||
|
expect(wrapper).not.toBeNull();
|
||||||
|
|
||||||
|
// Opening the Create Copy Job panel again to verify initial state
|
||||||
|
const createCopyJobButton = wrapper.getByTestId("CommandBar/Button:Create Copy Job");
|
||||||
|
await createCopyJobButton.click();
|
||||||
|
panel = frame.getByTestId("Panel:Create copy job");
|
||||||
|
await expect(panel).toBeVisible();
|
||||||
|
await expect(panel.getByRole("heading", { name: "Create copy job" })).toBeVisible();
|
||||||
|
|
||||||
|
// select different account dropdown
|
||||||
|
|
||||||
|
const accountDropdown = panel.getByTestId("account-dropdown");
|
||||||
|
await accountDropdown.click();
|
||||||
|
|
||||||
|
const dropdownItemsWrapper = frame.locator("div.ms-Dropdown-items");
|
||||||
|
expect(await dropdownItemsWrapper.getAttribute("aria-label")).toEqual("Account");
|
||||||
|
|
||||||
|
const allDropdownItems = await dropdownItemsWrapper.locator(`button.ms-Dropdown-item[role='option']`).all();
|
||||||
|
|
||||||
|
const filteredItems = [];
|
||||||
|
for (const item of allDropdownItems) {
|
||||||
|
const testContent = (await item.textContent()) ?? "";
|
||||||
|
if (testContent.trim() !== targetAccountName.trim()) {
|
||||||
|
filteredItems.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filteredItems.length > 0) {
|
||||||
|
const firstDropdownItem = filteredItems[0];
|
||||||
|
expectedSourceAccountName = (await firstDropdownItem.textContent()) ?? "";
|
||||||
|
await firstDropdownItem.click();
|
||||||
|
} else {
|
||||||
|
throw new Error("No dropdown items available after filtering");
|
||||||
|
}
|
||||||
|
|
||||||
|
const fluentUiCheckboxContainer = panel.getByTestId("migration-type-checkbox").locator("div.ms-Checkbox");
|
||||||
|
await fluentUiCheckboxContainer.click();
|
||||||
|
|
||||||
|
await panel.getByRole("button", { name: "Next" }).click();
|
||||||
|
|
||||||
|
// Verifying Assign Permissions panel for online copy
|
||||||
|
|
||||||
|
const permissionScreen = panel.getByTestId("Panel:AssignPermissionsContainer");
|
||||||
|
await expect(permissionScreen).toBeVisible();
|
||||||
|
|
||||||
|
await expect(permissionScreen.getByText("Online container copy", { exact: true })).toBeVisible();
|
||||||
|
await expect(permissionScreen.getByText("Cross-account container copy", { exact: true })).toBeVisible();
|
||||||
|
|
||||||
|
// Verify Point-in-Time Restore timer and refresh button workflow
|
||||||
|
|
||||||
|
await page.route(`**/Microsoft.DocumentDB/databaseAccounts/${expectedSourceAccountName}**`, async (route) => {
|
||||||
|
const mockData = {
|
||||||
|
identity: {
|
||||||
|
type: "SystemAssigned",
|
||||||
|
principalId: "00-11-22-33",
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
defaultIdentity: "SystemAssignedIdentity",
|
||||||
|
backupPolicy: {
|
||||||
|
type: "Continuous",
|
||||||
|
},
|
||||||
|
capabilities: [{ name: "EnableOnlineContainerCopy" }],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if (route.request().method() === "GET") {
|
||||||
|
const response = await route.fetch();
|
||||||
|
const actualData = await response.json();
|
||||||
|
const mergedData = { ...actualData };
|
||||||
|
|
||||||
|
set(mergedData, "identity", mockData.identity);
|
||||||
|
set(mergedData, "properties.defaultIdentity", mockData.properties.defaultIdentity);
|
||||||
|
set(mergedData, "properties.backupPolicy", mockData.properties.backupPolicy);
|
||||||
|
set(mergedData, "properties.capabilities", mockData.properties.capabilities);
|
||||||
|
|
||||||
|
await route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: "application/json",
|
||||||
|
body: JSON.stringify(mergedData),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await route.continue();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(permissionScreen).toBeVisible();
|
||||||
|
|
||||||
|
const expandedOnlineAccordionHeader = permissionScreen
|
||||||
|
.getByTestId("permission-group-container-onlineConfigs")
|
||||||
|
.locator("button[aria-expanded='true']");
|
||||||
|
await expect(expandedOnlineAccordionHeader).toBeVisible();
|
||||||
|
|
||||||
|
const accordionItem = expandedOnlineAccordionHeader
|
||||||
|
.locator("xpath=ancestor::*[contains(@class, 'fui-AccordionItem') or contains(@data-test, 'accordion-item')]")
|
||||||
|
.first();
|
||||||
|
|
||||||
|
const accordionPanel = accordionItem.locator("[role='tabpanel'], .fui-AccordionPanel, [data-test*='panel']").first();
|
||||||
|
|
||||||
|
await page.clock.install({ time: new Date("2024-01-01T10:00:00Z") });
|
||||||
|
|
||||||
|
const pitrBtn = accordionPanel.getByTestId("pointInTimeRestore:PrimaryBtn");
|
||||||
|
await expect(pitrBtn).toBeVisible();
|
||||||
|
await pitrBtn.click();
|
||||||
|
|
||||||
|
page.context().on("page", async (newPage) => {
|
||||||
|
const expectedUrlEndPattern = new RegExp(
|
||||||
|
`/providers/Microsoft.(DocumentDB|DocumentDb)/databaseAccounts/${expectedSourceAccountName}/backupRestore`,
|
||||||
|
);
|
||||||
|
expect(newPage.url()).toMatch(expectedUrlEndPattern);
|
||||||
|
await newPage.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
const loadingOverlay = frame.locator("[data-test='loading-overlay']");
|
||||||
|
await expect(loadingOverlay).toBeVisible();
|
||||||
|
|
||||||
|
const refreshBtn = accordionPanel.getByTestId("pointInTimeRestore:RefreshBtn");
|
||||||
|
await expect(refreshBtn).not.toBeVisible();
|
||||||
|
|
||||||
|
// Fast forward time by 11 minutes (11 * 60 * 1000ms = 660000ms)
|
||||||
|
await page.clock.fastForward(11 * 60 * 1000);
|
||||||
|
|
||||||
|
await expect(refreshBtn).toBeVisible();
|
||||||
|
await expect(pitrBtn).not.toBeVisible();
|
||||||
|
|
||||||
|
await refreshBtn.click();
|
||||||
|
await expect(loadingOverlay).toBeVisible();
|
||||||
|
|
||||||
|
await expect(loadingOverlay).toBeHidden({ timeout: 10 * 1000 });
|
||||||
|
|
||||||
|
// Veify Popover & Loading Overlay on permission screen with API mocks and accordion interactions
|
||||||
|
|
||||||
|
await page.route(
|
||||||
|
`**/Microsoft.DocumentDB/databaseAccounts/${expectedSourceAccountName}/sqlRoleAssignments*`,
|
||||||
|
async (route) => {
|
||||||
|
await route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: "application/json",
|
||||||
|
body: JSON.stringify({
|
||||||
|
value: [
|
||||||
|
{
|
||||||
|
principalId: "00-11-22-33",
|
||||||
|
roleDefinitionId: `Microsoft.DocumentDB/databaseAccounts/${expectedSourceAccountName}/77-88-99`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await page.route("**/Microsoft.DocumentDB/databaseAccounts/*/77-88-99**", async (route) => {
|
||||||
|
await route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: "application/json",
|
||||||
|
body: JSON.stringify({
|
||||||
|
value: [
|
||||||
|
{
|
||||||
|
name: "00000000-0000-0000-0000-000000000001",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.route(`**/Microsoft.DocumentDB/databaseAccounts/${targetAccountName}**`, async (route) => {
|
||||||
|
const mockData = {
|
||||||
|
identity: {
|
||||||
|
type: "SystemAssigned",
|
||||||
|
principalId: "00-11-22-33",
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
defaultIdentity: "SystemAssignedIdentity",
|
||||||
|
backupPolicy: {
|
||||||
|
type: "Continuous",
|
||||||
|
},
|
||||||
|
capabilities: [{ name: "EnableOnlineContainerCopy" }],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (route.request().method() === "PATCH") {
|
||||||
|
await route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: "application/json",
|
||||||
|
body: JSON.stringify({ status: "Succeeded" }),
|
||||||
|
});
|
||||||
|
} else if (route.request().method() === "GET") {
|
||||||
|
// Get the actual response and merge with mock data
|
||||||
|
const response = await route.fetch();
|
||||||
|
const actualData = await response.json();
|
||||||
|
const mergedData = { ...actualData };
|
||||||
|
set(mergedData, "identity", mockData.identity);
|
||||||
|
set(mergedData, "properties.defaultIdentity", mockData.properties.defaultIdentity);
|
||||||
|
set(mergedData, "properties.backupPolicy", mockData.properties.backupPolicy);
|
||||||
|
set(mergedData, "properties.capabilities", mockData.properties.capabilities);
|
||||||
|
|
||||||
|
await route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: "application/json",
|
||||||
|
body: JSON.stringify(mergedData),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await route.continue();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(permissionScreen).toBeVisible();
|
||||||
|
|
||||||
|
const expandedCrossAccordionHeader = permissionScreen
|
||||||
|
.getByTestId("permission-group-container-crossAccountConfigs")
|
||||||
|
.locator("button[aria-expanded='true']");
|
||||||
|
await expect(expandedCrossAccordionHeader).toBeVisible();
|
||||||
|
|
||||||
|
const crossAccordionItem = expandedCrossAccordionHeader
|
||||||
|
.locator("xpath=ancestor::*[contains(@class, 'fui-AccordionItem') or contains(@data-test, 'accordion-item')]")
|
||||||
|
.first();
|
||||||
|
|
||||||
|
const crossAccordionPanel = crossAccordionItem
|
||||||
|
.locator("[role='tabpanel'], .fui-AccordionPanel, [data-test*='panel']")
|
||||||
|
.first();
|
||||||
|
|
||||||
|
const toggleButton = crossAccordionPanel.getByTestId("btn-toggle");
|
||||||
|
await expect(toggleButton).toBeVisible();
|
||||||
|
await toggleButton.click();
|
||||||
|
|
||||||
|
const popover = frame.locator("[data-test='popover-container']");
|
||||||
|
await expect(popover).toBeVisible();
|
||||||
|
|
||||||
|
const yesButton = popover.getByRole("button", { name: /Yes/i });
|
||||||
|
const noButton = popover.getByRole("button", { name: /No/i });
|
||||||
|
await expect(yesButton).toBeVisible();
|
||||||
|
await expect(noButton).toBeVisible();
|
||||||
|
|
||||||
|
await yesButton.click();
|
||||||
|
|
||||||
|
await expect(loadingOverlay).toBeVisible();
|
||||||
|
|
||||||
|
await expect(loadingOverlay).toBeHidden({ timeout: 10 * 1000 });
|
||||||
|
await expect(popover).toBeHidden({ timeout: 10 * 1000 });
|
||||||
|
|
||||||
|
await panel.getByRole("button", { name: "Cancel" }).click();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.afterAll("Container Copy - After All", async () => {
|
||||||
|
await page.unroute(/.*/, (route) => route.continue());
|
||||||
|
await page.close();
|
||||||
|
});
|
||||||
@@ -9,7 +9,7 @@ let queryTab: QueryTab = null!;
|
|||||||
let queryEditor: Editor = null!;
|
let queryEditor: Editor = null!;
|
||||||
|
|
||||||
test.beforeAll("Create Test Database", async () => {
|
test.beforeAll("Create Test Database", async () => {
|
||||||
context = await createTestSQLContainer(true);
|
context = await createTestSQLContainer({ includeTestData: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
test.beforeEach("Open new query tab", async ({ page }) => {
|
test.beforeEach("Open new query tab", async ({ page }) => {
|
||||||
@@ -30,9 +30,12 @@ test.beforeEach("Open new query tab", async ({ page }) => {
|
|||||||
await explorer.frame.getByTestId("NotificationConsole/Contents").waitFor();
|
await explorer.frame.getByTestId("NotificationConsole/Contents").waitFor();
|
||||||
});
|
});
|
||||||
|
|
||||||
test.afterAll("Delete Test Database", async () => {
|
// Delete database only if not running in CI
|
||||||
|
if (!process.env.CI) {
|
||||||
|
test.afterAll("Delete Test Database", async () => {
|
||||||
await context?.dispose();
|
await context?.dispose();
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
test("Query results", async () => {
|
test("Query results", async () => {
|
||||||
// Run the query and verify the results
|
// Run the query and verify the results
|
||||||
|
|||||||
104
test/sql/scaleAndSettings/changePartitionKey.spec.ts
Normal file
104
test/sql/scaleAndSettings/changePartitionKey.spec.ts
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
// import { expect, test } from "@playwright/test";
|
||||||
|
// import { DataExplorer, getDropdownItemByNameOrPosition, TestAccount } from "../../fx";
|
||||||
|
// import { createTestSQLContainer, TestContainerContext } from "../../testData";
|
||||||
|
|
||||||
|
// test.describe("Change Partition Key", () => {
|
||||||
|
// 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 }) => {
|
||||||
|
// 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();
|
||||||
|
// });
|
||||||
|
|
||||||
|
// // Delete database only if not running in CI
|
||||||
|
// 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();
|
||||||
|
// await changePkPanel.getByTestId("remove-sub-partition-key-button-0").click();
|
||||||
|
|
||||||
|
// await changePkPanel.getByTestId("Panel/OkButton").click();
|
||||||
|
|
||||||
|
// 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("Pending")).toBeVisible({ timeout: 30 * 1000 });
|
||||||
|
// await expect(jobRow.getByText("Completed")).toBeVisible({ timeout: 5 * 60 * 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 getDropdownItemByNameOrPosition(
|
||||||
|
// explorer.frame,
|
||||||
|
// { name: newContainerId },
|
||||||
|
// { ariaLabel: "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 });
|
||||||
|
// });
|
||||||
|
// });
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { expect, Locator, test } from "@playwright/test";
|
import { Browser, expect, Locator, Page, test } from "@playwright/test";
|
||||||
import {
|
import {
|
||||||
CommandBarButton,
|
CommandBarButton,
|
||||||
DataExplorer,
|
DataExplorer,
|
||||||
@@ -9,121 +9,116 @@ import {
|
|||||||
} from "../../fx";
|
} from "../../fx";
|
||||||
import { createTestSQLContainer, TestContainerContext } from "../../testData";
|
import { createTestSQLContainer, TestContainerContext } from "../../testData";
|
||||||
|
|
||||||
test.describe("Autoscale and Manual throughput", () => {
|
interface SetupResult {
|
||||||
let context: TestContainerContext = null!;
|
context: TestContainerContext;
|
||||||
let explorer: DataExplorer = null!;
|
page: Page;
|
||||||
|
explorer: DataExplorer;
|
||||||
|
}
|
||||||
|
|
||||||
test.beforeAll("Create Test Database", async () => {
|
test.describe("Autoscale throughput", () => {
|
||||||
context = await createTestSQLContainer(true);
|
let setup: SetupResult;
|
||||||
|
|
||||||
|
test.beforeAll(async ({ browser }) => {
|
||||||
|
setup = await openScaleTab(browser);
|
||||||
|
|
||||||
|
// Switch manual -> autoscale once for this suite
|
||||||
|
const autoscaleRadioButton = setup.explorer.frame.getByText("Autoscale", { exact: true });
|
||||||
|
await autoscaleRadioButton.click();
|
||||||
|
await expect(setup.explorer.commandBarButton(CommandBarButton.Save)).toBeEnabled();
|
||||||
|
await setup.explorer.commandBarButton(CommandBarButton.Save).click();
|
||||||
|
await expect(setup.explorer.getConsoleHeaderStatus()).toContainText(
|
||||||
|
`Successfully updated offer for collection ${setup.context.container.id}`,
|
||||||
|
{ timeout: 2 * ONE_MINUTE_MS },
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.beforeEach("Open container settings", async ({ page }) => {
|
test.afterAll(async () => {
|
||||||
explorer = await DataExplorer.open(page, TestAccount.SQL);
|
await cleanup(setup);
|
||||||
|
|
||||||
// Click Scale & Settings and open Scale tab
|
|
||||||
await explorer.openScaleAndSettings(context);
|
|
||||||
const scaleTab = explorer.frame.getByTestId("settings-tab-header/ScaleTab");
|
|
||||||
await scaleTab.click();
|
|
||||||
});
|
|
||||||
|
|
||||||
test.afterAll("Delete Test Database", async () => {
|
|
||||||
await context?.dispose();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Update autoscale max throughput", async () => {
|
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 getThroughputInput(setup.explorer, "autopilot").fill(TEST_AUTOSCALE_MAX_THROUGHPUT_RU_2K.toString());
|
||||||
await switchManualToAutoscaleThroughput();
|
await setup.explorer.commandBarButton(CommandBarButton.Save).click();
|
||||||
|
|
||||||
// Update autoscale max throughput
|
await expect(setup.explorer.getConsoleHeaderStatus()).toContainText(
|
||||||
await getThroughputInput("autopilot").fill(TEST_AUTOSCALE_MAX_THROUGHPUT_RU_2K.toString());
|
`Successfully updated offer for collection ${setup.context.container.id}`,
|
||||||
|
{ timeout: 2 * ONE_MINUTE_MS },
|
||||||
// Save
|
|
||||||
await explorer.commandBarButton(CommandBarButton.Save).click();
|
|
||||||
|
|
||||||
// Read console message
|
|
||||||
await expect(explorer.getConsoleMessage()).toContainText(
|
|
||||||
`Successfully updated offer for collection ${context.container.id}`,
|
|
||||||
{
|
|
||||||
timeout: 2 * ONE_MINUTE_MS,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Update autoscale max throughput passed allowed limit", async () => {
|
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)
|
const softAllowedMaxThroughputString = await setup.explorer.frame
|
||||||
await switchManualToAutoscaleThroughput();
|
|
||||||
|
|
||||||
// Get soft allowed max throughput and remove commas
|
|
||||||
const softAllowedMaxThroughputString = await explorer.frame
|
|
||||||
.getByTestId("soft-allowed-maximum-throughput")
|
.getByTestId("soft-allowed-maximum-throughput")
|
||||||
.innerText();
|
.innerText();
|
||||||
const softAllowedMaxThroughput = Number(softAllowedMaxThroughputString.replace(/,/g, ""));
|
const softAllowedMaxThroughput = Number(softAllowedMaxThroughputString.replace(/,/g, ""));
|
||||||
|
|
||||||
// Try to set autoscale max throughput above allowed limit
|
await getThroughputInput(setup.explorer, "autopilot").fill((softAllowedMaxThroughput * 10).toString());
|
||||||
await getThroughputInput("autopilot").fill((softAllowedMaxThroughput * 10).toString());
|
await expect(setup.explorer.commandBarButton(CommandBarButton.Save)).toBeDisabled();
|
||||||
await expect(explorer.commandBarButton(CommandBarButton.Save)).toBeDisabled();
|
await expect(delayedApplyWarning(setup.explorer)).toBeVisible();
|
||||||
await expect(getThroughputInputErrorMessage("autopilot")).toContainText(
|
|
||||||
"This update isn't possible because it would increase the total throughput",
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Update autoscale max throughput with invalid increment", async () => {
|
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 getThroughputInput(setup.explorer, "autopilot").fill("1100");
|
||||||
await switchManualToAutoscaleThroughput();
|
await expect(setup.explorer.commandBarButton(CommandBarButton.Save)).toBeDisabled();
|
||||||
|
await expect(getThroughputInputErrorMessage(setup.explorer, "autopilot")).toContainText(
|
||||||
// Try to set autoscale max throughput with invalid increment
|
|
||||||
await getThroughputInput("autopilot").fill("1100");
|
|
||||||
await expect(explorer.commandBarButton(CommandBarButton.Save)).toBeDisabled();
|
|
||||||
await expect(getThroughputInputErrorMessage("autopilot")).toContainText(
|
|
||||||
"Throughput value must be in increments of 1000",
|
"Throughput value must be in increments of 1000",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe("Manual throughput", () => {
|
||||||
|
let setup: SetupResult;
|
||||||
|
|
||||||
|
test.beforeAll(async ({ browser }) => {
|
||||||
|
setup = await openScaleTab(browser);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.afterAll(async () => {
|
||||||
|
await cleanup(setup);
|
||||||
|
});
|
||||||
|
|
||||||
test("Update manual throughput", async () => {
|
test("Update manual throughput", async () => {
|
||||||
await getThroughputInput("manual").fill(TEST_MANUAL_THROUGHPUT_RU_2K.toString());
|
await getThroughputInput(setup.explorer, "manual").fill(TEST_MANUAL_THROUGHPUT_RU_2K.toString());
|
||||||
await explorer.commandBarButton(CommandBarButton.Save).click();
|
await setup.explorer.commandBarButton(CommandBarButton.Save).click();
|
||||||
await expect(explorer.getConsoleMessage()).toContainText(
|
await expect(setup.explorer.getConsoleHeaderStatus()).toContainText(
|
||||||
`Successfully updated offer for collection ${context.container.id}`,
|
`Successfully updated offer for collection ${setup.context.container.id}`,
|
||||||
{
|
{ timeout: 2 * ONE_MINUTE_MS },
|
||||||
timeout: 2 * ONE_MINUTE_MS,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Update manual throughput passed allowed limit", async () => {
|
test("Update manual throughput passed allowed limit", async () => {
|
||||||
// Get soft allowed max throughput and remove commas
|
const softAllowedMaxThroughputString = await setup.explorer.frame
|
||||||
const softAllowedMaxThroughputString = await explorer.frame
|
|
||||||
.getByTestId("soft-allowed-maximum-throughput")
|
.getByTestId("soft-allowed-maximum-throughput")
|
||||||
.innerText();
|
.innerText();
|
||||||
const softAllowedMaxThroughput = Number(softAllowedMaxThroughputString.replace(/,/g, ""));
|
const softAllowedMaxThroughput = Number(softAllowedMaxThroughputString.replace(/,/g, ""));
|
||||||
|
|
||||||
// Try to set manual throughput above allowed limit
|
await getThroughputInput(setup.explorer, "manual").fill((softAllowedMaxThroughput * 10).toString());
|
||||||
await getThroughputInput("manual").fill((softAllowedMaxThroughput * 10).toString());
|
await expect(delayedApplyWarning(setup.explorer)).toBeVisible();
|
||||||
await expect(explorer.commandBarButton(CommandBarButton.Save)).toBeDisabled();
|
|
||||||
await expect(getThroughputInputErrorMessage("manual")).toContainText(
|
|
||||||
"This update isn't possible because it would increase the total throughput",
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Helper methods
|
|
||||||
const getThroughputInput = (type: "manual" | "autopilot"): Locator => {
|
|
||||||
return explorer.frame.getByTestId(`${type}-throughput-input`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getThroughputInputErrorMessage = (type: "manual" | "autopilot"): Locator => {
|
|
||||||
return explorer.frame.getByTestId(`${type}-throughput-input-error`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const switchManualToAutoscaleThroughput = async (): Promise<void> => {
|
|
||||||
const autoscaleRadioButton = explorer.frame.getByText("Autoscale", { exact: true });
|
|
||||||
await autoscaleRadioButton.click();
|
|
||||||
await expect(explorer.commandBarButton(CommandBarButton.Save)).toBeEnabled();
|
|
||||||
await explorer.commandBarButton(CommandBarButton.Save).click();
|
|
||||||
await expect(explorer.getConsoleMessage()).toContainText(
|
|
||||||
`Successfully updated offer for collection ${context.container.id}`,
|
|
||||||
{
|
|
||||||
timeout: ONE_MINUTE_MS,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const delayedApplyWarning = (explorer: DataExplorer): Locator =>
|
||||||
|
explorer.frame.locator("#updateThroughputDelayedApplyWarningMessage");
|
||||||
|
|
||||||
|
const getThroughputInput = (explorer: DataExplorer, type: "manual" | "autopilot"): Locator =>
|
||||||
|
explorer.frame.getByTestId(`${type}-throughput-input`);
|
||||||
|
|
||||||
|
const getThroughputInputErrorMessage = (explorer: DataExplorer, type: "manual" | "autopilot"): Locator =>
|
||||||
|
explorer.frame.getByTestId(`${type}-throughput-input-error`);
|
||||||
|
|
||||||
|
async function openScaleTab(browser: Browser): Promise<SetupResult> {
|
||||||
|
const context = await createTestSQLContainer();
|
||||||
|
const page = await browser.newPage();
|
||||||
|
const explorer = await DataExplorer.open(page, TestAccount.SQL);
|
||||||
|
|
||||||
|
await explorer.openScaleAndSettings(context);
|
||||||
|
await explorer.frame.getByTestId("settings-tab-header/ScaleTab").click();
|
||||||
|
|
||||||
|
return { context, page, explorer };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cleanup({ context }: Partial<SetupResult>) {
|
||||||
|
if (!process.env.CI) {
|
||||||
|
await context?.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,14 +6,10 @@ test.describe("Settings under Scale & Settings", () => {
|
|||||||
let context: TestContainerContext = null!;
|
let context: TestContainerContext = null!;
|
||||||
let explorer: DataExplorer = null!;
|
let explorer: DataExplorer = null!;
|
||||||
|
|
||||||
test.beforeAll("Create Test Database", async () => {
|
test.beforeAll("Create Test Database & Open Settings tab", async ({ browser }) => {
|
||||||
context = await createTestSQLContainer(true);
|
context = await createTestSQLContainer();
|
||||||
});
|
const page = await browser.newPage();
|
||||||
|
|
||||||
test.beforeEach("Open Settings tab under Scale & Settings", async ({ page }) => {
|
|
||||||
explorer = await DataExplorer.open(page, TestAccount.SQL);
|
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
|
// Click Scale & Settings and open Scale tab
|
||||||
await explorer.openScaleAndSettings(context);
|
await explorer.openScaleAndSettings(context);
|
||||||
@@ -21,18 +17,24 @@ test.describe("Settings under Scale & Settings", () => {
|
|||||||
await settingsTab.click();
|
await settingsTab.click();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Delete database only if not running in CI
|
||||||
|
if (!process.env.CI) {
|
||||||
test.afterAll("Delete Test Database", async () => {
|
test.afterAll("Delete Test Database", async () => {
|
||||||
await context?.dispose();
|
await context?.dispose();
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
test("Update TTL to On (no default)", async () => {
|
test("Update TTL to On (no default)", async () => {
|
||||||
const ttlOnNoDefaultRadioButton = explorer.frame.getByRole("radio", { name: "ttl-on-no-default-option" });
|
const ttlOnNoDefaultRadioButton = explorer.frame.getByRole("radio", { name: "ttl-on-no-default-option" });
|
||||||
await ttlOnNoDefaultRadioButton.click();
|
await ttlOnNoDefaultRadioButton.click();
|
||||||
|
|
||||||
await explorer.commandBarButton(CommandBarButton.Save).click();
|
await explorer.commandBarButton(CommandBarButton.Save).click();
|
||||||
await expect(explorer.getConsoleMessage()).toContainText(`Successfully updated container ${context.container.id}`, {
|
await expect(explorer.getConsoleHeaderStatus()).toContainText(
|
||||||
timeout: ONE_MINUTE_MS,
|
`Successfully updated container ${context.container.id}`,
|
||||||
});
|
{
|
||||||
|
timeout: 2 * ONE_MINUTE_MS,
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Update TTL to On (with user entry)", async () => {
|
test("Update TTL to On (with user entry)", async () => {
|
||||||
@@ -44,27 +46,11 @@ test.describe("Settings under Scale & Settings", () => {
|
|||||||
await ttlInput.fill("30000");
|
await ttlInput.fill("30000");
|
||||||
|
|
||||||
await explorer.commandBarButton(CommandBarButton.Save).click();
|
await explorer.commandBarButton(CommandBarButton.Save).click();
|
||||||
await expect(explorer.getConsoleMessage()).toContainText(`Successfully updated container ${context.container.id}`, {
|
await expect(explorer.getConsoleHeaderStatus()).toContainText(
|
||||||
timeout: ONE_MINUTE_MS,
|
`Successfully updated container ${context.container.id}`,
|
||||||
});
|
{
|
||||||
});
|
timeout: 2 * 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,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ test("Tables CRUD", async ({ page }) => {
|
|||||||
|
|
||||||
const explorer = await DataExplorer.open(page, TestAccount.Tables);
|
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(
|
await explorer.whilePanelOpen(
|
||||||
"New Table",
|
"New Table",
|
||||||
async (panel, okButton) => {
|
async (panel, okButton) => {
|
||||||
|
|||||||
@@ -74,8 +74,81 @@ export class TestContainerContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createTestSQLContainer(includeTestData?: boolean) {
|
type createTestSqlContainerConfig = {
|
||||||
const databaseId = generateUniqueName("db");
|
includeTestData?: boolean;
|
||||||
|
partitionKey?: string;
|
||||||
|
databaseName?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type createMultipleTestSqlContainerConfig = {
|
||||||
|
containerCount?: number;
|
||||||
|
partitionKey?: string;
|
||||||
|
databaseName?: string;
|
||||||
|
accountType: TestAccount.SQLContainerCopyOnly | TestAccount.SQL;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function createMultipleTestContainers({
|
||||||
|
partitionKey = "/partitionKey",
|
||||||
|
databaseName = "",
|
||||||
|
containerCount = 1,
|
||||||
|
accountType = TestAccount.SQL,
|
||||||
|
}: createMultipleTestSqlContainerConfig): Promise<TestContainerContext[]> {
|
||||||
|
const creationPromises: Promise<TestContainerContext>[] = [];
|
||||||
|
|
||||||
|
const databaseId = databaseName ? databaseName : generateUniqueName("db");
|
||||||
|
const credentials = getAzureCLICredentials();
|
||||||
|
const adaptedCredentials = new AzureIdentityCredentialAdapter(credentials);
|
||||||
|
const armClient = new CosmosDBManagementClient(adaptedCredentials, subscriptionId);
|
||||||
|
const accountName = getAccountName(accountType);
|
||||||
|
const account = await armClient.databaseAccounts.get(resourceGroupName, accountName);
|
||||||
|
|
||||||
|
const clientOptions: CosmosClientOptions = {
|
||||||
|
endpoint: account.documentEndpoint!,
|
||||||
|
};
|
||||||
|
|
||||||
|
const rbacToken =
|
||||||
|
accountType === TestAccount.SQL
|
||||||
|
? process.env.NOSQL_TESTACCOUNT_TOKEN
|
||||||
|
: accountType === TestAccount.SQLContainerCopyOnly
|
||||||
|
? process.env.NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN
|
||||||
|
: "";
|
||||||
|
if (rbacToken) {
|
||||||
|
clientOptions.tokenProvider = async (): Promise<string> => {
|
||||||
|
const AUTH_PREFIX = `type=aad&ver=1.0&sig=`;
|
||||||
|
const authorizationToken = `${AUTH_PREFIX}${rbacToken}`;
|
||||||
|
return authorizationToken;
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const keys = await armClient.databaseAccounts.listKeys(resourceGroupName, accountName);
|
||||||
|
clientOptions.key = keys.primaryMasterKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = new CosmosClient(clientOptions);
|
||||||
|
const { database } = await client.databases.createIfNotExists({ id: databaseId });
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (let i = 0; i < containerCount; i++) {
|
||||||
|
const containerId = `testcontainer_${Date.now()}_${Math.random().toString(36).substring(6)}_${i}`;
|
||||||
|
creationPromises.push(
|
||||||
|
database.containers.createIfNotExists({ id: containerId, partitionKey }).then(({ container }) => {
|
||||||
|
return new TestContainerContext(armClient, client, database, container, new Map<string, TestItem>());
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const contexts = await Promise.all(creationPromises);
|
||||||
|
return contexts;
|
||||||
|
} catch (e) {
|
||||||
|
await database.delete();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createTestSQLContainer({
|
||||||
|
includeTestData = false,
|
||||||
|
partitionKey = "/partitionKey",
|
||||||
|
databaseName = "",
|
||||||
|
}: createTestSqlContainerConfig = {}) {
|
||||||
|
const databaseId = databaseName ? databaseName : generateUniqueName("db");
|
||||||
const containerId = "testcontainer"; // A unique container name isn't needed because the database is unique
|
const containerId = "testcontainer"; // A unique container name isn't needed because the database is unique
|
||||||
const credentials = getAzureCLICredentials();
|
const credentials = getAzureCLICredentials();
|
||||||
const adaptedCredentials = new AzureIdentityCredentialAdapter(credentials);
|
const adaptedCredentials = new AzureIdentityCredentialAdapter(credentials);
|
||||||
@@ -104,7 +177,7 @@ export async function createTestSQLContainer(includeTestData?: boolean) {
|
|||||||
try {
|
try {
|
||||||
const { container } = await database.containers.createIfNotExists({
|
const { container } = await database.containers.createIfNotExists({
|
||||||
id: containerId,
|
id: containerId,
|
||||||
partitionKey: "/partitionKey",
|
partitionKey,
|
||||||
});
|
});
|
||||||
if (includeTestData) {
|
if (includeTestData) {
|
||||||
const batchCount = TestData.length / 100;
|
const batchCount = TestData.length / 100;
|
||||||
|
|||||||
@@ -11,8 +11,12 @@ const accountName = urlSearchParams.get("accountName") || "portal-sql-runner-wes
|
|||||||
const selfServeType = urlSearchParams.get("selfServeType") || "example";
|
const selfServeType = urlSearchParams.get("selfServeType") || "example";
|
||||||
const iframeSrc = urlSearchParams.get("iframeSrc") || "explorer.html?platform=Portal&disablePortalInitCache";
|
const iframeSrc = urlSearchParams.get("iframeSrc") || "explorer.html?platform=Portal&disablePortalInitCache";
|
||||||
const authToken = urlSearchParams.get("token");
|
const authToken = urlSearchParams.get("token");
|
||||||
|
const enablecontainercopy = urlSearchParams.get("enablecontainercopy");
|
||||||
|
|
||||||
const nosqlRbacToken = urlSearchParams.get("nosqlRbacToken") || process.env.NOSQL_TESTACCOUNT_TOKEN || "";
|
const nosqlRbacToken =
|
||||||
|
urlSearchParams.get("nosqlRbacToken") ||
|
||||||
|
(enablecontainercopy ? process.env.NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN : process.env.NOSQL_TESTACCOUNT_TOKEN) ||
|
||||||
|
"";
|
||||||
const nosqlReadOnlyRbacToken =
|
const nosqlReadOnlyRbacToken =
|
||||||
urlSearchParams.get("nosqlReadOnlyRbacToken") || process.env.NOSQL_READONLY_TESTACCOUNT_TOKEN || "";
|
urlSearchParams.get("nosqlReadOnlyRbacToken") || process.env.NOSQL_READONLY_TESTACCOUNT_TOKEN || "";
|
||||||
const tableRbacToken = urlSearchParams.get("tableRbacToken") || process.env.TABLE_TESTACCOUNT_TOKEN || "";
|
const tableRbacToken = urlSearchParams.get("tableRbacToken") || process.env.TABLE_TESTACCOUNT_TOKEN || "";
|
||||||
@@ -83,6 +87,7 @@ const initTestExplorer = async (): Promise<void> => {
|
|||||||
authorizationToken: `Bearer ${authToken}`,
|
authorizationToken: `Bearer ${authToken}`,
|
||||||
aadToken: rbacToken,
|
aadToken: rbacToken,
|
||||||
features: {},
|
features: {},
|
||||||
|
containerCopyEnabled: enablecontainercopy === "true",
|
||||||
hasWriteAccess: true,
|
hasWriteAccess: true,
|
||||||
csmEndpoint: "https://management.azure.com",
|
csmEndpoint: "https://management.azure.com",
|
||||||
dnsSuffix: "documents.azure.com",
|
dnsSuffix: "documents.azure.com",
|
||||||
|
|||||||
@@ -74,17 +74,50 @@ async function main() {
|
|||||||
}
|
}
|
||||||
} else if (account.kind === "GlobalDocumentDB") {
|
} else if (account.kind === "GlobalDocumentDB") {
|
||||||
const sqlDatabases = await client.sqlResources.listSqlDatabases(resourceGroupName, account.name);
|
const sqlDatabases = await client.sqlResources.listSqlDatabases(resourceGroupName, account.name);
|
||||||
for (const database of sqlDatabases) {
|
const sqlDatabasesToDelete = sqlDatabases.map(async (database) => {
|
||||||
|
await deleteWithRetry(client, database, account.name);
|
||||||
|
});
|
||||||
|
await Promise.all(sqlDatabasesToDelete);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retry logic for handling throttling
|
||||||
|
async function deleteWithRetry(client, database, accountName) {
|
||||||
|
const maxRetries = 5;
|
||||||
|
let attempt = 0;
|
||||||
|
let backoffTime = 1000; // Start with 1 second
|
||||||
|
|
||||||
|
while (attempt < maxRetries) {
|
||||||
|
try {
|
||||||
const timestamp = Number(database.resource._ts) * 1000;
|
const timestamp = Number(database.resource._ts) * 1000;
|
||||||
if (timestamp && timestamp < thirtyMinutesAgo) {
|
if (timestamp && timestamp < thirtyMinutesAgo) {
|
||||||
await client.sqlResources.deleteSqlDatabase(resourceGroupName, account.name, database.name);
|
await client.sqlResources.deleteSqlDatabase(resourceGroupName, accountName, database.name);
|
||||||
console.log(`DELETED: ${account.name} | ${database.name} | Age: ${friendlyTime(Date.now() - timestamp)}`);
|
console.log(`DELETED: ${accountName} | ${database.name} | Age: ${friendlyTime(Date.now() - timestamp)}`);
|
||||||
} else {
|
} else {
|
||||||
console.log(`SKIPPED: ${account.name} | ${database.name} | Age: ${friendlyTime(Date.now() - timestamp)}`);
|
console.log(`SKIPPED: ${accountName} | ${database.name} | Age: ${friendlyTime(Date.now() - timestamp)}`);
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
|
} catch (error) {
|
||||||
|
if (error.statusCode === 429) {
|
||||||
|
// Throttling error (HTTP 429), apply exponential backoff
|
||||||
|
console.log(`Throttling detected, retrying ${database.name}... (Attempt ${attempt + 1})`);
|
||||||
|
await delay(backoffTime);
|
||||||
|
attempt++;
|
||||||
|
backoffTime *= 2; // Exponential backoff
|
||||||
|
} else {
|
||||||
|
// For other errors, log and break
|
||||||
|
console.error(`Error deleting ${database.name}:`, error);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
console.log(`Failed to delete ${database.name} after ${maxRetries} attempts.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to delay the retry attempts
|
||||||
|
function delay(ms) {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
}
|
}
|
||||||
|
|
||||||
main()
|
main()
|
||||||
|
|||||||
Reference in New Issue
Block a user