mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-01-08 12:07:06 +00:00
Compare commits
3 Commits
users/jawe
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b71ea50972 | ||
|
|
e27cff0553 | ||
|
|
4ac8cd8fe4 |
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
|
||||||
|
|||||||
@@ -406,7 +406,11 @@ body {
|
|||||||
width: 440px;
|
width: 440px;
|
||||||
min-height: 565px;
|
min-height: 565px;
|
||||||
}
|
}
|
||||||
|
.dataExplorerLoaderforcopyJobs{
|
||||||
|
width: 100%;
|
||||||
|
min-height: 565px;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
.dataExplorerTabLoaderContainer {
|
.dataExplorerTabLoaderContainer {
|
||||||
left: initial;
|
left: initial;
|
||||||
top: initial;
|
top: initial;
|
||||||
|
|||||||
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: {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Overlay, Spinner, SpinnerSize } from "@fluentui/react";
|
import { Overlay, Spinner, SpinnerSize } from "@fluentui/react";
|
||||||
|
import { useThemeStore } from "hooks/useTheme";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
interface LoadingOverlayProps {
|
interface LoadingOverlayProps {
|
||||||
@@ -7,6 +8,7 @@ interface LoadingOverlayProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const LoadingOverlay: React.FC<LoadingOverlayProps> = ({ isLoading, label }) => {
|
const LoadingOverlay: React.FC<LoadingOverlayProps> = ({ isLoading, label }) => {
|
||||||
|
const isDarkMode = useThemeStore((state) => state.isDarkMode);
|
||||||
if (!isLoading) {
|
if (!isLoading) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -16,7 +18,7 @@ const LoadingOverlay: React.FC<LoadingOverlayProps> = ({ isLoading, label }) =>
|
|||||||
data-test="loading-overlay"
|
data-test="loading-overlay"
|
||||||
styles={{
|
styles={{
|
||||||
root: {
|
root: {
|
||||||
backgroundColor: "rgba(255,255,255,0.9)",
|
backgroundColor: isDarkMode ? "rgba(32, 31, 30, 0.9)" : "rgba(255,255,255,0.9)",
|
||||||
zIndex: 9999,
|
zIndex: 9999,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
@@ -24,7 +26,11 @@ const LoadingOverlay: React.FC<LoadingOverlayProps> = ({ isLoading, label }) =>
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Spinner size={SpinnerSize.large} label={label} styles={{ label: { fontWeight: 600 } }} />
|
<Spinner
|
||||||
|
size={SpinnerSize.large}
|
||||||
|
label={label}
|
||||||
|
styles={{ label: { fontWeight: 600, color: isDarkMode ? "#ffffff" : "#323130" } }}
|
||||||
|
/>
|
||||||
</Overlay>
|
</Overlay>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,3 +11,14 @@
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Override dark mode inherit for pagination icons */
|
||||||
|
body.isDarkMode .pager-container .ms-Button .ms-Button-icon,
|
||||||
|
body.isDarkMode .pager-container .ms-Button i {
|
||||||
|
color: var(--colorBrandForeground1);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.isDarkMode .pager-container .ms-Button:disabled .ms-Button-icon,
|
||||||
|
body.isDarkMode .pager-container .ms-Button:disabled i {
|
||||||
|
color: var(--colorNeutralForegroundDisabled);
|
||||||
|
}
|
||||||
@@ -59,7 +59,7 @@ const Pager: React.FC<PagerProps> = ({
|
|||||||
return (
|
return (
|
||||||
<div className={className || "pager-container"}>
|
<div className={className || "pager-container"}>
|
||||||
{showItemCount && (
|
{showItemCount && (
|
||||||
<Text>
|
<Text className="themeText">
|
||||||
Showing {startIndex + 1} - {endIndex} of {totalCount} items
|
Showing {startIndex + 1} - {endIndex} of {totalCount} items
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
@@ -82,7 +82,7 @@ const Pager: React.FC<PagerProps> = ({
|
|||||||
disabled={disabled || currentPage === 1}
|
disabled={disabled || currentPage === 1}
|
||||||
styles={iconButtonStyles}
|
styles={iconButtonStyles}
|
||||||
/>
|
/>
|
||||||
<Text>
|
<Text className="themeText">
|
||||||
Page {currentPage} of {totalPages}
|
Page {currentPage} of {totalPages}
|
||||||
</Text>
|
</Text>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import "@testing-library/jest-dom";
|
import "@testing-library/jest-dom";
|
||||||
import Explorer from "Explorer/Explorer";
|
import Explorer from "Explorer/Explorer";
|
||||||
|
import { getDataTransferJobs } from "../../../Common/dataAccess/dataTransfers";
|
||||||
import * as Logger from "../../../Common/Logger";
|
import * as Logger from "../../../Common/Logger";
|
||||||
import { useSidePanel } from "../../../hooks/useSidePanel";
|
import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||||
import * as dataTransferService from "../../../Utils/arm/generatedClients/dataTransferService/dataTransferJobs";
|
import * as dataTransferService from "../../../Utils/arm/generatedClients/dataTransferService/dataTransferJobs";
|
||||||
@@ -30,6 +31,7 @@ jest.mock("../../../Common/Logger");
|
|||||||
jest.mock("../../../Utils/arm/generatedClients/dataTransferService/dataTransferJobs");
|
jest.mock("../../../Utils/arm/generatedClients/dataTransferService/dataTransferJobs");
|
||||||
jest.mock("../MonitorCopyJobs/MonitorCopyJobRefState");
|
jest.mock("../MonitorCopyJobs/MonitorCopyJobRefState");
|
||||||
jest.mock("../CopyJobUtils");
|
jest.mock("../CopyJobUtils");
|
||||||
|
jest.mock("../../../Common/dataAccess/dataTransfers");
|
||||||
|
|
||||||
describe("CopyJobActions", () => {
|
describe("CopyJobActions", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -154,33 +156,31 @@ describe("CopyJobActions", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should fetch and format copy jobs successfully", async () => {
|
it("should fetch and format copy jobs successfully", async () => {
|
||||||
const mockResponse = {
|
const mockResponse = [
|
||||||
value: [
|
{
|
||||||
{
|
properties: {
|
||||||
properties: {
|
jobName: "job-1",
|
||||||
jobName: "job-1",
|
status: "InProgress",
|
||||||
status: "InProgress",
|
lastUpdatedUtcTime: "2025-01-01T10:00:00Z",
|
||||||
lastUpdatedUtcTime: "2025-01-01T10:00:00Z",
|
processedCount: 50,
|
||||||
processedCount: 50,
|
totalCount: 100,
|
||||||
totalCount: 100,
|
mode: "online",
|
||||||
mode: "online",
|
duration: "01:30:45",
|
||||||
duration: "01:30:45",
|
source: {
|
||||||
source: {
|
component: "CosmosDBSql",
|
||||||
component: "CosmosDBSql",
|
databaseName: "source-db",
|
||||||
databaseName: "source-db",
|
containerName: "source-container",
|
||||||
containerName: "source-container",
|
},
|
||||||
},
|
destination: {
|
||||||
destination: {
|
component: "CosmosDBSql",
|
||||||
component: "CosmosDBSql",
|
databaseName: "target-db",
|
||||||
databaseName: "target-db",
|
containerName: "target-container",
|
||||||
containerName: "target-container",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
},
|
||||||
};
|
];
|
||||||
|
|
||||||
(dataTransferService.listByDatabaseAccount as jest.Mock).mockResolvedValue(mockResponse);
|
(getDataTransferJobs as jest.Mock).mockResolvedValue(mockResponse);
|
||||||
(CopyJobUtils.formatUTCDateTime as jest.Mock).mockReturnValue({
|
(CopyJobUtils.formatUTCDateTime as jest.Mock).mockReturnValue({
|
||||||
formattedDateTime: "1/1/2025, 10:00:00 AM",
|
formattedDateTime: "1/1/2025, 10:00:00 AM",
|
||||||
timestamp: 1704106800000,
|
timestamp: 1704106800000,
|
||||||
@@ -201,38 +201,36 @@ describe("CopyJobActions", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should filter jobs by CosmosDBSql component", async () => {
|
it("should filter jobs by CosmosDBSql component", async () => {
|
||||||
const mockResponse = {
|
const mockResponse = [
|
||||||
value: [
|
{
|
||||||
{
|
properties: {
|
||||||
properties: {
|
jobName: "sql-job",
|
||||||
jobName: "sql-job",
|
status: "Completed",
|
||||||
status: "Completed",
|
lastUpdatedUtcTime: "2025-01-01T10:00:00Z",
|
||||||
lastUpdatedUtcTime: "2025-01-01T10:00:00Z",
|
processedCount: 100,
|
||||||
processedCount: 100,
|
totalCount: 100,
|
||||||
totalCount: 100,
|
mode: "offline",
|
||||||
mode: "offline",
|
duration: "02:00:00",
|
||||||
duration: "02:00:00",
|
source: { component: "CosmosDBSql", databaseName: "db1", containerName: "c1" },
|
||||||
source: { component: "CosmosDBSql", databaseName: "db1", containerName: "c1" },
|
destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" },
|
||||||
destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" },
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
},
|
||||||
properties: {
|
{
|
||||||
jobName: "other-job",
|
properties: {
|
||||||
status: "Completed",
|
jobName: "other-job",
|
||||||
lastUpdatedUtcTime: "2025-01-01T11:00:00Z",
|
status: "Completed",
|
||||||
processedCount: 100,
|
lastUpdatedUtcTime: "2025-01-01T11:00:00Z",
|
||||||
totalCount: 100,
|
processedCount: 100,
|
||||||
mode: "offline",
|
totalCount: 100,
|
||||||
duration: "01:00:00",
|
mode: "offline",
|
||||||
source: { component: "OtherComponent", databaseName: "db1", containerName: "c1" },
|
duration: "01:00:00",
|
||||||
destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" },
|
source: { component: "OtherComponent", databaseName: "db1", containerName: "c1" },
|
||||||
},
|
destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" },
|
||||||
},
|
},
|
||||||
],
|
},
|
||||||
};
|
];
|
||||||
|
|
||||||
(dataTransferService.listByDatabaseAccount as jest.Mock).mockResolvedValue(mockResponse);
|
(getDataTransferJobs as jest.Mock).mockResolvedValue(mockResponse);
|
||||||
(CopyJobUtils.formatUTCDateTime as jest.Mock).mockReturnValue({
|
(CopyJobUtils.formatUTCDateTime as jest.Mock).mockReturnValue({
|
||||||
formattedDateTime: "1/1/2025, 10:00:00 AM",
|
formattedDateTime: "1/1/2025, 10:00:00 AM",
|
||||||
timestamp: 1704106800000,
|
timestamp: 1704106800000,
|
||||||
@@ -247,38 +245,36 @@ describe("CopyJobActions", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should sort jobs by last updated time (newest first)", async () => {
|
it("should sort jobs by last updated time (newest first)", async () => {
|
||||||
const mockResponse = {
|
const mockResponse = [
|
||||||
value: [
|
{
|
||||||
{
|
properties: {
|
||||||
properties: {
|
jobName: "older-job",
|
||||||
jobName: "older-job",
|
status: "Completed",
|
||||||
status: "Completed",
|
lastUpdatedUtcTime: "2025-01-01T10:00:00Z",
|
||||||
lastUpdatedUtcTime: "2025-01-01T10:00:00Z",
|
processedCount: 100,
|
||||||
processedCount: 100,
|
totalCount: 100,
|
||||||
totalCount: 100,
|
mode: "offline",
|
||||||
mode: "offline",
|
duration: "01:00:00",
|
||||||
duration: "01:00:00",
|
source: { component: "CosmosDBSql", databaseName: "db1", containerName: "c1" },
|
||||||
source: { component: "CosmosDBSql", databaseName: "db1", containerName: "c1" },
|
destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" },
|
||||||
destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" },
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
},
|
||||||
properties: {
|
{
|
||||||
jobName: "newer-job",
|
properties: {
|
||||||
status: "InProgress",
|
jobName: "newer-job",
|
||||||
lastUpdatedUtcTime: "2025-01-02T10:00:00Z",
|
status: "InProgress",
|
||||||
processedCount: 50,
|
lastUpdatedUtcTime: "2025-01-02T10:00:00Z",
|
||||||
totalCount: 100,
|
processedCount: 50,
|
||||||
mode: "online",
|
totalCount: 100,
|
||||||
duration: "00:30:00",
|
mode: "online",
|
||||||
source: { component: "CosmosDBSql", databaseName: "db3", containerName: "c3" },
|
duration: "00:30:00",
|
||||||
destination: { component: "CosmosDBSql", databaseName: "db4", containerName: "c4" },
|
source: { component: "CosmosDBSql", databaseName: "db3", containerName: "c3" },
|
||||||
},
|
destination: { component: "CosmosDBSql", databaseName: "db4", containerName: "c4" },
|
||||||
},
|
},
|
||||||
],
|
},
|
||||||
};
|
];
|
||||||
|
|
||||||
(dataTransferService.listByDatabaseAccount as jest.Mock).mockResolvedValue(mockResponse);
|
(getDataTransferJobs as jest.Mock).mockResolvedValue(mockResponse);
|
||||||
(CopyJobUtils.formatUTCDateTime as jest.Mock).mockReturnValue({
|
(CopyJobUtils.formatUTCDateTime as jest.Mock).mockReturnValue({
|
||||||
formattedDateTime: "1/1/2025, 10:00:00 AM",
|
formattedDateTime: "1/1/2025, 10:00:00 AM",
|
||||||
timestamp: 1704106800000,
|
timestamp: 1704106800000,
|
||||||
@@ -293,25 +289,23 @@ describe("CopyJobActions", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should calculate completion percentage correctly", async () => {
|
it("should calculate completion percentage correctly", async () => {
|
||||||
const mockResponse = {
|
const mockResponse = [
|
||||||
value: [
|
{
|
||||||
{
|
properties: {
|
||||||
properties: {
|
jobName: "job-1",
|
||||||
jobName: "job-1",
|
status: "InProgress",
|
||||||
status: "InProgress",
|
lastUpdatedUtcTime: "2025-01-01T10:00:00Z",
|
||||||
lastUpdatedUtcTime: "2025-01-01T10:00:00Z",
|
processedCount: 75,
|
||||||
processedCount: 75,
|
totalCount: 100,
|
||||||
totalCount: 100,
|
mode: "online",
|
||||||
mode: "online",
|
duration: "01:00:00",
|
||||||
duration: "01:00:00",
|
source: { component: "CosmosDBSql", databaseName: "db1", containerName: "c1" },
|
||||||
source: { component: "CosmosDBSql", databaseName: "db1", containerName: "c1" },
|
destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" },
|
||||||
destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" },
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
],
|
},
|
||||||
};
|
];
|
||||||
|
|
||||||
(dataTransferService.listByDatabaseAccount as jest.Mock).mockResolvedValue(mockResponse);
|
(getDataTransferJobs as jest.Mock).mockResolvedValue(mockResponse);
|
||||||
(CopyJobUtils.formatUTCDateTime as jest.Mock).mockReturnValue({
|
(CopyJobUtils.formatUTCDateTime as jest.Mock).mockReturnValue({
|
||||||
formattedDateTime: "1/1/2025, 10:00:00 AM",
|
formattedDateTime: "1/1/2025, 10:00:00 AM",
|
||||||
timestamp: 1704106800000,
|
timestamp: 1704106800000,
|
||||||
@@ -325,25 +319,23 @@ describe("CopyJobActions", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should handle zero total count gracefully", async () => {
|
it("should handle zero total count gracefully", async () => {
|
||||||
const mockResponse = {
|
const mockResponse = [
|
||||||
value: [
|
{
|
||||||
{
|
properties: {
|
||||||
properties: {
|
jobName: "job-1",
|
||||||
jobName: "job-1",
|
status: "Pending",
|
||||||
status: "Pending",
|
lastUpdatedUtcTime: "2025-01-01T10:00:00Z",
|
||||||
lastUpdatedUtcTime: "2025-01-01T10:00:00Z",
|
processedCount: 0,
|
||||||
processedCount: 0,
|
totalCount: 0,
|
||||||
totalCount: 0,
|
mode: "online",
|
||||||
mode: "online",
|
duration: "00:00:00",
|
||||||
duration: "00:00:00",
|
source: { component: "CosmosDBSql", databaseName: "db1", containerName: "c1" },
|
||||||
source: { component: "CosmosDBSql", databaseName: "db1", containerName: "c1" },
|
destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" },
|
||||||
destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" },
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
],
|
},
|
||||||
};
|
];
|
||||||
|
|
||||||
(dataTransferService.listByDatabaseAccount as jest.Mock).mockResolvedValue(mockResponse);
|
(getDataTransferJobs as jest.Mock).mockResolvedValue(mockResponse);
|
||||||
(CopyJobUtils.formatUTCDateTime as jest.Mock).mockReturnValue({
|
(CopyJobUtils.formatUTCDateTime as jest.Mock).mockReturnValue({
|
||||||
formattedDateTime: "1/1/2025, 10:00:00 AM",
|
formattedDateTime: "1/1/2025, 10:00:00 AM",
|
||||||
timestamp: 1704106800000,
|
timestamp: 1704106800000,
|
||||||
@@ -361,26 +353,24 @@ describe("CopyJobActions", () => {
|
|||||||
message: "Error message line 1\r\n\r\nError message line 2",
|
message: "Error message line 1\r\n\r\nError message line 2",
|
||||||
code: "ErrorCode123",
|
code: "ErrorCode123",
|
||||||
};
|
};
|
||||||
const mockResponse = {
|
const mockResponse = [
|
||||||
value: [
|
{
|
||||||
{
|
properties: {
|
||||||
properties: {
|
jobName: "failed-job",
|
||||||
jobName: "failed-job",
|
status: "Failed",
|
||||||
status: "Failed",
|
lastUpdatedUtcTime: "2025-01-01T10:00:00Z",
|
||||||
lastUpdatedUtcTime: "2025-01-01T10:00:00Z",
|
processedCount: 50,
|
||||||
processedCount: 50,
|
totalCount: 100,
|
||||||
totalCount: 100,
|
mode: "offline",
|
||||||
mode: "offline",
|
duration: "00:30:00",
|
||||||
duration: "00:30:00",
|
source: { component: "CosmosDBSql", databaseName: "db1", containerName: "c1" },
|
||||||
source: { component: "CosmosDBSql", databaseName: "db1", containerName: "c1" },
|
destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" },
|
||||||
destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" },
|
error: mockError,
|
||||||
error: mockError,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
],
|
},
|
||||||
};
|
];
|
||||||
|
|
||||||
(dataTransferService.listByDatabaseAccount as jest.Mock).mockResolvedValue(mockResponse);
|
(getDataTransferJobs as jest.Mock).mockResolvedValue(mockResponse);
|
||||||
(CopyJobUtils.formatUTCDateTime as jest.Mock).mockReturnValue({
|
(CopyJobUtils.formatUTCDateTime as jest.Mock).mockReturnValue({
|
||||||
formattedDateTime: "1/1/2025, 10:00:00 AM",
|
formattedDateTime: "1/1/2025, 10:00:00 AM",
|
||||||
timestamp: 1704106800000,
|
timestamp: 1704106800000,
|
||||||
@@ -408,7 +398,7 @@ describe("CopyJobActions", () => {
|
|||||||
};
|
};
|
||||||
(global as any).AbortController = jest.fn(() => mockAbortController);
|
(global as any).AbortController = jest.fn(() => mockAbortController);
|
||||||
|
|
||||||
(dataTransferService.listByDatabaseAccount as jest.Mock).mockResolvedValue({ value: [] });
|
(getDataTransferJobs as jest.Mock).mockResolvedValue([]);
|
||||||
|
|
||||||
getCopyJobs();
|
getCopyJobs();
|
||||||
expect(mockAbortController.abort).not.toHaveBeenCalled();
|
expect(mockAbortController.abort).not.toHaveBeenCalled();
|
||||||
@@ -418,9 +408,7 @@ describe("CopyJobActions", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should throw error for invalid response format", async () => {
|
it("should throw error for invalid response format", async () => {
|
||||||
(dataTransferService.listByDatabaseAccount as jest.Mock).mockResolvedValue({
|
(getDataTransferJobs as jest.Mock).mockResolvedValue("not-an-array");
|
||||||
value: "not-an-array",
|
|
||||||
});
|
|
||||||
|
|
||||||
await expect(getCopyJobs()).rejects.toThrow("Invalid migration job status response: Expected an array of jobs.");
|
await expect(getCopyJobs()).rejects.toThrow("Invalid migration job status response: Expected an array of jobs.");
|
||||||
});
|
});
|
||||||
@@ -430,7 +418,7 @@ describe("CopyJobActions", () => {
|
|||||||
message: "Aborted",
|
message: "Aborted",
|
||||||
content: JSON.stringify({ message: "signal is aborted without reason" }),
|
content: JSON.stringify({ message: "signal is aborted without reason" }),
|
||||||
};
|
};
|
||||||
(dataTransferService.listByDatabaseAccount as jest.Mock).mockRejectedValue(abortError);
|
(getDataTransferJobs as jest.Mock).mockRejectedValue(abortError);
|
||||||
|
|
||||||
await expect(getCopyJobs()).rejects.toMatchObject({
|
await expect(getCopyJobs()).rejects.toMatchObject({
|
||||||
message: expect.stringContaining("Previous copy job request was cancelled."),
|
message: expect.stringContaining("Previous copy job request was cancelled."),
|
||||||
@@ -439,7 +427,7 @@ describe("CopyJobActions", () => {
|
|||||||
|
|
||||||
it("should handle generic errors", async () => {
|
it("should handle generic errors", async () => {
|
||||||
const genericError = new Error("Network error");
|
const genericError = new Error("Network error");
|
||||||
(dataTransferService.listByDatabaseAccount as jest.Mock).mockRejectedValue(genericError);
|
(getDataTransferJobs as jest.Mock).mockRejectedValue(genericError);
|
||||||
|
|
||||||
await expect(getCopyJobs()).rejects.toThrow("Network error");
|
await expect(getCopyJobs()).rejects.toThrow("Network error");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import Explorer from "Explorer/Explorer";
|
import Explorer from "Explorer/Explorer";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
|
import { getDataTransferJobs } from "../../../Common/dataAccess/dataTransfers";
|
||||||
import { logError } from "../../../Common/Logger";
|
import { logError } from "../../../Common/Logger";
|
||||||
import { useSidePanel } from "../../../hooks/useSidePanel";
|
import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||||
import {
|
import {
|
||||||
cancel,
|
cancel,
|
||||||
complete,
|
complete,
|
||||||
create,
|
create,
|
||||||
listByDatabaseAccount,
|
|
||||||
pause,
|
pause,
|
||||||
resume,
|
resume,
|
||||||
} from "../../../Utils/arm/generatedClients/dataTransferService/dataTransferJobs";
|
} from "../../../Utils/arm/generatedClients/dataTransferService/dataTransferJobs";
|
||||||
@@ -63,14 +63,8 @@ export const getCopyJobs = async (): Promise<CopyJobType[]> => {
|
|||||||
const { subscriptionId, resourceGroup, accountName } = getAccountDetailsFromResourceId(
|
const { subscriptionId, resourceGroup, accountName } = getAccountDetailsFromResourceId(
|
||||||
userContext.databaseAccount?.id || "",
|
userContext.databaseAccount?.id || "",
|
||||||
);
|
);
|
||||||
const response = await listByDatabaseAccount(
|
const jobs = await getDataTransferJobs(subscriptionId, resourceGroup, accountName, copyJobsAbortController.signal);
|
||||||
subscriptionId,
|
|
||||||
resourceGroup,
|
|
||||||
accountName,
|
|
||||||
copyJobsAbortController.signal,
|
|
||||||
);
|
|
||||||
|
|
||||||
const jobs = response.value || [];
|
|
||||||
if (!Array.isArray(jobs)) {
|
if (!Array.isArray(jobs)) {
|
||||||
throw new Error("Invalid migration job status response: Expected an array of jobs.");
|
throw new Error("Invalid migration job status response: Expected an array of jobs.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ describe("CopyJobCommandBar", () => {
|
|||||||
|
|
||||||
render(<CopyJobCommandBar explorer={mockExplorer} />);
|
render(<CopyJobCommandBar explorer={mockExplorer} />);
|
||||||
|
|
||||||
expect(mockGetCommandBarButtons).toHaveBeenCalledWith(mockExplorer);
|
expect(mockGetCommandBarButtons).toHaveBeenCalledWith(mockExplorer, false);
|
||||||
expect(mockGetCommandBarButtons).toHaveBeenCalledTimes(1);
|
expect(mockGetCommandBarButtons).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -163,7 +163,7 @@ describe("CopyJobCommandBar", () => {
|
|||||||
|
|
||||||
render(<CopyJobCommandBar explorer={mockExplorer} />);
|
render(<CopyJobCommandBar explorer={mockExplorer} />);
|
||||||
|
|
||||||
expect(mockGetCommandBarButtons).toHaveBeenCalledWith(mockExplorer);
|
expect(mockGetCommandBarButtons).toHaveBeenCalledWith(mockExplorer, false);
|
||||||
expect(mockConvertButton.mock.calls[0][0]).toEqual(mockCommandButtonProps);
|
expect(mockConvertButton.mock.calls[0][0]).toEqual(mockCommandButtonProps);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -175,11 +175,11 @@ describe("CopyJobCommandBar", () => {
|
|||||||
mockConvertButton.mockReturnValue([]);
|
mockConvertButton.mockReturnValue([]);
|
||||||
|
|
||||||
const { rerender } = render(<CopyJobCommandBar explorer={mockExplorer1} />);
|
const { rerender } = render(<CopyJobCommandBar explorer={mockExplorer1} />);
|
||||||
expect(mockGetCommandBarButtons).toHaveBeenCalledWith(mockExplorer1);
|
expect(mockGetCommandBarButtons).toHaveBeenCalledWith(mockExplorer1, false);
|
||||||
|
|
||||||
rerender(<CopyJobCommandBar explorer={mockExplorer2} />);
|
rerender(<CopyJobCommandBar explorer={mockExplorer2} />);
|
||||||
|
|
||||||
expect(mockGetCommandBarButtons).toHaveBeenCalledWith(mockExplorer2);
|
expect(mockGetCommandBarButtons).toHaveBeenCalledWith(mockExplorer2, false);
|
||||||
expect(mockGetCommandBarButtons).toHaveBeenCalledTimes(2);
|
expect(mockGetCommandBarButtons).toHaveBeenCalledTimes(2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,24 +1,28 @@
|
|||||||
import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react";
|
import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { StyleConstants } from "../../../Common/StyleConstants";
|
import { useThemeStore } from "../../../hooks/useTheme";
|
||||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||||
import * as CommandBarUtil from "../../Menus/CommandBar/CommandBarUtil";
|
import * as CommandBarUtil from "../../Menus/CommandBar/CommandBarUtil";
|
||||||
|
import { getThemeTokens } from "../../Theme/ThemeUtil";
|
||||||
import { ContainerCopyProps } from "../Types/CopyJobTypes";
|
import { ContainerCopyProps } from "../Types/CopyJobTypes";
|
||||||
import { getCommandBarButtons } from "./Utils";
|
import { getCommandBarButtons } from "./Utils";
|
||||||
|
|
||||||
const backgroundColor = StyleConstants.BaseLight;
|
|
||||||
const rootStyle = {
|
|
||||||
root: {
|
|
||||||
backgroundColor: backgroundColor,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const CopyJobCommandBar: React.FC<ContainerCopyProps> = ({ explorer }) => {
|
const CopyJobCommandBar: React.FC<ContainerCopyProps> = ({ explorer }) => {
|
||||||
const commandBarItems: CommandButtonComponentProps[] = getCommandBarButtons(explorer);
|
const isDarkMode = useThemeStore((state) => state.isDarkMode);
|
||||||
|
const themeTokens = getThemeTokens(isDarkMode);
|
||||||
|
const backgroundColor = themeTokens.colorNeutralBackground1;
|
||||||
|
|
||||||
|
const rootStyle = {
|
||||||
|
root: {
|
||||||
|
backgroundColor: backgroundColor,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const commandBarItems: CommandButtonComponentProps[] = getCommandBarButtons(explorer, isDarkMode);
|
||||||
const controlButtons: ICommandBarItemProps[] = CommandBarUtil.convertButton(commandBarItems, backgroundColor);
|
const controlButtons: ICommandBarItemProps[] = CommandBarUtil.convertButton(commandBarItems, backgroundColor);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="commandBarContainer">
|
<div className="commandBarContainer" style={{ backgroundColor }}>
|
||||||
<FluentCommandBar
|
<FluentCommandBar
|
||||||
ariaLabel="Use left and right arrow keys to navigate between commands"
|
ariaLabel="Use left and right arrow keys to navigate between commands"
|
||||||
styles={rootStyle}
|
styles={rootStyle}
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ describe("CommandBar Utils", () => {
|
|||||||
|
|
||||||
describe("getCommandBarButtons", () => {
|
describe("getCommandBarButtons", () => {
|
||||||
it("should return an array of command button props", () => {
|
it("should return an array of command button props", () => {
|
||||||
const buttons = getCommandBarButtons(mockExplorer);
|
const buttons = getCommandBarButtons(mockExplorer, false);
|
||||||
|
|
||||||
expect(buttons).toBeDefined();
|
expect(buttons).toBeDefined();
|
||||||
expect(Array.isArray(buttons)).toBe(true);
|
expect(Array.isArray(buttons)).toBe(true);
|
||||||
@@ -58,7 +58,7 @@ describe("CommandBar Utils", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should include create copy job button", () => {
|
it("should include create copy job button", () => {
|
||||||
const buttons = getCommandBarButtons(mockExplorer);
|
const buttons = getCommandBarButtons(mockExplorer, false);
|
||||||
const createButton = buttons[0];
|
const createButton = buttons[0];
|
||||||
|
|
||||||
expect(createButton).toBeDefined();
|
expect(createButton).toBeDefined();
|
||||||
@@ -70,7 +70,7 @@ describe("CommandBar Utils", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should include refresh button", () => {
|
it("should include refresh button", () => {
|
||||||
const buttons = getCommandBarButtons(mockExplorer);
|
const buttons = getCommandBarButtons(mockExplorer, false);
|
||||||
const refreshButton = buttons[1];
|
const refreshButton = buttons[1];
|
||||||
|
|
||||||
expect(refreshButton).toBeDefined();
|
expect(refreshButton).toBeDefined();
|
||||||
@@ -80,11 +80,11 @@ describe("CommandBar Utils", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should include feedback button when platform is Portal", () => {
|
it("should include feedback button when platform is Portal", () => {
|
||||||
const buttons = getCommandBarButtons(mockExplorer);
|
const buttons = getCommandBarButtons(mockExplorer, false);
|
||||||
|
|
||||||
expect(buttons.length).toBe(3);
|
expect(buttons.length).toBe(4);
|
||||||
|
|
||||||
const feedbackButton = buttons[2];
|
const feedbackButton = buttons[3];
|
||||||
expect(feedbackButton).toBeDefined();
|
expect(feedbackButton).toBeDefined();
|
||||||
expect(feedbackButton.ariaLabel).toBe("Provide feedback on copy jobs");
|
expect(feedbackButton.ariaLabel).toBe("Provide feedback on copy jobs");
|
||||||
expect(feedbackButton.tooltipText).toBe("Feedback");
|
expect(feedbackButton.tooltipText).toBe("Feedback");
|
||||||
@@ -105,13 +105,13 @@ describe("CommandBar Utils", () => {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const { getCommandBarButtons: getCommandBarButtonsEmulator } = await import("./Utils");
|
const { getCommandBarButtons: getCommandBarButtonsEmulator } = await import("./Utils");
|
||||||
const buttons = getCommandBarButtonsEmulator(mockExplorer);
|
const buttons = getCommandBarButtonsEmulator(mockExplorer, false);
|
||||||
|
|
||||||
expect(buttons.length).toBe(2);
|
expect(buttons.length).toBe(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should call openCreateCopyJobPanel when create button is clicked", () => {
|
it("should call openCreateCopyJobPanel when create button is clicked", () => {
|
||||||
const buttons = getCommandBarButtons(mockExplorer);
|
const buttons = getCommandBarButtons(mockExplorer, false);
|
||||||
const createButton = buttons[0];
|
const createButton = buttons[0];
|
||||||
|
|
||||||
createButton.onCommandClick({} as React.SyntheticEvent);
|
createButton.onCommandClick({} as React.SyntheticEvent);
|
||||||
@@ -121,7 +121,7 @@ describe("CommandBar Utils", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should call refreshJobList when refresh button is clicked", () => {
|
it("should call refreshJobList when refresh button is clicked", () => {
|
||||||
const buttons = getCommandBarButtons(mockExplorer);
|
const buttons = getCommandBarButtons(mockExplorer, false);
|
||||||
const refreshButton = buttons[1];
|
const refreshButton = buttons[1];
|
||||||
|
|
||||||
refreshButton.onCommandClick({} as React.SyntheticEvent);
|
refreshButton.onCommandClick({} as React.SyntheticEvent);
|
||||||
@@ -130,8 +130,8 @@ describe("CommandBar Utils", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should call openContainerCopyFeedbackBlade when feedback button is clicked", () => {
|
it("should call openContainerCopyFeedbackBlade when feedback button is clicked", () => {
|
||||||
const buttons = getCommandBarButtons(mockExplorer);
|
const buttons = getCommandBarButtons(mockExplorer, false);
|
||||||
const feedbackButton = buttons[2];
|
const feedbackButton = buttons[3];
|
||||||
|
|
||||||
feedbackButton.onCommandClick({} as React.SyntheticEvent);
|
feedbackButton.onCommandClick({} as React.SyntheticEvent);
|
||||||
|
|
||||||
@@ -139,7 +139,7 @@ describe("CommandBar Utils", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should return buttons with correct icon sources", () => {
|
it("should return buttons with correct icon sources", () => {
|
||||||
const buttons = getCommandBarButtons(mockExplorer);
|
const buttons = getCommandBarButtons(mockExplorer, false);
|
||||||
|
|
||||||
expect(buttons[0].iconSrc).toBeDefined();
|
expect(buttons[0].iconSrc).toBeDefined();
|
||||||
expect(buttons[0].iconAlt).toBe("Create Copy Job");
|
expect(buttons[0].iconAlt).toBe("Create Copy Job");
|
||||||
@@ -148,7 +148,10 @@ describe("CommandBar Utils", () => {
|
|||||||
expect(buttons[1].iconAlt).toBe("Refresh");
|
expect(buttons[1].iconAlt).toBe("Refresh");
|
||||||
|
|
||||||
expect(buttons[2].iconSrc).toBeDefined();
|
expect(buttons[2].iconSrc).toBeDefined();
|
||||||
expect(buttons[2].iconAlt).toBe("Feedback");
|
expect(buttons[2].iconAlt).toBe("Dark Theme");
|
||||||
|
|
||||||
|
expect(buttons[3].iconSrc).toBeDefined();
|
||||||
|
expect(buttons[3].iconAlt).toBe("Feedback");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle null MonitorCopyJobsRefState ref gracefully", () => {
|
it("should handle null MonitorCopyJobsRefState ref gracefully", () => {
|
||||||
@@ -157,14 +160,14 @@ describe("CommandBar Utils", () => {
|
|||||||
return selector(state);
|
return selector(state);
|
||||||
});
|
});
|
||||||
|
|
||||||
const buttons = getCommandBarButtons(mockExplorer);
|
const buttons = getCommandBarButtons(mockExplorer, false);
|
||||||
const refreshButton = buttons[1];
|
const refreshButton = buttons[1];
|
||||||
|
|
||||||
expect(() => refreshButton.onCommandClick({} as React.SyntheticEvent)).not.toThrow();
|
expect(() => refreshButton.onCommandClick({} as React.SyntheticEvent)).not.toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should set hasPopup to false for all buttons", () => {
|
it("should set hasPopup to false for all buttons", () => {
|
||||||
const buttons = getCommandBarButtons(mockExplorer);
|
const buttons = getCommandBarButtons(mockExplorer, false);
|
||||||
|
|
||||||
buttons.forEach((button) => {
|
buttons.forEach((button) => {
|
||||||
expect(button.hasPopup).toBe(false);
|
expect(button.hasPopup).toBe(false);
|
||||||
@@ -172,7 +175,7 @@ describe("CommandBar Utils", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should set commandButtonLabel to undefined for all buttons", () => {
|
it("should set commandButtonLabel to undefined for all buttons", () => {
|
||||||
const buttons = getCommandBarButtons(mockExplorer);
|
const buttons = getCommandBarButtons(mockExplorer, false);
|
||||||
|
|
||||||
buttons.forEach((button) => {
|
buttons.forEach((button) => {
|
||||||
expect(button.commandButtonLabel).toBeUndefined();
|
expect(button.commandButtonLabel).toBeUndefined();
|
||||||
@@ -180,7 +183,7 @@ describe("CommandBar Utils", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should respect disabled state when provided", () => {
|
it("should respect disabled state when provided", () => {
|
||||||
const buttons = getCommandBarButtons(mockExplorer);
|
const buttons = getCommandBarButtons(mockExplorer, false);
|
||||||
|
|
||||||
buttons.forEach((button) => {
|
buttons.forEach((button) => {
|
||||||
expect(button.disabled).toBe(false);
|
expect(button.disabled).toBe(false);
|
||||||
@@ -188,7 +191,7 @@ describe("CommandBar Utils", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should return CommandButtonComponentProps with all required properties", () => {
|
it("should return CommandButtonComponentProps with all required properties", () => {
|
||||||
const buttons = getCommandBarButtons(mockExplorer);
|
const buttons = getCommandBarButtons(mockExplorer, false);
|
||||||
|
|
||||||
buttons.forEach((button: CommandButtonComponentProps) => {
|
buttons.forEach((button: CommandButtonComponentProps) => {
|
||||||
expect(button).toHaveProperty("iconSrc");
|
expect(button).toHaveProperty("iconSrc");
|
||||||
@@ -202,18 +205,19 @@ describe("CommandBar Utils", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should maintain button order: create, refresh, feedback", () => {
|
it("should maintain button order: create, refresh, themeToggle, feedback", () => {
|
||||||
const buttons = getCommandBarButtons(mockExplorer);
|
const buttons = getCommandBarButtons(mockExplorer, false);
|
||||||
|
|
||||||
expect(buttons[0].tooltipText).toBe("Create Copy Job");
|
expect(buttons[0].tooltipText).toBe("Create Copy Job");
|
||||||
expect(buttons[1].tooltipText).toBe("Refresh");
|
expect(buttons[1].tooltipText).toBe("Refresh");
|
||||||
expect(buttons[2].tooltipText).toBe("Feedback");
|
expect(buttons[2].tooltipText).toBe("Dark Theme");
|
||||||
|
expect(buttons[3].tooltipText).toBe("Feedback");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Button click handlers", () => {
|
describe("Button click handlers", () => {
|
||||||
it("should execute click handlers without errors", () => {
|
it("should execute click handlers without errors", () => {
|
||||||
const buttons = getCommandBarButtons(mockExplorer);
|
const buttons = getCommandBarButtons(mockExplorer, false);
|
||||||
|
|
||||||
buttons.forEach((button) => {
|
buttons.forEach((button) => {
|
||||||
expect(() => button.onCommandClick({} as React.SyntheticEvent)).not.toThrow();
|
expect(() => button.onCommandClick({} as React.SyntheticEvent)).not.toThrow();
|
||||||
@@ -221,7 +225,7 @@ describe("CommandBar Utils", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should call correct action for each button", () => {
|
it("should call correct action for each button", () => {
|
||||||
const buttons = getCommandBarButtons(mockExplorer);
|
const buttons = getCommandBarButtons(mockExplorer, false);
|
||||||
|
|
||||||
buttons[0].onCommandClick({} as React.SyntheticEvent);
|
buttons[0].onCommandClick({} as React.SyntheticEvent);
|
||||||
expect(Actions.openCreateCopyJobPanel).toHaveBeenCalledWith(mockExplorer);
|
expect(Actions.openCreateCopyJobPanel).toHaveBeenCalledWith(mockExplorer);
|
||||||
@@ -229,14 +233,14 @@ describe("CommandBar Utils", () => {
|
|||||||
buttons[1].onCommandClick({} as React.SyntheticEvent);
|
buttons[1].onCommandClick({} as React.SyntheticEvent);
|
||||||
expect(mockRefreshJobList).toHaveBeenCalled();
|
expect(mockRefreshJobList).toHaveBeenCalled();
|
||||||
|
|
||||||
buttons[2].onCommandClick({} as React.SyntheticEvent);
|
buttons[3].onCommandClick({} as React.SyntheticEvent);
|
||||||
expect(mockOpenContainerCopyFeedbackBlade).toHaveBeenCalled();
|
expect(mockOpenContainerCopyFeedbackBlade).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Accessibility", () => {
|
describe("Accessibility", () => {
|
||||||
it("should have aria labels for all buttons", () => {
|
it("should have aria labels for all buttons", () => {
|
||||||
const buttons = getCommandBarButtons(mockExplorer);
|
const buttons = getCommandBarButtons(mockExplorer, false);
|
||||||
|
|
||||||
buttons.forEach((button) => {
|
buttons.forEach((button) => {
|
||||||
expect(button.ariaLabel).toBeDefined();
|
expect(button.ariaLabel).toBeDefined();
|
||||||
@@ -246,7 +250,7 @@ describe("CommandBar Utils", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should have tooltip text for all buttons", () => {
|
it("should have tooltip text for all buttons", () => {
|
||||||
const buttons = getCommandBarButtons(mockExplorer);
|
const buttons = getCommandBarButtons(mockExplorer, false);
|
||||||
|
|
||||||
buttons.forEach((button) => {
|
buttons.forEach((button) => {
|
||||||
expect(button.tooltipText).toBeDefined();
|
expect(button.tooltipText).toBeDefined();
|
||||||
@@ -256,7 +260,7 @@ describe("CommandBar Utils", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should have icon alt text for all buttons", () => {
|
it("should have icon alt text for all buttons", () => {
|
||||||
const buttons = getCommandBarButtons(mockExplorer);
|
const buttons = getCommandBarButtons(mockExplorer, false);
|
||||||
|
|
||||||
buttons.forEach((button) => {
|
buttons.forEach((button) => {
|
||||||
expect(button.iconAlt).toBeDefined();
|
expect(button.iconAlt).toBeDefined();
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import AddIcon from "../../../../images/Add.svg";
|
import AddIcon from "../../../../images/Add.svg";
|
||||||
import FeedbackIcon from "../../../../images/Feedback-Command.svg";
|
import FeedbackIcon from "../../../../images/Feedback-Command.svg";
|
||||||
|
import MoonIcon from "../../../../images/MoonIcon.svg";
|
||||||
import RefreshIcon from "../../../../images/refresh-cosmos.svg";
|
import RefreshIcon from "../../../../images/refresh-cosmos.svg";
|
||||||
|
import SunIcon from "../../../../images/SunIcon.svg";
|
||||||
import { configContext, Platform } from "../../../ConfigContext";
|
import { configContext, Platform } from "../../../ConfigContext";
|
||||||
|
import { useThemeStore } from "../../../hooks/useTheme";
|
||||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||||
import Explorer from "../../Explorer";
|
import Explorer from "../../Explorer";
|
||||||
import * as Actions from "../Actions/CopyJobActions";
|
import * as Actions from "../Actions/CopyJobActions";
|
||||||
@@ -9,7 +12,7 @@ import ContainerCopyMessages from "../ContainerCopyMessages";
|
|||||||
import { MonitorCopyJobsRefState } from "../MonitorCopyJobs/MonitorCopyJobRefState";
|
import { MonitorCopyJobsRefState } from "../MonitorCopyJobs/MonitorCopyJobRefState";
|
||||||
import { CopyJobCommandBarBtnType } from "../Types/CopyJobTypes";
|
import { CopyJobCommandBarBtnType } from "../Types/CopyJobTypes";
|
||||||
|
|
||||||
function getCopyJobBtns(explorer: Explorer): CopyJobCommandBarBtnType[] {
|
function getCopyJobBtns(explorer: Explorer, isDarkMode: boolean): CopyJobCommandBarBtnType[] {
|
||||||
const monitorCopyJobsRef = MonitorCopyJobsRefState((state) => state.ref);
|
const monitorCopyJobsRef = MonitorCopyJobsRefState((state) => state.ref);
|
||||||
const buttons: CopyJobCommandBarBtnType[] = [
|
const buttons: CopyJobCommandBarBtnType[] = [
|
||||||
{
|
{
|
||||||
@@ -26,7 +29,15 @@ function getCopyJobBtns(explorer: Explorer): CopyJobCommandBarBtnType[] {
|
|||||||
ariaLabel: ContainerCopyMessages.refreshButtonAriaLabel,
|
ariaLabel: ContainerCopyMessages.refreshButtonAriaLabel,
|
||||||
onClick: () => monitorCopyJobsRef?.refreshJobList(),
|
onClick: () => monitorCopyJobsRef?.refreshJobList(),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: "themeToggle",
|
||||||
|
iconSrc: isDarkMode ? SunIcon : MoonIcon,
|
||||||
|
label: isDarkMode ? "Light Theme" : "Dark Theme",
|
||||||
|
ariaLabel: isDarkMode ? "Switch to Light Theme" : "Switch to Dark Theme",
|
||||||
|
onClick: () => useThemeStore.getState().toggleTheme(),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
if (configContext.platform === Platform.Portal) {
|
if (configContext.platform === Platform.Portal) {
|
||||||
buttons.push({
|
buttons.push({
|
||||||
key: "feedback",
|
key: "feedback",
|
||||||
@@ -54,6 +65,6 @@ function btnMapper(config: CopyJobCommandBarBtnType): CommandButtonComponentProp
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCommandBarButtons(explorer: Explorer): CommandButtonComponentProps[] {
|
export function getCommandBarButtons(explorer: Explorer, isDarkMode: boolean): CommandButtonComponentProps[] {
|
||||||
return getCopyJobBtns(explorer).map(btnMapper);
|
return getCopyJobBtns(explorer, isDarkMode).map(btnMapper);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,12 @@ import useToggle from "./hooks/useToggle";
|
|||||||
const managedIdentityTooltip = (
|
const managedIdentityTooltip = (
|
||||||
<Text>
|
<Text>
|
||||||
{ContainerCopyMessages.addManagedIdentity.tooltip.content}
|
{ContainerCopyMessages.addManagedIdentity.tooltip.content}
|
||||||
<Link href={ContainerCopyMessages.addManagedIdentity.tooltip.href} target="_blank" rel="noopener noreferrer">
|
<Link
|
||||||
|
style={{ color: "var(--colorBrandForeground1)" }}
|
||||||
|
href={ContainerCopyMessages.addManagedIdentity.tooltip.href}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
{ContainerCopyMessages.addManagedIdentity.tooltip.hrefText}
|
{ContainerCopyMessages.addManagedIdentity.tooltip.hrefText}
|
||||||
</Link>
|
</Link>
|
||||||
</Text>
|
</Text>
|
||||||
@@ -26,7 +31,7 @@ const AddManagedIdentity: React.FC<AddManagedIdentityProps> = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack className="addManagedIdentityContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}>
|
<Stack className="addManagedIdentityContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}>
|
||||||
<Text>
|
<Text className="themeText">
|
||||||
{ContainerCopyMessages.addManagedIdentity.description} 
|
{ContainerCopyMessages.addManagedIdentity.description} 
|
||||||
<Link href={ContainerCopyMessages.addManagedIdentity.descriptionHref} target="_blank" rel="noopener noreferrer">
|
<Link href={ContainerCopyMessages.addManagedIdentity.descriptionHref} target="_blank" rel="noopener noreferrer">
|
||||||
{ContainerCopyMessages.addManagedIdentity.descriptionHrefText}
|
{ContainerCopyMessages.addManagedIdentity.descriptionHrefText}
|
||||||
|
|||||||
@@ -13,7 +13,12 @@ import useToggle from "./hooks/useToggle";
|
|||||||
const TooltipContent = (
|
const TooltipContent = (
|
||||||
<Text>
|
<Text>
|
||||||
{ContainerCopyMessages.readPermissionAssigned.tooltip.content}
|
{ContainerCopyMessages.readPermissionAssigned.tooltip.content}
|
||||||
<Link href={ContainerCopyMessages.readPermissionAssigned.tooltip.href} target="_blank" rel="noopener noreferrer">
|
<Link
|
||||||
|
style={{ color: "var(--colorBrandForeground1)" }}
|
||||||
|
href={ContainerCopyMessages.readPermissionAssigned.tooltip.href}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
{ContainerCopyMessages.readPermissionAssigned.tooltip.hrefText}
|
{ContainerCopyMessages.readPermissionAssigned.tooltip.hrefText}
|
||||||
</Link>
|
</Link>
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -48,8 +48,8 @@ const PermissionGroup: React.FC<PermissionGroupConfig> = ({ id, title, descripti
|
|||||||
tokens={{ childrenGap: 15 }}
|
tokens={{ childrenGap: 15 }}
|
||||||
styles={{
|
styles={{
|
||||||
root: {
|
root: {
|
||||||
background: "#fafafa",
|
background: "var(--colorNeutralBackground2)",
|
||||||
border: "1px solid #e1e1e1",
|
border: "1px solid var(--colorNeutralStroke1)",
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
padding: 16,
|
padding: 16,
|
||||||
boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
|
boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
|
||||||
@@ -57,11 +57,11 @@ const PermissionGroup: React.FC<PermissionGroupConfig> = ({ id, title, descripti
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Stack tokens={{ childrenGap: 5 }}>
|
<Stack tokens={{ childrenGap: 5 }}>
|
||||||
<Text variant="medium" style={{ fontWeight: 600 }}>
|
<Text variant="medium" style={{ fontWeight: 600, color: "var(--colorNeutralForeground1)" }}>
|
||||||
{title}
|
{title}
|
||||||
</Text>
|
</Text>
|
||||||
{description && (
|
{description && (
|
||||||
<Text variant="small" styles={{ root: { color: "#605E5C" } }}>
|
<Text variant="small" styles={{ root: { color: "var(--colorNeutralForeground2)" } }}>
|
||||||
{description}
|
{description}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
@@ -105,7 +105,7 @@ const AssignPermissions = () => {
|
|||||||
className="assignPermissionsContainer"
|
className="assignPermissionsContainer"
|
||||||
tokens={{ childrenGap: 20 }}
|
tokens={{ childrenGap: 20 }}
|
||||||
>
|
>
|
||||||
<Text variant="medium">
|
<Text variant="medium" style={{ color: "var(--colorNeutralForeground1)" }}>
|
||||||
{isSameAccount && copyJobState.migrationType === CopyJobMigrationType.Online
|
{isSameAccount && copyJobState.migrationType === CopyJobMigrationType.Online
|
||||||
? ContainerCopyMessages.assignPermissions.intraAccountOnlineDescription(
|
? ContainerCopyMessages.assignPermissions.intraAccountOnlineDescription(
|
||||||
copyJobState?.source?.account?.name || "",
|
copyJobState?.source?.account?.name || "",
|
||||||
|
|||||||
@@ -12,7 +12,12 @@ import useToggle from "./hooks/useToggle";
|
|||||||
const managedIdentityTooltip = (
|
const managedIdentityTooltip = (
|
||||||
<Text>
|
<Text>
|
||||||
{ContainerCopyMessages.defaultManagedIdentity.tooltip.content}
|
{ContainerCopyMessages.defaultManagedIdentity.tooltip.content}
|
||||||
<Link href={ContainerCopyMessages.defaultManagedIdentity.tooltip.href} target="_blank" rel="noopener noreferrer">
|
<Link
|
||||||
|
style={{ color: "var(--colorBrandForeground1)" }}
|
||||||
|
href={ContainerCopyMessages.defaultManagedIdentity.tooltip.href}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
{ContainerCopyMessages.defaultManagedIdentity.tooltip.hrefText}
|
{ContainerCopyMessages.defaultManagedIdentity.tooltip.hrefText}
|
||||||
</Link>
|
</Link>
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -13,7 +13,12 @@ import InfoTooltip from "../Components/InfoTooltip";
|
|||||||
const tooltipContent = (
|
const tooltipContent = (
|
||||||
<Text>
|
<Text>
|
||||||
{ContainerCopyMessages.pointInTimeRestore.tooltip.content}
|
{ContainerCopyMessages.pointInTimeRestore.tooltip.content}
|
||||||
<Link href={ContainerCopyMessages.pointInTimeRestore.tooltip.href} target="_blank" rel="noopener noreferrer">
|
<Link
|
||||||
|
style={{ color: "var(--colorBrandForeground1)" }}
|
||||||
|
href={ContainerCopyMessages.pointInTimeRestore.tooltip.href}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
{ContainerCopyMessages.pointInTimeRestore.tooltip.hrefText}
|
{ContainerCopyMessages.pointInTimeRestore.tooltip.hrefText}
|
||||||
</Link>
|
</Link>
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ exports[`AddManagedIdentity Snapshot Tests renders initial state correctly 1`] =
|
|||||||
class="ms-Stack addManagedIdentityContainer css-109"
|
class="ms-Stack addManagedIdentityContainer css-109"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="css-110"
|
class="themeText css-110"
|
||||||
>
|
>
|
||||||
A system-assigned managed identity is restricted to one per resource and is tied to the lifecycle of this resource. Once enabled, you can grant permissions to the managed identity by using Azure role-based access control (Azure RBAC). The managed identity is authenticated with Microsoft Entra ID, so you don’t have to store any credentials in code.
|
A system-assigned managed identity is restricted to one per resource and is tied to the lifecycle of this resource. Once enabled, you can grant permissions to the managed identity by using Azure role-based access control (Azure RBAC). The managed identity is authenticated with Microsoft Entra ID, so you don’t have to store any credentials in code.
|
||||||
|
|
||||||
@@ -93,7 +93,7 @@ exports[`AddManagedIdentity Snapshot Tests renders loading state 1`] = `
|
|||||||
class="ms-Stack addManagedIdentityContainer css-109"
|
class="ms-Stack addManagedIdentityContainer css-109"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="css-110"
|
class="themeText css-110"
|
||||||
>
|
>
|
||||||
A system-assigned managed identity is restricted to one per resource and is tied to the lifecycle of this resource. Once enabled, you can grant permissions to the managed identity by using Azure role-based access control (Azure RBAC). The managed identity is authenticated with Microsoft Entra ID, so you don’t have to store any credentials in code.
|
A system-assigned managed identity is restricted to one per resource and is tied to the lifecycle of this resource. Once enabled, you can grant permissions to the managed identity by using Azure role-based access control (Azure RBAC). The managed identity is authenticated with Microsoft Entra ID, so you don’t have to store any credentials in code.
|
||||||
|
|
||||||
@@ -196,13 +196,13 @@ exports[`AddManagedIdentity Snapshot Tests renders loading state 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
class="css-124"
|
class="themeText css-124"
|
||||||
style="font-weight: 600;"
|
style="font-weight: 600;"
|
||||||
>
|
>
|
||||||
Enable system assigned managed identity
|
Enable system assigned managed identity
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-110"
|
class="themeText css-110"
|
||||||
>
|
>
|
||||||
Enable system-assigned managed identity on the test-target-account. To confirm, click the "Yes" button.
|
Enable system-assigned managed identity on the test-target-account. To confirm, click the "Yes" button.
|
||||||
</span>
|
</span>
|
||||||
@@ -265,7 +265,7 @@ exports[`AddManagedIdentity Snapshot Tests renders with toggle on and popover vi
|
|||||||
class="ms-Stack addManagedIdentityContainer css-109"
|
class="ms-Stack addManagedIdentityContainer css-109"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="css-110"
|
class="themeText css-110"
|
||||||
>
|
>
|
||||||
A system-assigned managed identity is restricted to one per resource and is tied to the lifecycle of this resource. Once enabled, you can grant permissions to the managed identity by using Azure role-based access control (Azure RBAC). The managed identity is authenticated with Microsoft Entra ID, so you don’t have to store any credentials in code.
|
A system-assigned managed identity is restricted to one per resource and is tied to the lifecycle of this resource. Once enabled, you can grant permissions to the managed identity by using Azure role-based access control (Azure RBAC). The managed identity is authenticated with Microsoft Entra ID, so you don’t have to store any credentials in code.
|
||||||
|
|
||||||
@@ -351,13 +351,13 @@ exports[`AddManagedIdentity Snapshot Tests renders with toggle on and popover vi
|
|||||||
style="max-width: 450px;"
|
style="max-width: 450px;"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="css-124"
|
class="themeText css-124"
|
||||||
style="font-weight: 600;"
|
style="font-weight: 600;"
|
||||||
>
|
>
|
||||||
Enable system assigned managed identity
|
Enable system assigned managed identity
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-110"
|
class="themeText css-110"
|
||||||
>
|
>
|
||||||
Enable system-assigned managed identity on the test-target-account. To confirm, click the "Yes" button.
|
Enable system-assigned managed identity on the test-target-account. To confirm, click the "Yes" button.
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -23,10 +23,10 @@ const PopoverContainer: React.FC<PopoverContainerProps> = React.memo(
|
|||||||
style={{ maxWidth: 450 }}
|
style={{ maxWidth: 450 }}
|
||||||
>
|
>
|
||||||
<LoadingOverlay isLoading={isLoading} label={ContainerCopyMessages.popoverOverlaySpinnerLabel} />
|
<LoadingOverlay isLoading={isLoading} label={ContainerCopyMessages.popoverOverlaySpinnerLabel} />
|
||||||
<Text variant="mediumPlus" style={{ fontWeight: 600 }}>
|
<Text variant="mediumPlus" className="themeText" style={{ fontWeight: 600 }}>
|
||||||
{title}
|
{title}
|
||||||
</Text>
|
</Text>
|
||||||
<Text>{children}</Text>
|
<Text className="themeText">{children}</Text>
|
||||||
<Stack horizontal tokens={{ childrenGap: 20 }}>
|
<Stack horizontal tokens={{ childrenGap: 20 }}>
|
||||||
<PrimaryButton text={"Yes"} onClick={onPrimary} disabled={isLoading} />
|
<PrimaryButton text={"Yes"} onClick={onPrimary} disabled={isLoading} />
|
||||||
<DefaultButton text="No" onClick={onCancel} disabled={isLoading} />
|
<DefaultButton text="No" onClick={onCancel} disabled={isLoading} />
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ exports[`PopoverMessage Component Edge Cases should handle empty string title 1`
|
|||||||
style="max-width: 450px;"
|
style="max-width: 450px;"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="css-110"
|
class="themeText css-110"
|
||||||
style="font-weight: 600;"
|
style="font-weight: 600;"
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
class="css-111"
|
class="themeText css-111"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
Test content
|
Test content
|
||||||
@@ -76,7 +76,7 @@ exports[`PopoverMessage Component Edge Cases should handle null children 1`] = `
|
|||||||
style="max-width: 450px;"
|
style="max-width: 450px;"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="css-110"
|
class="themeText css-110"
|
||||||
style="font-weight: 600;"
|
style="font-weight: 600;"
|
||||||
>
|
>
|
||||||
Test Title
|
Test Title
|
||||||
@@ -139,7 +139,7 @@ exports[`PopoverMessage Component Edge Cases should handle undefined children 1`
|
|||||||
style="max-width: 450px;"
|
style="max-width: 450px;"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="css-110"
|
class="themeText css-110"
|
||||||
style="font-weight: 600;"
|
style="font-weight: 600;"
|
||||||
>
|
>
|
||||||
Test Title
|
Test Title
|
||||||
@@ -202,13 +202,13 @@ exports[`PopoverMessage Component Edge Cases should handle very long title 1`] =
|
|||||||
style="max-width: 450px;"
|
style="max-width: 450px;"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="css-110"
|
class="themeText css-110"
|
||||||
style="font-weight: 600;"
|
style="font-weight: 600;"
|
||||||
>
|
>
|
||||||
This is a very long title that might cause layout issues or text wrapping in the popover component
|
This is a very long title that might cause layout issues or text wrapping in the popover component
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-111"
|
class="themeText css-111"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
Test content
|
Test content
|
||||||
@@ -274,13 +274,13 @@ exports[`PopoverMessage Component Rendering should render correctly when visible
|
|||||||
style="max-width: 450px;"
|
style="max-width: 450px;"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="css-110"
|
class="themeText css-110"
|
||||||
style="font-weight: 600;"
|
style="font-weight: 600;"
|
||||||
>
|
>
|
||||||
Test Title
|
Test Title
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-111"
|
class="themeText css-111"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
Test content
|
Test content
|
||||||
@@ -344,13 +344,13 @@ exports[`PopoverMessage Component Rendering should render correctly with differe
|
|||||||
style="max-width: 450px;"
|
style="max-width: 450px;"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="css-110"
|
class="themeText css-110"
|
||||||
style="font-weight: 600;"
|
style="font-weight: 600;"
|
||||||
>
|
>
|
||||||
Test Title
|
Test Title
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-111"
|
class="themeText css-111"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<p>
|
<p>
|
||||||
@@ -419,13 +419,13 @@ exports[`PopoverMessage Component Rendering should render correctly with differe
|
|||||||
style="max-width: 450px;"
|
style="max-width: 450px;"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="css-110"
|
class="themeText css-110"
|
||||||
style="font-weight: 600;"
|
style="font-weight: 600;"
|
||||||
>
|
>
|
||||||
Custom Title
|
Custom Title
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-111"
|
class="themeText css-111"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
Test content
|
Test content
|
||||||
@@ -493,13 +493,13 @@ exports[`PopoverMessage Component Rendering should render correctly with loading
|
|||||||
data-testid="loading-overlay"
|
data-testid="loading-overlay"
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
class="css-110"
|
class="themeText css-110"
|
||||||
style="font-weight: 600;"
|
style="font-weight: 600;"
|
||||||
>
|
>
|
||||||
Test Title
|
Test Title
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-111"
|
class="themeText css-111"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
Test content
|
Test content
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ const AddCollectionPanelWrapper: React.FunctionComponent<AddCollectionPanelWrapp
|
|||||||
return (
|
return (
|
||||||
<Stack className="addCollectionPanelWrapper">
|
<Stack className="addCollectionPanelWrapper">
|
||||||
<Stack.Item className="addCollectionPanelHeader">
|
<Stack.Item className="addCollectionPanelHeader">
|
||||||
<Text>{ContainerCopyMessages.createNewContainerSubHeading}</Text>
|
<Text className="themeText">{ContainerCopyMessages.createNewContainerSubHeading}</Text>
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
<Stack.Item className="addCollectionPanelBody">
|
<Stack.Item className="addCollectionPanelBody">
|
||||||
<AddCollectionPanel explorer={explorer} isCopyJobFlow={true} onSubmitSuccess={handleAddCollectionSuccess} />
|
<AddCollectionPanel explorer={explorer} isCopyJobFlow={true} onSubmitSuccess={handleAddCollectionSuccess} />
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ exports[`AddCollectionPanelWrapper Component Rendering should match snapshot 1`]
|
|||||||
class="ms-StackItem addCollectionPanelHeader css-110"
|
class="ms-StackItem addCollectionPanelHeader css-110"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="css-111"
|
class="themeText css-111"
|
||||||
>
|
>
|
||||||
Select the properties for your container.
|
Select the properties for your container.
|
||||||
</span>
|
</span>
|
||||||
@@ -50,7 +50,7 @@ exports[`AddCollectionPanelWrapper Component Rendering should match snapshot wit
|
|||||||
class="ms-StackItem addCollectionPanelHeader css-110"
|
class="ms-StackItem addCollectionPanelHeader css-110"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="css-111"
|
class="themeText css-111"
|
||||||
>
|
>
|
||||||
Select the properties for your container.
|
Select the properties for your container.
|
||||||
</span>
|
</span>
|
||||||
@@ -91,7 +91,7 @@ exports[`AddCollectionPanelWrapper Component Rendering should match snapshot wit
|
|||||||
class="ms-StackItem addCollectionPanelHeader css-110"
|
class="ms-StackItem addCollectionPanelHeader css-110"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="css-111"
|
class="themeText css-111"
|
||||||
>
|
>
|
||||||
Select the properties for your container.
|
Select the properties for your container.
|
||||||
</span>
|
</span>
|
||||||
@@ -132,7 +132,7 @@ exports[`AddCollectionPanelWrapper Component Rendering should match snapshot wit
|
|||||||
class="ms-StackItem addCollectionPanelHeader css-110"
|
class="ms-StackItem addCollectionPanelHeader css-110"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="css-111"
|
class="themeText css-111"
|
||||||
>
|
>
|
||||||
Select the properties for your container.
|
Select the properties for your container.
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -36,12 +36,16 @@ const PreviewCopyJob: React.FC = () => {
|
|||||||
<TextField data-test="job-name-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 themeText">{ContainerCopyMessages.sourceSubscriptionLabel}</Text>
|
||||||
<Text data-test="source-subscription-name">{copyJobState.source?.subscription?.displayName}</Text>
|
<Text data-test="source-subscription-name" className="themeText">
|
||||||
|
{copyJobState.source?.subscription?.displayName}
|
||||||
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack>
|
<Stack>
|
||||||
<Text className="bold">{ContainerCopyMessages.sourceAccountLabel}</Text>
|
<Text className="bold themeText">{ContainerCopyMessages.sourceAccountLabel}</Text>
|
||||||
<Text data-test="source-account-name">{copyJobState.source?.account?.name}</Text>
|
<Text data-test="source-account-name" className="themeText">
|
||||||
|
{copyJobState.source?.account?.name}
|
||||||
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack>
|
<Stack>
|
||||||
<DetailsList
|
<DetailsList
|
||||||
|
|||||||
@@ -47,12 +47,12 @@ exports[`PreviewCopyJob should handle special characters in database and contain
|
|||||||
class="ms-Stack css-124"
|
class="ms-Stack css-124"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="bold css-125"
|
class="bold themeText css-125"
|
||||||
>
|
>
|
||||||
Source subscription
|
Source subscription
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-125"
|
class="themeText css-125"
|
||||||
data-test="source-subscription-name"
|
data-test="source-subscription-name"
|
||||||
>
|
>
|
||||||
Test Subscription
|
Test Subscription
|
||||||
@@ -62,12 +62,12 @@ exports[`PreviewCopyJob should handle special characters in database and contain
|
|||||||
class="ms-Stack css-124"
|
class="ms-Stack css-124"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="bold css-125"
|
class="bold themeText css-125"
|
||||||
>
|
>
|
||||||
Source account
|
Source account
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-125"
|
class="themeText css-125"
|
||||||
data-test="source-account-name"
|
data-test="source-account-name"
|
||||||
>
|
>
|
||||||
test-account
|
test-account
|
||||||
@@ -369,12 +369,12 @@ exports[`PreviewCopyJob should render component with cross-subscription setup 1`
|
|||||||
class="ms-Stack css-124"
|
class="ms-Stack css-124"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="bold css-125"
|
class="bold themeText css-125"
|
||||||
>
|
>
|
||||||
Source subscription
|
Source subscription
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-125"
|
class="themeText css-125"
|
||||||
data-test="source-subscription-name"
|
data-test="source-subscription-name"
|
||||||
>
|
>
|
||||||
Test Subscription
|
Test Subscription
|
||||||
@@ -384,12 +384,12 @@ exports[`PreviewCopyJob should render component with cross-subscription setup 1`
|
|||||||
class="ms-Stack css-124"
|
class="ms-Stack css-124"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="bold css-125"
|
class="bold themeText css-125"
|
||||||
>
|
>
|
||||||
Source account
|
Source account
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-125"
|
class="themeText css-125"
|
||||||
data-test="source-account-name"
|
data-test="source-account-name"
|
||||||
>
|
>
|
||||||
test-account
|
test-account
|
||||||
@@ -691,12 +691,12 @@ exports[`PreviewCopyJob should render with default state and empty job name 1`]
|
|||||||
class="ms-Stack css-124"
|
class="ms-Stack css-124"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="bold css-125"
|
class="bold themeText css-125"
|
||||||
>
|
>
|
||||||
Source subscription
|
Source subscription
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-125"
|
class="themeText css-125"
|
||||||
data-test="source-subscription-name"
|
data-test="source-subscription-name"
|
||||||
>
|
>
|
||||||
Test Subscription
|
Test Subscription
|
||||||
@@ -706,12 +706,12 @@ exports[`PreviewCopyJob should render with default state and empty job name 1`]
|
|||||||
class="ms-Stack css-124"
|
class="ms-Stack css-124"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="bold css-125"
|
class="bold themeText css-125"
|
||||||
>
|
>
|
||||||
Source account
|
Source account
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-125"
|
class="themeText css-125"
|
||||||
data-test="source-account-name"
|
data-test="source-account-name"
|
||||||
>
|
>
|
||||||
test-account
|
test-account
|
||||||
@@ -1013,12 +1013,12 @@ exports[`PreviewCopyJob should render with long subscription and account names 1
|
|||||||
class="ms-Stack css-124"
|
class="ms-Stack css-124"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="bold css-125"
|
class="bold themeText css-125"
|
||||||
>
|
>
|
||||||
Source subscription
|
Source subscription
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-125"
|
class="themeText css-125"
|
||||||
data-test="source-subscription-name"
|
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
|
||||||
@@ -1028,12 +1028,12 @@ exports[`PreviewCopyJob should render with long subscription and account names 1
|
|||||||
class="ms-Stack css-124"
|
class="ms-Stack css-124"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="bold css-125"
|
class="bold themeText css-125"
|
||||||
>
|
>
|
||||||
Source account
|
Source account
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-125"
|
class="themeText css-125"
|
||||||
data-test="source-account-name"
|
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
|
||||||
@@ -1335,12 +1335,12 @@ exports[`PreviewCopyJob should render with missing source account information 1`
|
|||||||
class="ms-Stack css-124"
|
class="ms-Stack css-124"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="bold css-125"
|
class="bold themeText css-125"
|
||||||
>
|
>
|
||||||
Source subscription
|
Source subscription
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-125"
|
class="themeText css-125"
|
||||||
data-test="source-subscription-name"
|
data-test="source-subscription-name"
|
||||||
>
|
>
|
||||||
Test Subscription
|
Test Subscription
|
||||||
@@ -1350,7 +1350,7 @@ exports[`PreviewCopyJob should render with missing source account information 1`
|
|||||||
class="ms-Stack css-124"
|
class="ms-Stack css-124"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="bold css-125"
|
class="bold themeText css-125"
|
||||||
>
|
>
|
||||||
Source account
|
Source account
|
||||||
</span>
|
</span>
|
||||||
@@ -1651,7 +1651,7 @@ exports[`PreviewCopyJob should render with missing source subscription informati
|
|||||||
class="ms-Stack css-124"
|
class="ms-Stack css-124"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="bold css-125"
|
class="bold themeText css-125"
|
||||||
>
|
>
|
||||||
Source subscription
|
Source subscription
|
||||||
</span>
|
</span>
|
||||||
@@ -1660,12 +1660,12 @@ exports[`PreviewCopyJob should render with missing source subscription informati
|
|||||||
class="ms-Stack css-124"
|
class="ms-Stack css-124"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="bold css-125"
|
class="bold themeText css-125"
|
||||||
>
|
>
|
||||||
Source account
|
Source account
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-125"
|
class="themeText css-125"
|
||||||
data-test="source-account-name"
|
data-test="source-account-name"
|
||||||
>
|
>
|
||||||
test-account
|
test-account
|
||||||
@@ -1967,12 +1967,12 @@ exports[`PreviewCopyJob should render with online migration type 1`] = `
|
|||||||
class="ms-Stack css-124"
|
class="ms-Stack css-124"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="bold css-125"
|
class="bold themeText css-125"
|
||||||
>
|
>
|
||||||
Source subscription
|
Source subscription
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-125"
|
class="themeText css-125"
|
||||||
data-test="source-subscription-name"
|
data-test="source-subscription-name"
|
||||||
>
|
>
|
||||||
Test Subscription
|
Test Subscription
|
||||||
@@ -1982,12 +1982,12 @@ exports[`PreviewCopyJob should render with online migration type 1`] = `
|
|||||||
class="ms-Stack css-124"
|
class="ms-Stack css-124"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="bold css-125"
|
class="bold themeText css-125"
|
||||||
>
|
>
|
||||||
Source account
|
Source account
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-125"
|
class="themeText css-125"
|
||||||
data-test="source-account-name"
|
data-test="source-account-name"
|
||||||
>
|
>
|
||||||
test-account
|
test-account
|
||||||
@@ -2289,12 +2289,12 @@ exports[`PreviewCopyJob should render with pre-filled job name 1`] = `
|
|||||||
class="ms-Stack css-124"
|
class="ms-Stack css-124"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="bold css-125"
|
class="bold themeText css-125"
|
||||||
>
|
>
|
||||||
Source subscription
|
Source subscription
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-125"
|
class="themeText css-125"
|
||||||
data-test="source-subscription-name"
|
data-test="source-subscription-name"
|
||||||
>
|
>
|
||||||
Test Subscription
|
Test Subscription
|
||||||
@@ -2304,12 +2304,12 @@ exports[`PreviewCopyJob should render with pre-filled job name 1`] = `
|
|||||||
class="ms-Stack css-124"
|
class="ms-Stack css-124"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="bold css-125"
|
class="bold themeText css-125"
|
||||||
>
|
>
|
||||||
Source account
|
Source account
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-125"
|
class="themeText css-125"
|
||||||
data-test="source-account-name"
|
data-test="source-account-name"
|
||||||
>
|
>
|
||||||
test-account
|
test-account
|
||||||
@@ -2611,12 +2611,12 @@ exports[`PreviewCopyJob should render with undefined database and container name
|
|||||||
class="ms-Stack css-124"
|
class="ms-Stack css-124"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="bold css-125"
|
class="bold themeText css-125"
|
||||||
>
|
>
|
||||||
Source subscription
|
Source subscription
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-125"
|
class="themeText css-125"
|
||||||
data-test="source-subscription-name"
|
data-test="source-subscription-name"
|
||||||
>
|
>
|
||||||
Test Subscription
|
Test Subscription
|
||||||
@@ -2626,12 +2626,12 @@ exports[`PreviewCopyJob should render with undefined database and container name
|
|||||||
class="ms-Stack css-124"
|
class="ms-Stack css-124"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="bold css-125"
|
class="bold themeText css-125"
|
||||||
>
|
>
|
||||||
Source account
|
Source account
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-125"
|
class="themeText css-125"
|
||||||
data-test="source-account-name"
|
data-test="source-account-name"
|
||||||
>
|
>
|
||||||
test-account
|
test-account
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/* eslint-disable react/prop-types */
|
/* eslint-disable react/prop-types */
|
||||||
/* eslint-disable react/display-name */
|
/* eslint-disable react/display-name */
|
||||||
import { Checkbox, Stack } from "@fluentui/react";
|
import { Checkbox, ICheckboxStyles, Stack } from "@fluentui/react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import ContainerCopyMessages from "../../../../ContainerCopyMessages";
|
import ContainerCopyMessages from "../../../../ContainerCopyMessages";
|
||||||
|
|
||||||
@@ -9,8 +9,25 @@ interface MigrationTypeCheckboxProps {
|
|||||||
onChange: (_ev?: React.FormEvent, checked?: boolean) => void;
|
onChange: (_ev?: React.FormEvent, checked?: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MigrationTypeCheckbox: React.FC<MigrationTypeCheckboxProps> = React.memo(({ checked, onChange }) => (
|
const checkboxStyles: ICheckboxStyles = {
|
||||||
<Stack horizontal horizontalAlign="space-between" className="migrationTypeRow" data-test="migration-type-checkbox">
|
text: { color: "var(--colorNeutralForeground1)" },
|
||||||
<Checkbox label={ContainerCopyMessages.migrationTypeCheckboxLabel} checked={checked} onChange={onChange} />
|
checkbox: { borderColor: "var(--colorNeutralStroke1)" },
|
||||||
</Stack>
|
root: {
|
||||||
));
|
selectors: {
|
||||||
|
":hover .ms-Checkbox-text": { color: "var(--colorNeutralForeground1)" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MigrationTypeCheckbox: React.FC<MigrationTypeCheckboxProps> = React.memo(({ checked, onChange }) => {
|
||||||
|
return (
|
||||||
|
<Stack horizontal horizontalAlign="space-between" className="migrationTypeRow" data-test="migration-type-checkbox">
|
||||||
|
<Checkbox
|
||||||
|
label={ContainerCopyMessages.migrationTypeCheckboxLabel}
|
||||||
|
checked={checked}
|
||||||
|
onChange={onChange}
|
||||||
|
styles={checkboxStyles}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const SelectAccount = React.memo(() => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack data-test="Panel:SelectAccountContainer" className="selectAccountContainer" tokens={{ childrenGap: 15 }}>
|
<Stack data-test="Panel:SelectAccountContainer" className="selectAccountContainer" tokens={{ childrenGap: 15 }}>
|
||||||
<Text>{ContainerCopyMessages.selectAccountDescription}</Text>
|
<Text className="themeText">{ContainerCopyMessages.selectAccountDescription}</Text>
|
||||||
|
|
||||||
<SubscriptionDropdown />
|
<SubscriptionDropdown />
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ exports[`SelectAccount Component Rendering should render correctly with snapshot
|
|||||||
data-test="Panel:SelectAccountContainer"
|
data-test="Panel:SelectAccountContainer"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="css-110"
|
class="themeText css-110"
|
||||||
>
|
>
|
||||||
Please select a source account from which to copy.
|
Please select a source account from which to copy.
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ const SelectSourceAndTargetContainers = ({ showAddCollectionPanel }: SelectSourc
|
|||||||
className="selectSourceAndTargetContainers"
|
className="selectSourceAndTargetContainers"
|
||||||
tokens={{ childrenGap: 25 }}
|
tokens={{ childrenGap: 25 }}
|
||||||
>
|
>
|
||||||
<span>{ContainerCopyMessages.selectSourceAndTargetContainersDescription}</span>
|
<span className="themeText">{ContainerCopyMessages.selectSourceAndTargetContainersDescription}</span>
|
||||||
<DatabaseContainerSection
|
<DatabaseContainerSection
|
||||||
heading={ContainerCopyMessages.sourceContainerSubHeading}
|
heading={ContainerCopyMessages.sourceContainerSubHeading}
|
||||||
databaseOptions={sourceDatabaseOptions}
|
databaseOptions={sourceDatabaseOptions}
|
||||||
|
|||||||
@@ -44,7 +44,11 @@ export const DatabaseContainerSection = ({
|
|||||||
data-test={`${sectionType}-containerDropdown`}
|
data-test={`${sectionType}-containerDropdown`}
|
||||||
/>
|
/>
|
||||||
{handleOnDemandCreateContainer && (
|
{handleOnDemandCreateContainer && (
|
||||||
<ActionButton className="create-container-link-btn" onClick={() => handleOnDemandCreateContainer()}>
|
<ActionButton
|
||||||
|
className="create-container-link-btn"
|
||||||
|
style={{ color: "var(--colorBrandForeground1)" }}
|
||||||
|
onClick={() => handleOnDemandCreateContainer()}
|
||||||
|
>
|
||||||
{ContainerCopyMessages.createContainerButtonLabel}
|
{ContainerCopyMessages.createContainerButtonLabel}
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { DetailsList, DetailsListLayoutMode, IColumn, Stack, Text } from "@fluentui/react";
|
import { DetailsList, DetailsListLayoutMode, IColumn, Stack, Text } from "@fluentui/react";
|
||||||
import React, { memo } from "react";
|
import React, { memo } from "react";
|
||||||
|
import { useThemeStore } from "../../../../hooks/useTheme";
|
||||||
import ContainerCopyMessages from "../../ContainerCopyMessages";
|
import ContainerCopyMessages from "../../ContainerCopyMessages";
|
||||||
import { CopyJobStatusType } from "../../Enums/CopyJobEnums";
|
import { CopyJobStatusType } from "../../Enums/CopyJobEnums";
|
||||||
import { CopyJobType } from "../../Types/CopyJobTypes";
|
import { CopyJobType } from "../../Types/CopyJobTypes";
|
||||||
@@ -63,6 +64,19 @@ const getCopyJobDetailsListColumns = (): IColumn[] => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const CopyJobDetails: React.FC<CopyJobDetailsProps> = ({ job }) => {
|
const CopyJobDetails: React.FC<CopyJobDetailsProps> = ({ job }) => {
|
||||||
|
const isDarkMode = useThemeStore((state) => state.isDarkMode);
|
||||||
|
|
||||||
|
const errorMessageStyle: React.CSSProperties = {
|
||||||
|
whiteSpace: "pre-wrap",
|
||||||
|
...(isDarkMode && {
|
||||||
|
whiteSpace: "pre-wrap",
|
||||||
|
backgroundColor: "var(--colorNeutralBackground2)",
|
||||||
|
color: "var(--colorNeutralForeground1)",
|
||||||
|
padding: "10px",
|
||||||
|
borderRadius: "4px",
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
const selectedContainers = [
|
const selectedContainers = [
|
||||||
{
|
{
|
||||||
sourceContainerName: job?.Source?.containerName || "N/A",
|
sourceContainerName: job?.Source?.containerName || "N/A",
|
||||||
@@ -77,10 +91,10 @@ const CopyJobDetails: React.FC<CopyJobDetailsProps> = ({ job }) => {
|
|||||||
<Stack className="copyJobDetailsContainer" tokens={{ childrenGap: 15 }} data-testid="copy-job-details">
|
<Stack className="copyJobDetailsContainer" tokens={{ childrenGap: 15 }} data-testid="copy-job-details">
|
||||||
{job.Error ? (
|
{job.Error ? (
|
||||||
<Stack.Item data-testid="error-stack" style={sectionCss.verticalAlign}>
|
<Stack.Item data-testid="error-stack" style={sectionCss.verticalAlign}>
|
||||||
<Text className="bold" style={sectionCss.headingText}>
|
<Text className="bold themeText" style={sectionCss.headingText}>
|
||||||
{ContainerCopyMessages.errorTitle}
|
{ContainerCopyMessages.errorTitle}
|
||||||
</Text>
|
</Text>
|
||||||
<Text as="pre" style={{ whiteSpace: "pre-wrap" }}>
|
<Text as="pre" style={errorMessageStyle}>
|
||||||
{job.Error.message}
|
{job.Error.message}
|
||||||
</Text>
|
</Text>
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
@@ -88,16 +102,16 @@ const CopyJobDetails: React.FC<CopyJobDetailsProps> = ({ job }) => {
|
|||||||
<Stack.Item data-testid="selectedcollection-stack">
|
<Stack.Item data-testid="selectedcollection-stack">
|
||||||
<Stack tokens={{ childrenGap: 15 }}>
|
<Stack tokens={{ childrenGap: 15 }}>
|
||||||
<Stack.Item style={sectionCss.verticalAlign}>
|
<Stack.Item style={sectionCss.verticalAlign}>
|
||||||
<Text className="bold">{ContainerCopyMessages.MonitorJobs.Columns.lastUpdatedTime}</Text>
|
<Text className="bold themeText">{ContainerCopyMessages.MonitorJobs.Columns.lastUpdatedTime}</Text>
|
||||||
<Text>{job.LastUpdatedTime}</Text>
|
<Text className="themeText">{job.LastUpdatedTime}</Text>
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
<Stack.Item style={sectionCss.verticalAlign}>
|
<Stack.Item style={sectionCss.verticalAlign}>
|
||||||
<Text className="bold">{ContainerCopyMessages.sourceAccountLabel}</Text>
|
<Text className="bold themeText">{ContainerCopyMessages.sourceAccountLabel}</Text>
|
||||||
<Text>{job.Source?.remoteAccountName}</Text>
|
<Text className="themeText">{job.Source?.remoteAccountName}</Text>
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
<Stack.Item style={sectionCss.verticalAlign}>
|
<Stack.Item style={sectionCss.verticalAlign}>
|
||||||
<Text className="bold">{ContainerCopyMessages.MonitorJobs.Columns.mode}</Text>
|
<Text className="bold themeText">{ContainerCopyMessages.MonitorJobs.Columns.mode}</Text>
|
||||||
<Text>{job.Mode}</Text>
|
<Text className="themeText">{job.Mode}</Text>
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
|
|||||||
@@ -1,30 +1,14 @@
|
|||||||
import { FontIcon, getTheme, mergeStyles, mergeStyleSets, Spinner, SpinnerSize, Stack, Text } from "@fluentui/react";
|
import { FontIcon, mergeStyles, Spinner, SpinnerSize, Stack, Text } from "@fluentui/react";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import ContainerCopyMessages from "../../ContainerCopyMessages";
|
import ContainerCopyMessages from "../../ContainerCopyMessages";
|
||||||
import { CopyJobStatusType } from "../../Enums/CopyJobEnums";
|
import { CopyJobStatusType } from "../../Enums/CopyJobEnums";
|
||||||
|
|
||||||
const theme = getTheme();
|
|
||||||
|
|
||||||
const iconClass = mergeStyles({
|
const iconClass = mergeStyles({
|
||||||
fontSize: "16px",
|
fontSize: "16px",
|
||||||
marginRight: "8px",
|
marginRight: "8px",
|
||||||
});
|
});
|
||||||
|
|
||||||
const classNames = mergeStyleSets({
|
|
||||||
[CopyJobStatusType.Pending]: [{ color: theme.semanticColors.bodySubtext }, iconClass],
|
|
||||||
[CopyJobStatusType.InProgress]: [{ color: theme.palette.themePrimary }, iconClass],
|
|
||||||
[CopyJobStatusType.Running]: [{ color: theme.palette.themePrimary }, iconClass],
|
|
||||||
[CopyJobStatusType.Partitioning]: [{ color: theme.palette.themePrimary }, iconClass],
|
|
||||||
[CopyJobStatusType.Paused]: [{ color: theme.palette.themePrimary }, iconClass],
|
|
||||||
[CopyJobStatusType.Skipped]: [{ color: theme.semanticColors.bodySubtext }, iconClass],
|
|
||||||
[CopyJobStatusType.Cancelled]: [{ color: theme.semanticColors.bodySubtext }, iconClass],
|
|
||||||
[CopyJobStatusType.Failed]: [{ color: theme.semanticColors.errorIcon }, iconClass],
|
|
||||||
[CopyJobStatusType.Faulted]: [{ color: theme.semanticColors.errorIcon }, iconClass],
|
|
||||||
[CopyJobStatusType.Completed]: [{ color: theme.semanticColors.successIcon }, iconClass],
|
|
||||||
unknown: [{ color: theme.semanticColors.bodySubtext }, iconClass],
|
|
||||||
});
|
|
||||||
|
|
||||||
const iconMap: Partial<Record<CopyJobStatusType, string>> = {
|
const iconMap: Partial<Record<CopyJobStatusType, string>> = {
|
||||||
[CopyJobStatusType.Pending]: "Clock",
|
[CopyJobStatusType.Pending]: "Clock",
|
||||||
[CopyJobStatusType.Paused]: "CirclePause",
|
[CopyJobStatusType.Paused]: "CirclePause",
|
||||||
@@ -35,6 +19,17 @@ const iconMap: Partial<Record<CopyJobStatusType, string>> = {
|
|||||||
[CopyJobStatusType.Completed]: "CompletedSolid",
|
[CopyJobStatusType.Completed]: "CompletedSolid",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Icon colors for different statuses
|
||||||
|
const statusIconColors: Partial<Record<CopyJobStatusType, string>> = {
|
||||||
|
[CopyJobStatusType.Failed]: "var(--colorPaletteRedForeground1)",
|
||||||
|
[CopyJobStatusType.Faulted]: "var(--colorPaletteRedForeground1)",
|
||||||
|
[CopyJobStatusType.Completed]: "var(--colorSuccessGreen)",
|
||||||
|
[CopyJobStatusType.InProgress]: "var(--colorBrandForeground1)",
|
||||||
|
[CopyJobStatusType.Running]: "var(--colorBrandForeground1)",
|
||||||
|
[CopyJobStatusType.Partitioning]: "var(--colorBrandForeground1)",
|
||||||
|
[CopyJobStatusType.Paused]: "var(--colorBrandForeground1)",
|
||||||
|
};
|
||||||
|
|
||||||
export interface CopyJobStatusWithIconProps {
|
export interface CopyJobStatusWithIconProps {
|
||||||
status: CopyJobStatusType;
|
status: CopyJobStatusType;
|
||||||
}
|
}
|
||||||
@@ -47,19 +42,17 @@ const CopyJobStatusWithIcon: React.FC<CopyJobStatusWithIconProps> = React.memo((
|
|||||||
CopyJobStatusType.InProgress,
|
CopyJobStatusType.InProgress,
|
||||||
CopyJobStatusType.Partitioning,
|
CopyJobStatusType.Partitioning,
|
||||||
].includes(status);
|
].includes(status);
|
||||||
|
const iconColor = statusIconColors[status] || "var(--colorNeutralForeground2)";
|
||||||
|
const iconStyle = mergeStyles(iconClass, { color: iconColor });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack horizontal verticalAlign="center">
|
<Stack horizontal verticalAlign="center">
|
||||||
{isSpinnerStatus ? (
|
{isSpinnerStatus ? (
|
||||||
<Spinner size={SpinnerSize.small} style={{ marginRight: "8px" }} />
|
<Spinner size={SpinnerSize.small} style={{ marginRight: "8px" }} />
|
||||||
) : (
|
) : (
|
||||||
<FontIcon
|
<FontIcon aria-label={status} iconName={iconMap[status] || "UnknownSolid"} className={iconStyle} />
|
||||||
aria-label={status}
|
|
||||||
iconName={iconMap[status] || "UnknownSolid"}
|
|
||||||
className={classNames[status] || classNames.unknown}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
<Text>{statusText}</Text>
|
<Text className="themeText">{statusText}</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import {
|
|||||||
} from "@fluentui/react";
|
} from "@fluentui/react";
|
||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
import Pager from "../../../../Common/Pager";
|
import Pager from "../../../../Common/Pager";
|
||||||
|
import { useThemeStore } from "../../../../hooks/useTheme";
|
||||||
|
import { getThemeTokens } from "../../../Theme/ThemeUtil";
|
||||||
import { openCopyJobDetailsPanel } from "../../Actions/CopyJobActions";
|
import { openCopyJobDetailsPanel } from "../../Actions/CopyJobActions";
|
||||||
import { CopyJobType, HandleJobActionClickType } from "../../Types/CopyJobTypes";
|
import { CopyJobType, HandleJobActionClickType } from "../../Types/CopyJobTypes";
|
||||||
import { getColumns } from "./CopyJobColumns";
|
import { getColumns } from "./CopyJobColumns";
|
||||||
@@ -26,13 +28,15 @@ interface CopyJobsListProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const styles = {
|
const styles = {
|
||||||
container: { height: "calc(100vh - 25em)" } as React.CSSProperties,
|
container: { height: "100%" } as React.CSSProperties,
|
||||||
stackItem: { position: "relative", marginBottom: "20px" } as React.CSSProperties,
|
stackItem: { position: "relative", marginBottom: "20px" } as React.CSSProperties,
|
||||||
};
|
};
|
||||||
|
|
||||||
const PAGE_SIZE = 10;
|
const PAGE_SIZE = 10;
|
||||||
|
|
||||||
const CopyJobsList: React.FC<CopyJobsListProps> = ({ jobs, handleActionClick, pageSize = PAGE_SIZE }) => {
|
const CopyJobsList: React.FC<CopyJobsListProps> = ({ jobs, handleActionClick, pageSize = PAGE_SIZE }) => {
|
||||||
|
const isDarkMode = useThemeStore((state) => state.isDarkMode);
|
||||||
|
const themeTokens = getThemeTokens(isDarkMode);
|
||||||
const [startIndex, setStartIndex] = React.useState(0);
|
const [startIndex, setStartIndex] = React.useState(0);
|
||||||
const [sortedJobs, setSortedJobs] = React.useState<CopyJobType[]>(jobs);
|
const [sortedJobs, setSortedJobs] = React.useState<CopyJobType[]>(jobs);
|
||||||
const [sortedColumnKey, setSortedColumnKey] = React.useState<string | undefined>(undefined);
|
const [sortedColumnKey, setSortedColumnKey] = React.useState<string | undefined>(undefined);
|
||||||
@@ -88,11 +92,28 @@ const CopyJobsList: React.FC<CopyJobsListProps> = ({ jobs, handleActionClick, pa
|
|||||||
enableShimmer={false}
|
enableShimmer={false}
|
||||||
constrainMode={ConstrainMode.unconstrained}
|
constrainMode={ConstrainMode.unconstrained}
|
||||||
layoutMode={DetailsListLayoutMode.justified}
|
layoutMode={DetailsListLayoutMode.justified}
|
||||||
onRenderDetailsHeader={(props, defaultRender) => (
|
onRenderDetailsHeader={(props, defaultRender) => {
|
||||||
<Sticky stickyPosition={StickyPositionType.Header} isScrollSynced>
|
const bgColor = themeTokens.colorNeutralBackground3;
|
||||||
{defaultRender({ ...props })}
|
const textColor = themeTokens.colorNeutralForeground1;
|
||||||
</Sticky>
|
return (
|
||||||
)}
|
<Sticky stickyPosition={StickyPositionType.Header} isScrollSynced stickyBackgroundColor={bgColor}>
|
||||||
|
<div style={{ backgroundColor: bgColor }}>
|
||||||
|
{defaultRender({
|
||||||
|
...props,
|
||||||
|
styles: {
|
||||||
|
root: {
|
||||||
|
backgroundColor: bgColor,
|
||||||
|
selectors: {
|
||||||
|
".ms-DetailsHeader-cellTitle": { color: textColor },
|
||||||
|
".ms-DetailsHeader-cellName": { color: textColor },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</Sticky>
|
||||||
|
);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</ScrollablePane>
|
</ScrollablePane>
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ exports[`CopyJobStatusWithIcon Spinner Status Types renders InProgress with spin
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
class="css-112"
|
class="themeText css-112"
|
||||||
>
|
>
|
||||||
Running
|
Running
|
||||||
</span>
|
</span>
|
||||||
@@ -33,7 +33,7 @@ exports[`CopyJobStatusWithIcon Spinner Status Types renders Partitioning with sp
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
class="css-112"
|
class="themeText css-112"
|
||||||
>
|
>
|
||||||
Running
|
Running
|
||||||
</span>
|
</span>
|
||||||
@@ -53,7 +53,7 @@ exports[`CopyJobStatusWithIcon Spinner Status Types renders Running with spinner
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
class="css-112"
|
class="themeText css-112"
|
||||||
>
|
>
|
||||||
Running
|
Running
|
||||||
</span>
|
</span>
|
||||||
@@ -66,7 +66,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
|
|||||||
>
|
>
|
||||||
<i
|
<i
|
||||||
aria-label="Cancelled"
|
aria-label="Cancelled"
|
||||||
class="ms-Icon root-105 css-118 mocked-style-Cancelled"
|
class="ms-Icon root-105 css-118 mocked-styles"
|
||||||
data-icon-name="StatusErrorFull"
|
data-icon-name="StatusErrorFull"
|
||||||
role="img"
|
role="img"
|
||||||
style="font-family: "FabricMDL2Icons-4";"
|
style="font-family: "FabricMDL2Icons-4";"
|
||||||
@@ -74,7 +74,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
|
|||||||
|
|
||||||
</i>
|
</i>
|
||||||
<span
|
<span
|
||||||
class="css-112"
|
class="themeText css-112"
|
||||||
>
|
>
|
||||||
Cancelled
|
Cancelled
|
||||||
</span>
|
</span>
|
||||||
@@ -87,7 +87,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
|
|||||||
>
|
>
|
||||||
<i
|
<i
|
||||||
aria-label="Completed"
|
aria-label="Completed"
|
||||||
class="ms-Icon root-105 css-120 mocked-style-Completed"
|
class="ms-Icon root-105 css-120 mocked-styles"
|
||||||
data-icon-name="CompletedSolid"
|
data-icon-name="CompletedSolid"
|
||||||
role="img"
|
role="img"
|
||||||
style="font-family: "FabricMDL2Icons-5";"
|
style="font-family: "FabricMDL2Icons-5";"
|
||||||
@@ -95,7 +95,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
|
|||||||
|
|
||||||
</i>
|
</i>
|
||||||
<span
|
<span
|
||||||
class="css-112"
|
class="themeText css-112"
|
||||||
>
|
>
|
||||||
Completed
|
Completed
|
||||||
</span>
|
</span>
|
||||||
@@ -108,7 +108,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
|
|||||||
>
|
>
|
||||||
<i
|
<i
|
||||||
aria-label="Failed"
|
aria-label="Failed"
|
||||||
class="ms-Icon root-105 css-118 mocked-style-Failed"
|
class="ms-Icon root-105 css-118 mocked-styles"
|
||||||
data-icon-name="StatusErrorFull"
|
data-icon-name="StatusErrorFull"
|
||||||
role="img"
|
role="img"
|
||||||
style="font-family: "FabricMDL2Icons-4";"
|
style="font-family: "FabricMDL2Icons-4";"
|
||||||
@@ -116,7 +116,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
|
|||||||
|
|
||||||
</i>
|
</i>
|
||||||
<span
|
<span
|
||||||
class="css-112"
|
class="themeText css-112"
|
||||||
>
|
>
|
||||||
Failed
|
Failed
|
||||||
</span>
|
</span>
|
||||||
@@ -129,7 +129,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
|
|||||||
>
|
>
|
||||||
<i
|
<i
|
||||||
aria-label="Faulted"
|
aria-label="Faulted"
|
||||||
class="ms-Icon root-105 css-118 mocked-style-Faulted"
|
class="ms-Icon root-105 css-118 mocked-styles"
|
||||||
data-icon-name="StatusErrorFull"
|
data-icon-name="StatusErrorFull"
|
||||||
role="img"
|
role="img"
|
||||||
style="font-family: "FabricMDL2Icons-4";"
|
style="font-family: "FabricMDL2Icons-4";"
|
||||||
@@ -137,7 +137,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
|
|||||||
|
|
||||||
</i>
|
</i>
|
||||||
<span
|
<span
|
||||||
class="css-112"
|
class="themeText css-112"
|
||||||
>
|
>
|
||||||
Failed
|
Failed
|
||||||
</span>
|
</span>
|
||||||
@@ -150,7 +150,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
|
|||||||
>
|
>
|
||||||
<i
|
<i
|
||||||
aria-label="Paused"
|
aria-label="Paused"
|
||||||
class="ms-Icon root-105 css-114 mocked-style-Paused"
|
class="ms-Icon root-105 css-114 mocked-styles"
|
||||||
data-icon-name="CirclePause"
|
data-icon-name="CirclePause"
|
||||||
role="img"
|
role="img"
|
||||||
style="font-family: "FabricMDL2Icons-11";"
|
style="font-family: "FabricMDL2Icons-11";"
|
||||||
@@ -158,7 +158,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
|
|||||||
|
|
||||||
</i>
|
</i>
|
||||||
<span
|
<span
|
||||||
class="css-112"
|
class="themeText css-112"
|
||||||
>
|
>
|
||||||
Paused
|
Paused
|
||||||
</span>
|
</span>
|
||||||
@@ -171,7 +171,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
|
|||||||
>
|
>
|
||||||
<i
|
<i
|
||||||
aria-label="Pending"
|
aria-label="Pending"
|
||||||
class="ms-Icon root-105 css-111 mocked-style-Pending"
|
class="ms-Icon root-105 css-111 mocked-styles"
|
||||||
data-icon-name="Clock"
|
data-icon-name="Clock"
|
||||||
role="img"
|
role="img"
|
||||||
style="font-family: "FabricMDL2Icons-2";"
|
style="font-family: "FabricMDL2Icons-2";"
|
||||||
@@ -179,7 +179,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
|
|||||||
|
|
||||||
</i>
|
</i>
|
||||||
<span
|
<span
|
||||||
class="css-112"
|
class="themeText css-112"
|
||||||
>
|
>
|
||||||
Queued
|
Queued
|
||||||
</span>
|
</span>
|
||||||
@@ -192,7 +192,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
|
|||||||
>
|
>
|
||||||
<i
|
<i
|
||||||
aria-label="Skipped"
|
aria-label="Skipped"
|
||||||
class="ms-Icon root-105 css-116 mocked-style-Skipped"
|
class="ms-Icon root-105 css-116 mocked-styles"
|
||||||
data-icon-name="StatusCircleBlock2"
|
data-icon-name="StatusCircleBlock2"
|
||||||
role="img"
|
role="img"
|
||||||
style="font-family: "FabricMDL2Icons-9";"
|
style="font-family: "FabricMDL2Icons-9";"
|
||||||
@@ -200,7 +200,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
|
|||||||
|
|
||||||
</i>
|
</i>
|
||||||
<span
|
<span
|
||||||
class="css-112"
|
class="themeText css-112"
|
||||||
>
|
>
|
||||||
Cancelled
|
Cancelled
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import CopyJobsNotFound from "../MonitorCopyJobs/Components/CopyJobs.NotFound";
|
|||||||
import { CopyJobType, JobActionUpdatorType } from "../Types/CopyJobTypes";
|
import { CopyJobType, JobActionUpdatorType } from "../Types/CopyJobTypes";
|
||||||
import CopyJobsList from "./Components/CopyJobsList";
|
import CopyJobsList from "./Components/CopyJobsList";
|
||||||
|
|
||||||
const FETCH_INTERVAL_MS = 30 * 1000;
|
const FETCH_INTERVAL = 2 * 60 * 1000;
|
||||||
const SHIMMER_INDENT_LEVELS: IndentLevel[] = Array(7).fill({ level: 0, width: "100%" });
|
const SHIMMER_INDENT_LEVELS: IndentLevel[] = Array(7).fill({ level: 0, width: "100%" });
|
||||||
|
|
||||||
interface MonitorCopyJobsProps {
|
interface MonitorCopyJobsProps {
|
||||||
@@ -57,7 +57,7 @@ const MonitorCopyJobs = forwardRef<MonitorCopyJobsRef, MonitorCopyJobsProps>(({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchJobs();
|
fetchJobs();
|
||||||
const intervalId = setInterval(fetchJobs, FETCH_INTERVAL_MS);
|
const intervalId = setInterval(fetchJobs, FETCH_INTERVAL);
|
||||||
|
|
||||||
return () => clearInterval(intervalId);
|
return () => clearInterval(intervalId);
|
||||||
}, [fetchJobs]);
|
}, [fetchJobs]);
|
||||||
|
|||||||
@@ -1,6 +1,30 @@
|
|||||||
@import "../../../less/Common/Constants.less";
|
@import "../../../less/Common/Constants.less";
|
||||||
|
|
||||||
|
// Common theme-aware classes
|
||||||
|
.themeText {
|
||||||
|
color: var(--colorNeutralForeground1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.themeTextSecondary {
|
||||||
|
color: var(--colorNeutralForeground2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.themeLinkText {
|
||||||
|
color: var(--colorBrandForeground1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.themeBackground {
|
||||||
|
background-color: var(--colorNeutralBackground1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.themeBackgroundSecondary {
|
||||||
|
background-color: var(--colorNeutralBackground2);
|
||||||
|
}
|
||||||
|
|
||||||
#containerCopyWrapper {
|
#containerCopyWrapper {
|
||||||
|
background-color: var(--colorNeutralBackground1);
|
||||||
|
color: var(--colorNeutralForeground1);
|
||||||
|
|
||||||
.centerContent {
|
.centerContent {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -9,21 +33,31 @@
|
|||||||
.noCopyJobsMessage {
|
.noCopyJobsMessage {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
color: @FocusColor;
|
color: var(--colorNeutralForeground2);
|
||||||
}
|
}
|
||||||
button.createCopyJobButton {
|
button.createCopyJobButton {
|
||||||
color: @LinkColor;
|
color: var(--colorBrandForeground1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.createCopyJobScreensContainer {
|
.createCopyJobScreensContainer {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 1em 1.5em;
|
padding: 1em 1.5em;
|
||||||
|
background-color: var(--colorNeutralBackground1);
|
||||||
|
color: var(--colorNeutralForeground1);
|
||||||
|
|
||||||
.pointInTimeRestoreContainer, .onlineCopyContainer {
|
.pointInTimeRestoreContainer, .onlineCopyContainer {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.toggle-label {
|
||||||
|
color: var(--colorNeutralForeground1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordionHeaderText {
|
||||||
|
color: var(--colorNeutralForeground1);
|
||||||
|
}
|
||||||
|
|
||||||
label {
|
label {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
@@ -71,7 +105,7 @@
|
|||||||
}
|
}
|
||||||
.foreground {
|
.foreground {
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
background-color: #f9f9f9;
|
background-color: var(--colorNeutralBackground2);
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||||
transform: translate(0%, -9%);
|
transform: translate(0%, -9%);
|
||||||
@@ -80,14 +114,40 @@
|
|||||||
.createCopyJobErrorMessageBar {
|
.createCopyJobErrorMessageBar {
|
||||||
margin-bottom: 2em;
|
margin-bottom: 2em;
|
||||||
}
|
}
|
||||||
|
body.isDarkMode & {
|
||||||
|
.ms-TooltipHost .ms-Image {
|
||||||
|
filter: invert(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms-TextField {
|
||||||
|
.ms-TextField-fieldGroup {
|
||||||
|
background-color: var(--colorNeutralBackground1);
|
||||||
|
border-color: var(--colorNeutralStroke1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms-TextField-field {
|
||||||
|
color: var(--colorNeutralForeground1);
|
||||||
|
background-color: var(--colorNeutralBackground1);
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: var(--colorNeutralForeground4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms-Label {
|
||||||
|
color: var(--colorNeutralForeground1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
.create-container-link-btn {
|
.create-container-link-btn {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
height: 25px;
|
height: 25px;
|
||||||
color: @LinkColor;
|
color: var(--colorBrandForeground1);
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Create collection panel */
|
/* Create collection panel */
|
||||||
@@ -105,7 +165,6 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
|
||||||
.ms-DetailsList {
|
.ms-DetailsList {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
@@ -114,33 +173,33 @@
|
|||||||
padding: @DefaultSpace 20px;
|
padding: @DefaultSpace 20px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: @DefaultFontSize;
|
font-size: @DefaultFontSize;
|
||||||
color: @BaseHigh;
|
color: var(--colorNeutralForeground1);
|
||||||
background-color: @BaseLow;
|
background-color: var(--colorNeutralBackground2);
|
||||||
border-bottom: @ButtonBorderWidth solid @BaseMedium;
|
border-bottom: @ButtonBorderWidth solid var(--colorNeutralStroke1);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: @BaseMediumLow;
|
background-color: var(--colorNeutralBackground3);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ms-DetailsRow {
|
.ms-DetailsRow {
|
||||||
border-bottom: @ButtonBorderWidth solid @BaseMedium;
|
border-bottom: @ButtonBorderWidth solid var(--colorNeutralStroke1);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: @BaseMediumLow;
|
background-color: var(--colorNeutralBackground2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ms-DetailsRow-cell {
|
.ms-DetailsRow-cell {
|
||||||
padding: @MediumSpace 20px;
|
padding: @MediumSpace 20px;
|
||||||
font-size: @DefaultFontSize;
|
font-size: @DefaultFontSize;
|
||||||
color: @BaseHigh;
|
color: var(--colorNeutralForeground1);
|
||||||
min-height: 48px;
|
min-height: 48px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
.jobNameLink {
|
.jobNameLink {
|
||||||
color: @LinkColor;
|
color: var(--colorBrandForeground1);
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -168,7 +227,7 @@
|
|||||||
}
|
}
|
||||||
.ms-DetailsRow-cell {
|
.ms-DetailsRow-cell {
|
||||||
font-size: @DefaultFontSize;
|
font-size: @DefaultFontSize;
|
||||||
color: @BaseHigh;
|
color: var(--colorNeutralForeground1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,7 +103,10 @@ export const ThroughputBucketsComponent: FC<ThroughputBucketsComponentProps> = (
|
|||||||
offText="Inactive"
|
offText="Inactive"
|
||||||
checked={bucket.maxThroughputPercentage !== 100}
|
checked={bucket.maxThroughputPercentage !== 100}
|
||||||
onChange={(event, checked) => onToggle(bucket.id, checked)}
|
onChange={(event, checked) => onToggle(bucket.id, checked)}
|
||||||
styles={{ root: { marginBottom: 0 }, text: { fontSize: 12 } }}
|
styles={{
|
||||||
|
root: { marginBottom: 0 },
|
||||||
|
text: { fontSize: 12, color: "var(--colorNeutralForeground1)" },
|
||||||
|
}}
|
||||||
></Toggle>
|
></Toggle>
|
||||||
</Stack>
|
</Stack>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ type VectorEmbeddingPolicyProperty = "dataType" | "distanceFunction" | "indexTyp
|
|||||||
const labelStyles = {
|
const labelStyles = {
|
||||||
root: {
|
root: {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
|
color: "var(--colorNeutralForeground1)",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -63,6 +64,8 @@ const textFieldStyles: IStyleFunctionOrObject<ITextFieldStyleProps, ITextFieldSt
|
|||||||
field: {
|
field: {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
padding: "0 8px",
|
padding: "0 8px",
|
||||||
|
backgroundColor: "var(--colorNeutralBackground1)",
|
||||||
|
color: "var(--colorNeutralForeground1)",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
? this.refreshDatabaseForResourceToken()
|
||||||
|
: this.refreshAllDatabases());
|
||||||
|
await this.refreshNotebookList();
|
||||||
}
|
}
|
||||||
|
|
||||||
await (userContext.authType === AuthType.ResourceToken
|
logConsoleInfo("Successfully refreshed databases");
|
||||||
? this.refreshDatabaseForResourceToken()
|
|
||||||
: this.refreshAllDatabases());
|
|
||||||
await this.refreshNotebookList();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Facade
|
// Facade
|
||||||
|
|||||||
@@ -853,7 +853,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
|
|
||||||
{!isSynapseLinkEnabled() && (
|
{!isSynapseLinkEnabled() && (
|
||||||
<Stack className="panelGroupSpacing">
|
<Stack className="panelGroupSpacing">
|
||||||
<Text variant="small">
|
<Text variant="small" style={{ color: "var(--colorNeutralForeground1)" }}>
|
||||||
Azure Synapse Link is required for creating an analytical store{" "}
|
Azure Synapse Link is required for creating an analytical store{" "}
|
||||||
{getCollectionName().toLocaleLowerCase()}. Enable Synapse Link for this Cosmos DB account. <br />
|
{getCollectionName().toLocaleLowerCase()}. Enable Synapse Link for this Cosmos DB account. <br />
|
||||||
<Link
|
<Link
|
||||||
|
|||||||
@@ -475,6 +475,11 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
|||||||
className="panelGroupSpacing"
|
className="panelGroupSpacing"
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"color": "var(--colorNeutralForeground1)",
|
||||||
|
}
|
||||||
|
}
|
||||||
variant="small"
|
variant="small"
|
||||||
>
|
>
|
||||||
Azure Synapse Link is required for creating an analytical store
|
Azure Synapse Link is required for creating an analytical store
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React from "react";
|
|||||||
import LoadingIndicator_3Squares from "../../../images/LoadingIndicator_3Squares.gif";
|
import LoadingIndicator_3Squares from "../../../images/LoadingIndicator_3Squares.gif";
|
||||||
|
|
||||||
export const PanelLoadingScreen: React.FunctionComponent = () => (
|
export const PanelLoadingScreen: React.FunctionComponent = () => (
|
||||||
<div id="loadingScreen" className="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer">
|
<div id="loadingScreen" className="dataExplorerLoaderContainer dataExplorerLoaderforcopyJobs">
|
||||||
<img className="dataExplorerLoader" src={LoadingIndicator_3Squares} />
|
<img className="dataExplorerLoader" src={LoadingIndicator_3Squares} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -316,11 +316,6 @@ body.isDarkMode {
|
|||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
// High specificity override for any nested elements
|
|
||||||
* {
|
|
||||||
color: var(--colorNeutralForeground1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure links maintain proper colors
|
// Ensure links maintain proper colors
|
||||||
.ms-Link {
|
.ms-Link {
|
||||||
color: var(--colorBrandForeground1);
|
color: var(--colorBrandForeground1);
|
||||||
@@ -438,7 +433,6 @@ body.isDarkMode {
|
|||||||
|
|
||||||
button {
|
button {
|
||||||
&:not(.ms-Button):not(.ms-IconButton) {
|
&:not(.ms-Button):not(.ms-IconButton) {
|
||||||
background-color: var(--colorNeutralBackground1);
|
|
||||||
color: var(--colorNeutralForeground1);
|
color: var(--colorNeutralForeground1);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
--colorCompoundBrandStroke1: @SelectionColor;
|
--colorCompoundBrandStroke1: @SelectionColor;
|
||||||
--colorBrandForeground1: @LinkColor;
|
--colorBrandForeground1: @LinkColor;
|
||||||
--colorPaletteRedForeground1: @ErrorColor;
|
--colorPaletteRedForeground1: @ErrorColor;
|
||||||
|
--colorSuccessGreen: #107c10;
|
||||||
--overlayBackground: rgba(0, 0, 0, 0.4);
|
--overlayBackground: rgba(0, 0, 0, 0.4);
|
||||||
--colorBrandBackground: @SelectionColor;
|
--colorBrandBackground: @SelectionColor;
|
||||||
--colorBrandBackgroundHover: @AccentMediumHigh;
|
--colorBrandBackgroundHover: @AccentMediumHigh;
|
||||||
@@ -32,6 +33,7 @@ body.isDarkMode {
|
|||||||
--colorCompoundBrandStroke1: #4db6e8;
|
--colorCompoundBrandStroke1: #4db6e8;
|
||||||
--colorBrandForeground1: #4db6e8;
|
--colorBrandForeground1: #4db6e8;
|
||||||
--colorPaletteRedForeground1: #f25d5d;
|
--colorPaletteRedForeground1: #f25d5d;
|
||||||
|
--colorSuccessGreen: #107c10;
|
||||||
--overlayBackground: rgba(0, 0, 0, 0.4);
|
--overlayBackground: rgba(0, 0, 0, 0.4);
|
||||||
--colorBrandBackground: #0078d4;
|
--colorBrandBackground: #0078d4;
|
||||||
--colorBrandBackgroundHover: #106ebe;
|
--colorBrandBackgroundHover: #106ebe;
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
215
test/fx.ts
215
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,10 +529,35 @@ 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> {
|
async getDropdownItemByName(name: string, ariaLabel?: string): Promise<Locator> {
|
||||||
const dropdownItemsWrapper = this.frame.locator("div.ms-Dropdown-items");
|
const dropdownItemsWrapper = this.frame.locator("div.ms-Dropdown-items");
|
||||||
if (ariaLabel) {
|
if (ariaLabel) {
|
||||||
@@ -480,7 +568,7 @@ export class DataExplorer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 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");
|
||||||
@@ -492,15 +580,126 @@ export class DataExplorer {
|
|||||||
throw new Error("Explorer frame not found");
|
throw new Error("Explorer frame not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
await explorerFrame?.getByTestId("DataExplorerRoot").waitFor();
|
if (!options?.enablecontainercopy) {
|
||||||
|
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) => {
|
||||||
|
|||||||
493
test/sql/containercopy.spec.ts
Normal file
493
test/sql/containercopy.spec.ts
Normal file
@@ -0,0 +1,493 @@
|
|||||||
|
/* 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 = "";
|
||||||
|
const VISIBLE_TIMEOUT_MS = 30 * 1000;
|
||||||
|
|
||||||
|
test.describe.configure({ mode: "serial" });
|
||||||
|
|
||||||
|
test.describe("Container Copy", () => {
|
||||||
|
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({ timeout: VISIBLE_TIMEOUT_MS });
|
||||||
|
await expect(wrapper.getByTestId("CommandBar/Button:Refresh")).toBeVisible({ timeout: VISIBLE_TIMEOUT_MS });
|
||||||
|
await expect(wrapper.getByTestId("CommandBar/Button:Feedback")).toBeVisible({ timeout: VISIBLE_TIMEOUT_MS });
|
||||||
|
});
|
||||||
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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
|
||||||
await context?.dispose();
|
if (!process.env.CI) {
|
||||||
});
|
test.afterAll("Delete Test Database", async () => {
|
||||||
|
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
|
||||||
|
|||||||
@@ -1,98 +1,104 @@
|
|||||||
import { expect, Page, test } from "@playwright/test";
|
// import { expect, test } from "@playwright/test";
|
||||||
import { DataExplorer, TestAccount } from "../../fx";
|
// import { DataExplorer, getDropdownItemByNameOrPosition, TestAccount } from "../../fx";
|
||||||
import { createTestSQLContainer, TestContainerContext } from "../../testData";
|
// import { createTestSQLContainer, TestContainerContext } from "../../testData";
|
||||||
|
|
||||||
test.describe("Change Partition Key", () => {
|
// test.describe("Change Partition Key", () => {
|
||||||
let pageInstance: Page;
|
// let context: TestContainerContext = null!;
|
||||||
let context: TestContainerContext = null!;
|
// let explorer: DataExplorer = null!;
|
||||||
let explorer: DataExplorer = null!;
|
// const newPartitionKeyPath = "newPartitionKey";
|
||||||
const newPartitionKeyPath = "/newPartitionKey";
|
// const newContainerId = "testcontainer_1";
|
||||||
const newContainerId = "testcontainer_1";
|
|
||||||
|
|
||||||
test.beforeAll("Create Test Database", async () => {
|
// test.beforeAll("Create Test Database", async () => {
|
||||||
context = await createTestSQLContainer();
|
// context = await createTestSQLContainer();
|
||||||
});
|
// });
|
||||||
|
|
||||||
test.beforeEach("Open container settings", async ({ page }) => {
|
// test.beforeEach("Open container settings", async ({ page }) => {
|
||||||
pageInstance = page;
|
// explorer = await DataExplorer.open(page, TestAccount.SQL);
|
||||||
explorer = await DataExplorer.open(page, TestAccount.SQL);
|
|
||||||
|
|
||||||
// Click Scale & Settings and open Partition Key tab
|
// // Click Scale & Settings and open Partition Key tab
|
||||||
await explorer.openScaleAndSettings(context);
|
// await explorer.openScaleAndSettings(context);
|
||||||
const PartitionKeyTab = explorer.frame.getByTestId("settings-tab-header/PartitionKeyTab");
|
// const PartitionKeyTab = explorer.frame.getByTestId("settings-tab-header/PartitionKeyTab");
|
||||||
await PartitionKeyTab.click();
|
// await expect(PartitionKeyTab).toBeVisible();
|
||||||
});
|
// await PartitionKeyTab.click();
|
||||||
|
// });
|
||||||
|
|
||||||
test.afterAll("Delete Test Database", async () => {
|
// // Delete database only if not running in CI
|
||||||
await context?.dispose();
|
// if (!process.env.CI) {
|
||||||
});
|
// test.afterEach("Delete Test Database", async () => {
|
||||||
|
// await context?.dispose();
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
test("Change partition key path", async () => {
|
// test("Change partition key path", async () => {
|
||||||
await expect(explorer.frame.getByText("/partitionKey")).toBeVisible();
|
// await expect(explorer.frame.getByText("/partitionKey")).toBeVisible();
|
||||||
await expect(explorer.frame.getByText("Change partition key")).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 safeguard the integrity of/)).toBeVisible();
|
||||||
await expect(explorer.frame.getByText(/To change the partition key/)).toBeVisible();
|
// await expect(explorer.frame.getByText(/To change the partition key/)).toBeVisible();
|
||||||
|
|
||||||
const changePartitionKeyButton = explorer.frame.getByTestId("change-partition-key-button");
|
// const changePartitionKeyButton = explorer.frame.getByTestId("change-partition-key-button");
|
||||||
expect(changePartitionKeyButton).toBeVisible();
|
// expect(changePartitionKeyButton).toBeVisible();
|
||||||
await changePartitionKeyButton.click();
|
// await changePartitionKeyButton.click();
|
||||||
|
|
||||||
// Fill out new partition key form in the panel
|
// // Fill out new partition key form in the panel
|
||||||
const changePkPanel = explorer.frame.getByTestId(`Panel:Change partition key`);
|
// const changePkPanel = explorer.frame.getByTestId(`Panel:Change partition key`);
|
||||||
await expect(changePkPanel.getByText(context.database.id)).toBeVisible();
|
// await expect(changePkPanel.getByText(context.database.id)).toBeVisible();
|
||||||
await expect(explorer.frame.getByRole("heading", { name: "Change partition key" })).toBeVisible();
|
// await expect(explorer.frame.getByRole("heading", { name: "Change partition key" })).toBeVisible();
|
||||||
await expect(explorer.frame.getByText(/When changing a container/)).toBeVisible();
|
// await expect(explorer.frame.getByText(/When changing a container/)).toBeVisible();
|
||||||
|
|
||||||
// Try to switch to new container
|
// // Try to switch to new container
|
||||||
await expect(changePkPanel.getByText("New container")).toBeVisible();
|
// await expect(changePkPanel.getByText("New container")).toBeVisible();
|
||||||
await expect(changePkPanel.getByText("Existing container")).toBeVisible();
|
// await expect(changePkPanel.getByText("Existing container")).toBeVisible();
|
||||||
await expect(changePkPanel.getByTestId("new-container-id-input")).toBeVisible();
|
// await expect(changePkPanel.getByTestId("new-container-id-input")).toBeVisible();
|
||||||
|
|
||||||
changePkPanel.getByTestId("new-container-id-input").fill(newContainerId);
|
// changePkPanel.getByTestId("new-container-id-input").fill(newContainerId);
|
||||||
await expect(changePkPanel.getByTestId("new-container-partition-key-input")).toBeVisible();
|
// await expect(changePkPanel.getByTestId("new-container-partition-key-input")).toBeVisible();
|
||||||
changePkPanel.getByTestId("new-container-partition-key-input").fill(newPartitionKeyPath);
|
// changePkPanel.getByTestId("new-container-partition-key-input").fill(newPartitionKeyPath);
|
||||||
|
|
||||||
await expect(changePkPanel.getByTestId("add-sub-partition-key-button")).toBeVisible();
|
// await expect(changePkPanel.getByTestId("add-sub-partition-key-button")).toBeVisible();
|
||||||
changePkPanel.getByTestId("add-sub-partition-key-button").click();
|
// changePkPanel.getByTestId("add-sub-partition-key-button").click();
|
||||||
await expect(changePkPanel.getByTestId("new-container-sub-partition-key-input-0")).toBeVisible();
|
// 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("remove-sub-partition-key-button-0")).toBeVisible();
|
||||||
await expect(changePkPanel.getByTestId("hierarchical-partitioning-info-text")).toBeVisible();
|
// await expect(changePkPanel.getByTestId("hierarchical-partitioning-info-text")).toBeVisible();
|
||||||
changePkPanel.getByTestId("new-container-sub-partition-key-input-0").fill("/customerId");
|
// await changePkPanel.getByTestId("remove-sub-partition-key-button-0").click();
|
||||||
|
|
||||||
await changePkPanel.getByTestId("Panel/OkButton").click();
|
// await changePkPanel.getByTestId("Panel/OkButton").click();
|
||||||
|
|
||||||
await pageInstance.waitForLoadState("networkidle");
|
// await expect(changePkPanel).not.toBeVisible({ timeout: 5 * 60 * 1000 });
|
||||||
await expect(changePkPanel).not.toBeVisible({ timeout: 60 * 1000 });
|
|
||||||
|
|
||||||
// Verify partition key change job
|
// // Verify partition key change job
|
||||||
const jobText = explorer.frame.getByText(/Partition key change job/);
|
// const jobText = explorer.frame.getByText(/Partition key change job/);
|
||||||
await expect(jobText).toBeVisible();
|
// await expect(jobText).toBeVisible();
|
||||||
await expect(explorer.frame.locator(".ms-ProgressIndicator-itemName")).toContainText("Portal_testcontainer_1");
|
// await expect(explorer.frame.locator(".ms-ProgressIndicator-itemName")).toContainText("Portal_testcontainer_1");
|
||||||
|
|
||||||
const jobRow = explorer.frame.locator(".ms-ProgressIndicator-itemDescription");
|
// const jobRow = explorer.frame.locator(".ms-ProgressIndicator-itemDescription");
|
||||||
await expect(jobRow.getByText("Completed")).toBeVisible({ timeout: 30 * 1000 });
|
// // 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);
|
// const newContainerNode = await explorer.waitForContainerNode(context.database.id, newContainerId);
|
||||||
expect(newContainerNode).not.toBeNull();
|
// expect(newContainerNode).not.toBeNull();
|
||||||
|
|
||||||
// Now try to switch to existing container
|
// // Now try to switch to existing container
|
||||||
await changePartitionKeyButton.click();
|
// await changePartitionKeyButton.click();
|
||||||
await changePkPanel.getByText("Existing container").click();
|
// await changePkPanel.getByText("Existing container").click();
|
||||||
await changePkPanel.getByLabel("Use existing container").check();
|
// await changePkPanel.getByLabel("Use existing container").check();
|
||||||
await changePkPanel.getByText("Choose an existing container").click();
|
// await changePkPanel.getByText("Choose an existing container").click();
|
||||||
|
|
||||||
const containerDropdownItem = await explorer.getDropdownItemByName(newContainerId, "Existing Containers");
|
// const containerDropdownItem = await getDropdownItemByNameOrPosition(
|
||||||
await containerDropdownItem.click();
|
// explorer.frame,
|
||||||
|
// { name: newContainerId },
|
||||||
|
// { ariaLabel: "Existing Containers" },
|
||||||
|
// );
|
||||||
|
// await containerDropdownItem.click();
|
||||||
|
|
||||||
await changePkPanel.getByTestId("Panel/OkButton").click();
|
// await changePkPanel.getByTestId("Panel/OkButton").click();
|
||||||
await explorer.frame.getByRole("button", { name: "Cancel" }).click();
|
// await explorer.frame.getByRole("button", { name: "Cancel" }).click();
|
||||||
|
|
||||||
// Dismiss overlay if it appears
|
// // Dismiss overlay if it appears
|
||||||
const overlayFrame = explorer.frame.locator("#webpack-dev-server-client-overlay").first();
|
// const overlayFrame = explorer.frame.locator("#webpack-dev-server-client-overlay").first();
|
||||||
if (await overlayFrame.count()) {
|
// if (await overlayFrame.count()) {
|
||||||
await overlayFrame.contentFrame().getByLabel("Dismiss").click();
|
// await overlayFrame.contentFrame().getByLabel("Dismiss").click();
|
||||||
}
|
// }
|
||||||
const cancelledJobRow = explorer.frame.getByTestId("Tab:tab0");
|
// const cancelledJobRow = explorer.frame.getByTestId("Tab:tab0");
|
||||||
await expect(cancelledJobRow.getByText("Cancelled")).toBeVisible({ timeout: 30 * 1000 });
|
// 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({ includeTestData: 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({ includeTestData: 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();
|
||||||
});
|
});
|
||||||
|
|
||||||
test.afterAll("Delete Test Database", async () => {
|
// Delete database only if not running in CI
|
||||||
await context?.dispose();
|
if (!process.env.CI) {
|
||||||
});
|
test.afterAll("Delete Test Database", async () => {
|
||||||
|
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) => {
|
||||||
|
|||||||
@@ -80,6 +80,69 @@ type createTestSqlContainerConfig = {
|
|||||||
databaseName?: 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({
|
export async function createTestSQLContainer({
|
||||||
includeTestData = false,
|
includeTestData = false,
|
||||||
partitionKey = "/partitionKey",
|
partitionKey = "/partitionKey",
|
||||||
|
|||||||
@@ -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) => {
|
||||||
const timestamp = Number(database.resource._ts) * 1000;
|
await deleteWithRetry(client, database, account.name);
|
||||||
if (timestamp && timestamp < thirtyMinutesAgo) {
|
});
|
||||||
await client.sqlResources.deleteSqlDatabase(resourceGroupName, account.name, database.name);
|
await Promise.all(sqlDatabasesToDelete);
|
||||||
console.log(`DELETED: ${account.name} | ${database.name} | Age: ${friendlyTime(Date.now() - timestamp)}`);
|
}
|
||||||
} else {
|
}
|
||||||
console.log(`SKIPPED: ${account.name} | ${database.name} | Age: ${friendlyTime(Date.now() - timestamp)}`);
|
}
|
||||||
}
|
|
||||||
|
// 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;
|
||||||
|
if (timestamp && timestamp < thirtyMinutesAgo) {
|
||||||
|
await client.sqlResources.deleteSqlDatabase(resourceGroupName, accountName, database.name);
|
||||||
|
console.log(`DELETED: ${accountName} | ${database.name} | Age: ${friendlyTime(Date.now() - timestamp)}`);
|
||||||
|
} else {
|
||||||
|
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