Merge branch 'master' of https://github.com/Azure/cosmos-explorer into users/aisayas/playwright

This commit is contained in:
Asier Isayas
2025-12-04 11:43:17 -08:00
18 changed files with 243 additions and 83 deletions
+31
View File
@@ -0,0 +1,31 @@
import { Overlay, Spinner, SpinnerSize } from "@fluentui/react";
import React from "react";
interface LoadingOverlayProps {
isLoading: boolean;
label: string;
}
const LoadingOverlay: React.FC<LoadingOverlayProps> = ({ isLoading, label }) => {
if (!isLoading) {
return null;
}
return (
<Overlay
styles={{
root: {
backgroundColor: "rgba(255,255,255,0.9)",
zIndex: 9999,
display: "flex",
alignItems: "center",
justifyContent: "center",
},
}}
>
<Spinner size={SpinnerSize.large} label={label} styles={{ label: { fontWeight: 600 } }} />
</Overlay>
);
};
export default LoadingOverlay;
@@ -55,11 +55,20 @@ export default {
"To copy data from the source to the destination container, ensure that the managed identity of the destination account has read access to the source account by completing the following steps.", "To copy data from the source to the destination container, ensure that the managed identity of the destination account has read access to the source account by completing the following steps.",
intraAccountOnlineDescription: (accountName: string) => intraAccountOnlineDescription: (accountName: string) =>
`Follow the steps below to enable online copy on your "${accountName}" account.`, `Follow the steps below to enable online copy on your "${accountName}" account.`,
commonConfiguration: {
title: "Common configuration",
description: "Basic permissions required for copy operations",
},
onlineConfiguration: {
title: "Online copy configuration",
description: "Additional permissions required for online copy operations",
},
}, },
toggleBtn: { toggleBtn: {
onText: "On", onText: "On",
offText: "Off", offText: "Off",
}, },
popoverOverlaySpinnerLabel: "Please wait while we process your request...",
addManagedIdentity: { addManagedIdentity: {
title: "System-assigned managed identity enabled.", title: "System-assigned managed identity enabled.",
description: description:
+9 -1
View File
@@ -1,5 +1,5 @@
import { DatabaseAccount } from "Contracts/DataModels"; import { DatabaseAccount } from "Contracts/DataModels";
import { CopyJobErrorType, CopyJobType } from "./Types/CopyJobTypes"; import { CopyJobContextState, CopyJobErrorType, CopyJobType } from "./Types/CopyJobTypes";
const azurePortalMpacEndpoint = "https://ms.portal.azure.com/"; const azurePortalMpacEndpoint = "https://ms.portal.azure.com/";
@@ -115,6 +115,14 @@ export function getAccountDetailsFromResourceId(accountId: string | undefined) {
return { subscriptionId, resourceGroup, accountName }; return { subscriptionId, resourceGroup, accountName };
} }
export function getContainerIdentifiers(container: CopyJobContextState["source"] | CopyJobContextState["target"]) {
return {
accountId: container?.account?.id || "",
databaseId: container?.databaseId || "",
containerId: container?.containerId || "",
};
}
export function isIntraAccountCopy(sourceAccountId: string | undefined, targetAccountId: string | undefined): boolean { export function isIntraAccountCopy(sourceAccountId: string | undefined, targetAccountId: string | undefined): boolean {
const sourceAccountDetails = getAccountDetailsFromResourceId(sourceAccountId); const sourceAccountDetails = getAccountDetailsFromResourceId(sourceAccountId);
const targetAccountDetails = getAccountDetailsFromResourceId(targetAccountId); const targetAccountDetails = getAccountDetailsFromResourceId(targetAccountId);
@@ -8,7 +8,8 @@ import ContainerCopyMessages from "../../../ContainerCopyMessages";
import { useCopyJobContext } from "../../../Context/CopyJobContext"; import { useCopyJobContext } from "../../../Context/CopyJobContext";
import { isIntraAccountCopy } from "../../../CopyJobUtils"; import { isIntraAccountCopy } from "../../../CopyJobUtils";
import { CopyJobMigrationType } from "../../../Enums/CopyJobEnums"; import { CopyJobMigrationType } from "../../../Enums/CopyJobEnums";
import usePermissionSections, { PermissionSectionConfig } from "./hooks/usePermissionsSection"; import { useCopyJobPrerequisitesCache } from "../../Utils/useCopyJobPrerequisitesCache";
import usePermissionSections, { PermissionGroupConfig, PermissionSectionConfig } from "./hooks/usePermissionsSection";
const PermissionSection: React.FC<PermissionSectionConfig> = ({ id, title, Component, completed, disabled }) => ( const PermissionSection: React.FC<PermissionSectionConfig> = ({ id, title, Component, completed, disabled }) => (
<AccordionItem key={id} value={id} disabled={disabled}> <AccordionItem key={id} value={id} disabled={disabled}>
@@ -30,43 +31,91 @@ const PermissionSection: React.FC<PermissionSectionConfig> = ({ id, title, Compo
</AccordionItem> </AccordionItem>
); );
const AssignPermissions = () => { const PermissionGroup: React.FC<PermissionGroupConfig> = ({ title, description, sections }) => {
const { copyJobState } = useCopyJobContext();
const permissionSections = usePermissionSections(copyJobState);
const [openItems, setOpenItems] = React.useState<string[]>([]); const [openItems, setOpenItems] = React.useState<string[]>([]);
useEffect(() => {
const firstIncompleteSection = sections.find((section) => !section.completed);
const nextOpenItems = firstIncompleteSection ? [firstIncompleteSection.id] : [];
if (JSON.stringify(openItems) !== JSON.stringify(nextOpenItems)) {
setOpenItems(nextOpenItems);
}
}, [sections]);
return (
<Stack
tokens={{ childrenGap: 15 }}
styles={{
root: {
background: "#fafafa",
border: "1px solid #e1e1e1",
borderRadius: 8,
padding: 16,
boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
},
}}
>
<Stack tokens={{ childrenGap: 5 }}>
<Text variant="medium" style={{ fontWeight: 600 }}>
{title}
</Text>
{description && (
<Text variant="small" styles={{ root: { color: "#605E5C" } }}>
{description}
</Text>
)}
</Stack>
<Accordion className="permissionsAccordion" collapsible openItems={openItems}>
{sections.map((section) => (
<PermissionSection key={section.id} {...section} />
))}
</Accordion>
</Stack>
);
};
const AssignPermissions = () => {
const { setValidationCache } = useCopyJobPrerequisitesCache();
const { copyJobState } = useCopyJobContext();
const permissionGroups = usePermissionSections(copyJobState);
const totalSectionsCount = React.useMemo(
() => permissionGroups.reduce((total, group) => total + group.sections.length, 0),
[permissionGroups],
);
const indentLevels = React.useMemo<IndentLevel[]>( const indentLevels = React.useMemo<IndentLevel[]>(
() => Array(copyJobState.migrationType === CopyJobMigrationType.Online ? 5 : 3).fill({ level: 0, width: "100%" }), () => Array(copyJobState.migrationType === CopyJobMigrationType.Online ? 5 : 3).fill({ level: 0, width: "100%" }),
[], [copyJobState.migrationType],
); );
const isSameAccount = isIntraAccountCopy(copyJobState?.source?.account?.id, copyJobState?.target?.account?.id); const isSameAccount = isIntraAccountCopy(copyJobState?.source?.account?.id, copyJobState?.target?.account?.id);
useEffect(() => { useEffect(() => {
const firstIncompleteSection = permissionSections.find((section) => !section.completed); return () => {
const nextOpenItems = firstIncompleteSection ? [firstIncompleteSection.id] : []; setValidationCache(new Map<string, boolean>());
if (JSON.stringify(openItems) !== JSON.stringify(nextOpenItems)) { };
setOpenItems(nextOpenItems); }, []);
}
}, [permissionSections]);
return ( return (
<Stack className="assignPermissionsContainer" tokens={{ childrenGap: 15 }}> <Stack className="assignPermissionsContainer" tokens={{ childrenGap: 20 }}>
<span> <Text variant="medium">
{isSameAccount && copyJobState.migrationType === CopyJobMigrationType.Online {isSameAccount && copyJobState.migrationType === CopyJobMigrationType.Online
? ContainerCopyMessages.assignPermissions.intraAccountOnlineDescription( ? ContainerCopyMessages.assignPermissions.intraAccountOnlineDescription(
copyJobState?.source?.account?.name || "", copyJobState?.source?.account?.name || "",
) )
: ContainerCopyMessages.assignPermissions.crossAccountDescription} : ContainerCopyMessages.assignPermissions.crossAccountDescription}
</span> </Text>
{permissionSections?.length === 0 ? (
{totalSectionsCount === 0 ? (
<ShimmerTree indentLevels={indentLevels} style={{ width: "100%" }} /> <ShimmerTree indentLevels={indentLevels} style={{ width: "100%" }} />
) : ( ) : (
<Accordion className="permissionsAccordion" collapsible openItems={openItems}> <Stack tokens={{ childrenGap: 25 }}>
{permissionSections.map((section) => ( {permissionGroups.map((group) => (
<PermissionSection key={section.id} {...section} /> <PermissionGroup key={group.id} {...group} />
))} ))}
</Accordion> </Stack>
)} )}
</Stack> </Stack>
); );
@@ -1,8 +1,9 @@
import { Link, PrimaryButton, Stack } from "@fluentui/react"; import { Link, PrimaryButton, Stack } from "@fluentui/react";
import { CapabilityNames } from "Common/Constants";
import { DatabaseAccount } from "Contracts/DataModels"; import { DatabaseAccount } from "Contracts/DataModels";
import React from "react"; import React from "react";
import { fetchDatabaseAccount } from "Utils/arm/databaseAccountUtils"; import { fetchDatabaseAccount } from "Utils/arm/databaseAccountUtils";
import { CapabilityNames } from "../../../../../Common/Constants";
import LoadingOverlay from "../../../../../Common/LoadingOverlay";
import { logError } from "../../../../../Common/Logger"; import { logError } from "../../../../../Common/Logger";
import { update as updateDatabaseAccount } from "../../../../../Utils/arm/generatedClients/cosmos/databaseAccounts"; import { update as updateDatabaseAccount } from "../../../../../Utils/arm/generatedClients/cosmos/databaseAccounts";
import ContainerCopyMessages from "../../../ContainerCopyMessages"; import ContainerCopyMessages from "../../../ContainerCopyMessages";
@@ -119,6 +120,7 @@ const OnlineCopyEnabled: React.FC = () => {
return ( return (
<Stack className="onlineCopyContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}> <Stack className="onlineCopyContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}>
<LoadingOverlay isLoading={loading} label={ContainerCopyMessages.popoverOverlaySpinnerLabel} />
<Stack.Item className="info-message"> <Stack.Item className="info-message">
{ContainerCopyMessages.onlineCopyEnabled.description(source?.account?.name || "")}&ensp; {ContainerCopyMessages.onlineCopyEnabled.description(source?.account?.name || "")}&ensp;
<Link href={ContainerCopyMessages.onlineCopyEnabled.href} target="_blank" rel="noopener noreferrer"> <Link href={ContainerCopyMessages.onlineCopyEnabled.href} target="_blank" rel="noopener noreferrer">
@@ -2,6 +2,7 @@ import { Link, PrimaryButton, Stack, Text } from "@fluentui/react";
import { DatabaseAccount } from "Contracts/DataModels"; import { DatabaseAccount } from "Contracts/DataModels";
import React, { useEffect, useRef, useState } from "react"; import React, { useEffect, useRef, useState } from "react";
import { fetchDatabaseAccount } from "Utils/arm/databaseAccountUtils"; import { fetchDatabaseAccount } from "Utils/arm/databaseAccountUtils";
import LoadingOverlay from "../../../../../Common/LoadingOverlay";
import { logError } from "../../../../../Common/Logger"; import { logError } from "../../../../../Common/Logger";
import ContainerCopyMessages from "../../../ContainerCopyMessages"; import ContainerCopyMessages from "../../../ContainerCopyMessages";
import { useCopyJobContext } from "../../../Context/CopyJobContext"; import { useCopyJobContext } from "../../../Context/CopyJobContext";
@@ -109,6 +110,7 @@ const PointInTimeRestore: React.FC = () => {
return ( return (
<Stack className="pointInTimeRestoreContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}> <Stack className="pointInTimeRestoreContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}>
<LoadingOverlay isLoading={loading} label={ContainerCopyMessages.popoverOverlaySpinnerLabel} />
<Stack.Item className="toggle-label"> <Stack.Item className="toggle-label">
{ContainerCopyMessages.pointInTimeRestore.description(source.account?.name ?? "")} {ContainerCopyMessages.pointInTimeRestore.description(source.account?.name ?? "")}
{tooltipContent && ( {tooltipContent && (
@@ -2,7 +2,7 @@ import { useEffect, useMemo, useRef, useState } from "react";
import { CapabilityNames } from "../../../../../../Common/Constants"; import { CapabilityNames } from "../../../../../../Common/Constants";
import { fetchRoleAssignments, fetchRoleDefinitions, RoleDefinitionType } from "../../../../../../Utils/arm/RbacUtils"; import { fetchRoleAssignments, fetchRoleDefinitions, RoleDefinitionType } from "../../../../../../Utils/arm/RbacUtils";
import ContainerCopyMessages from "../../../../ContainerCopyMessages"; import ContainerCopyMessages from "../../../../ContainerCopyMessages";
import { getAccountDetailsFromResourceId, isIntraAccountCopy } from "../../../../CopyJobUtils"; import { getAccountDetailsFromResourceId, getContainerIdentifiers, isIntraAccountCopy } from "../../../../CopyJobUtils";
import { import {
BackupPolicyType, BackupPolicyType,
CopyJobMigrationType, CopyJobMigrationType,
@@ -26,6 +26,13 @@ export interface PermissionSectionConfig {
validate?: (state: CopyJobContextState) => boolean | Promise<boolean>; validate?: (state: CopyJobContextState) => boolean | Promise<boolean>;
} }
export interface PermissionGroupConfig {
id: string;
title: string;
description: string;
sections: PermissionSectionConfig[];
}
export const SECTION_IDS = { export const SECTION_IDS = {
addManagedIdentity: "addManagedIdentity", addManagedIdentity: "addManagedIdentity",
defaultManagedIdentity: "defaultManagedIdentity", defaultManagedIdentity: "defaultManagedIdentity",
@@ -127,26 +134,81 @@ export function checkTargetHasReaderRoleOnSource(roleDefinitions: RoleDefinition
} }
/** /**
* Returns the permission sections configuration for the Assign Permissions screen. * Validates sections within a group sequentially.
* Memoizes derived values for performance and decouples logic for testability.
*/ */
const usePermissionSections = (state: CopyJobContextState): PermissionSectionConfig[] => { const validateSectionsInGroup = async (
const sourceAccountId = state?.source?.account?.id || ""; sections: PermissionSectionConfig[],
const targetAccountId = state?.target?.account?.id || ""; state: CopyJobContextState,
validationCache: Map<string, boolean>,
): Promise<PermissionSectionConfig[]> => {
const result: PermissionSectionConfig[] = [];
for (let i = 0; i < sections.length; i++) {
const section = sections[i];
if (validationCache.has(section.id) && validationCache.get(section.id) === true) {
result.push({ ...section, completed: true });
continue;
}
if (section.validate) {
const isValid = await section.validate(state);
validationCache.set(section.id, isValid);
result.push({ ...section, completed: isValid });
if (!isValid) {
// Mark remaining sections in this group as incomplete
for (let j = i + 1; j < sections.length; j++) {
result.push({ ...sections[j], completed: false });
}
break;
}
} else {
validationCache.set(section.id, false);
result.push({ ...section, completed: false });
}
}
return result;
};
/**
* Returns the permission groups configuration for the Assign Permissions screen.
* Groups validate independently but sections within each group validate sequentially.
*/
const usePermissionSections = (state: CopyJobContextState): PermissionGroupConfig[] => {
const sourceAccount = getContainerIdentifiers(state.source);
const targetAccount = getContainerIdentifiers(state.target);
const { validationCache, setValidationCache } = useCopyJobPrerequisitesCache(); const { validationCache, setValidationCache } = useCopyJobPrerequisitesCache();
const [permissionSections, setPermissionSections] = useState<PermissionSectionConfig[] | null>(null); const [permissionGroups, setPermissionGroups] = useState<PermissionGroupConfig[] | null>(null);
const isValidatingRef = useRef(false); const isValidatingRef = useRef(false);
const sectionToValidate = useMemo(() => { const groupsToValidate = useMemo(() => {
const isSameAccount = isIntraAccountCopy(sourceAccountId, targetAccountId); const isSameAccount = isIntraAccountCopy(sourceAccount.accountId, targetAccount.accountId);
const commonSections = isSameAccount ? [] : [...PERMISSION_SECTIONS_CONFIG];
const groups: PermissionGroupConfig[] = [];
const baseSections = isSameAccount ? [] : [...PERMISSION_SECTIONS_CONFIG]; if (commonSections.length > 0) {
if (state.migrationType === CopyJobMigrationType.Online) { groups.push({
return [...baseSections, ...PERMISSION_SECTIONS_FOR_ONLINE_JOBS]; id: "commonConfigs",
title: ContainerCopyMessages.assignPermissions.commonConfiguration.title,
description: ContainerCopyMessages.assignPermissions.commonConfiguration.description,
sections: commonSections,
});
} }
return baseSections;
}, [sourceAccountId, targetAccountId, state.migrationType]); if (state.migrationType === CopyJobMigrationType.Online) {
groups.push({
id: "onlineConfigs",
title: ContainerCopyMessages.assignPermissions.onlineConfiguration.title,
description: ContainerCopyMessages.assignPermissions.onlineConfiguration.description,
sections: [...PERMISSION_SECTIONS_FOR_ONLINE_JOBS],
});
}
return groups;
}, [sourceAccount.accountId, targetAccount.accountId, state.migrationType]);
const memoizedValidationCache = useMemo(() => { const memoizedValidationCache = useMemo(() => {
if (state.migrationType === CopyJobMigrationType.Offline) { if (state.migrationType === CopyJobMigrationType.Offline) {
@@ -157,52 +219,39 @@ const usePermissionSections = (state: CopyJobContextState): PermissionSectionCon
}, [state.migrationType]); }, [state.migrationType]);
useEffect(() => { useEffect(() => {
const validateSections = async () => { const validateGroups = async () => {
if (isValidatingRef.current) { if (isValidatingRef.current) {
return; return;
} }
isValidatingRef.current = true; isValidatingRef.current = true;
const result: PermissionSectionConfig[] = [];
const newValidationCache = new Map(memoizedValidationCache); const newValidationCache = new Map(memoizedValidationCache);
for (let i = 0; i < sectionToValidate.length; i++) { // Validate all groups independently (in parallel)
const section = sectionToValidate[i]; const validatedGroups = await Promise.all(
groupsToValidate.map(async (group) => {
const validatedSections = await validateSectionsInGroup(group.sections, state, newValidationCache);
if (newValidationCache.has(section.id) && newValidationCache.get(section.id) === true) { return {
result.push({ ...section, completed: true }); ...group,
continue; sections: validatedSections,
} };
if (section.validate) { }),
const isValid = await section.validate(state); );
newValidationCache.set(section.id, isValid);
result.push({ ...section, completed: isValid });
if (!isValid) {
for (let j = i + 1; j < sectionToValidate.length; j++) {
result.push({ ...sectionToValidate[j], completed: false });
}
break;
}
} else {
newValidationCache.set(section.id, false);
result.push({ ...section, completed: false });
}
}
setValidationCache(newValidationCache); setValidationCache(newValidationCache);
setPermissionSections(result); setPermissionGroups(validatedGroups);
isValidatingRef.current = false; isValidatingRef.current = false;
}; };
validateSections(); validateGroups();
return () => { return () => {
isValidatingRef.current = false; isValidatingRef.current = false;
}; };
}, [state, sectionToValidate]); }, [state, groupsToValidate]);
return permissionSections ?? []; return permissionGroups ?? [];
}; };
export default usePermissionSections; export default usePermissionSections;
@@ -2,6 +2,8 @@
/* eslint-disable react/display-name */ /* eslint-disable react/display-name */
import { DefaultButton, PrimaryButton, Stack, Text } from "@fluentui/react"; import { DefaultButton, PrimaryButton, Stack, Text } from "@fluentui/react";
import React from "react"; import React from "react";
import LoadingOverlay from "../../../../../Common/LoadingOverlay";
import ContainerCopyMessages from "../../../ContainerCopyMessages";
interface PopoverContainerProps { interface PopoverContainerProps {
isLoading?: boolean; isLoading?: boolean;
@@ -19,17 +21,13 @@ const PopoverContainer: React.FC<PopoverContainerProps> = React.memo(
tokens={{ childrenGap: 20 }} tokens={{ childrenGap: 20 }}
style={{ maxWidth: 450 }} style={{ maxWidth: 450 }}
> >
<LoadingOverlay isLoading={isLoading} label={ContainerCopyMessages.popoverOverlaySpinnerLabel} />
<Text variant="mediumPlus" style={{ fontWeight: 600 }}> <Text variant="mediumPlus" style={{ fontWeight: 600 }}>
{title} {title}
</Text> </Text>
<Text>{children}</Text> <Text>{children}</Text>
<Stack horizontal tokens={{ childrenGap: 20 }}> <Stack horizontal tokens={{ childrenGap: 20 }}>
<PrimaryButton <PrimaryButton text={"Yes"} onClick={onPrimary} disabled={isLoading} />
text={isLoading ? "" : "Yes"}
{...(isLoading ? { iconProps: { iconName: "SyncStatusSolid" } } : {})}
onClick={onPrimary}
disabled={isLoading}
/>
<DefaultButton text="No" onClick={onCancel} disabled={isLoading} /> <DefaultButton text="No" onClick={onCancel} disabled={isLoading} />
</Stack> </Stack>
</Stack> </Stack>
@@ -2,7 +2,7 @@ import { useCallback, useMemo, useReducer, useState } from "react";
import { useSidePanel } from "../../../../hooks/useSidePanel"; import { useSidePanel } from "../../../../hooks/useSidePanel";
import { submitCreateCopyJob } from "../../Actions/CopyJobActions"; import { submitCreateCopyJob } from "../../Actions/CopyJobActions";
import { useCopyJobContext } from "../../Context/CopyJobContext"; import { useCopyJobContext } from "../../Context/CopyJobContext";
import { isIntraAccountCopy } from "../../CopyJobUtils"; import { getContainerIdentifiers, isIntraAccountCopy } from "../../CopyJobUtils";
import { CopyJobMigrationType } from "../../Enums/CopyJobEnums"; import { CopyJobMigrationType } from "../../Enums/CopyJobEnums";
import { useCopyJobPrerequisitesCache } from "./useCopyJobPrerequisitesCache"; import { useCopyJobPrerequisitesCache } from "./useCopyJobPrerequisitesCache";
import { SCREEN_KEYS, useCreateCopyJobScreensList } from "./useCreateCopyJobScreensList"; import { SCREEN_KEYS, useCreateCopyJobScreensList } from "./useCreateCopyJobScreensList";
@@ -71,12 +71,6 @@ export function useCopyJobNavigation() {
useSidePanel.getState().closeSidePanel(); useSidePanel.getState().closeSidePanel();
}, []); }, []);
const getContainerIdentifiers = (container: typeof copyJobState.source | typeof copyJobState.target) => ({
accountId: container?.account?.id || "",
databaseId: container?.databaseId || "",
containerId: container?.containerId || "",
});
const areContainersIdentical = () => { const areContainersIdentical = () => {
const { source, target } = copyJobState; const { source, target } = copyJobState;
const sourceIds = getContainerIdentifiers(source); const sourceIds = getContainerIdentifiers(source);
@@ -20,6 +20,10 @@
height: 100%; height: 100%;
padding: 1em 1.5em; padding: 1em 1.5em;
.pointInTimeRestoreContainer, .onlineCopyContainer {
position: relative;
}
label { label {
padding: 0; padding: 0;
} }
@@ -59,6 +63,7 @@
} }
} }
.popover-container { .popover-container {
border-radius: 6px;
button[disabled] { button[disabled] {
cursor: not-allowed; cursor: not-allowed;
opacity: 0.8; opacity: 0.8;
@@ -66,7 +71,7 @@
} }
.foreground { .foreground {
z-index: 10; z-index: 10;
background-color: white; background-color: #f9f9f9;
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%);
@@ -89,6 +94,10 @@
.panelFormWrapper .panelMainContent { .panelFormWrapper .panelMainContent {
padding: 0; padding: 0;
} }
.createCopyJobScreensFooter {
margin-top: 50px;
}
} }
.monitorCopyJobs { .monitorCopyJobs {
@@ -286,6 +286,7 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
</Stack> </Stack>
<TextField <TextField
id="autoscaleRUValueField" id="autoscaleRUValueField"
data-test="autoscaleRUInput"
type="number" type="number"
styles={{ styles={{
fieldGroup: { width: 100, height: 27, flexShrink: 0 }, fieldGroup: { width: 100, height: 27, flexShrink: 0 },
@@ -2144,6 +2144,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
</Stack> </Stack>
<StyledTextFieldBase <StyledTextFieldBase
ariaLabel="Container max RU/s" ariaLabel="Container max RU/s"
data-test="autoscaleRUInput"
errorMessage="" errorMessage=""
id="autoscaleRUValueField" id="autoscaleRUValueField"
key=".0:$.$.1" key=".0:$.$.1"
@@ -2170,6 +2171,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
> >
<TextFieldBase <TextFieldBase
ariaLabel="Container max RU/s" ariaLabel="Container max RU/s"
data-test="autoscaleRUInput"
deferredValidationTime={200} deferredValidationTime={200}
errorMessage="" errorMessage=""
id="autoscaleRUValueField" id="autoscaleRUValueField"
@@ -2470,6 +2472,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
aria-invalid={false} aria-invalid={false}
aria-label="Container max RU/s" aria-label="Container max RU/s"
className="ms-TextField-field field-124" className="ms-TextField-field field-124"
data-test="autoscaleRUInput"
id="autoscaleRUValueField" id="autoscaleRUValueField"
max="9007199254740991" max="9007199254740991"
min={1000} min={1000}
+2 -1
View File
@@ -1,6 +1,6 @@
import { expect, test } from "@playwright/test"; import { expect, test } from "@playwright/test";
import { DataExplorer, TestAccount, generateUniqueName } from "../fx"; import { DataExplorer, TEST_AUTOSCALE_THROUGHPUT_RU, TestAccount, generateUniqueName } from "../fx";
test("Cassandra keyspace and table CRUD", async ({ page }) => { test("Cassandra keyspace and table CRUD", async ({ page }) => {
const keyspaceId = generateUniqueName("db"); const keyspaceId = generateUniqueName("db");
@@ -14,6 +14,7 @@ test("Cassandra keyspace and table CRUD", async ({ page }) => {
async (panel, okButton) => { async (panel, okButton) => {
await panel.getByPlaceholder("Type a new keyspace id").fill(keyspaceId); await panel.getByPlaceholder("Type a new keyspace id").fill(keyspaceId);
await panel.getByPlaceholder("Enter table Id").fill(tableId); await panel.getByPlaceholder("Enter table Id").fill(tableId);
await panel.getByTestId("autoscaleRUInput").fill(TEST_AUTOSCALE_THROUGHPUT_RU.toString());
await okButton.click(); await okButton.click();
}, },
{ closeTimeout: 5 * 60 * 1000 }, { closeTimeout: 5 * 60 * 1000 },
+1
View File
@@ -55,6 +55,7 @@ export const defaultAccounts: Record<TestAccount, string> = {
export const resourceGroupName = process.env.DE_TEST_RESOURCE_GROUP ?? "de-e2e-tests"; export const resourceGroupName = process.env.DE_TEST_RESOURCE_GROUP ?? "de-e2e-tests";
export const subscriptionId = process.env.DE_TEST_SUBSCRIPTION_ID ?? "69e02f2d-f059-4409-9eac-97e8a276ae2c"; export const subscriptionId = process.env.DE_TEST_SUBSCRIPTION_ID ?? "69e02f2d-f059-4409-9eac-97e8a276ae2c";
export const TEST_AUTOSCALE_THROUGHPUT_RU = 1000;
export const TEST_AUTOSCALE_MAX_THROUGHPUT_RU_2K = 2000; export const TEST_AUTOSCALE_MAX_THROUGHPUT_RU_2K = 2000;
export const TEST_MANUAL_THROUGHPUT_RU_2K = 2000; export const TEST_MANUAL_THROUGHPUT_RU_2K = 2000;
export const ONE_MINUTE_MS: number = 60 * 1000; export const ONE_MINUTE_MS: number = 60 * 1000;
+2 -1
View File
@@ -1,6 +1,6 @@
import { expect, test } from "@playwright/test"; import { expect, test } from "@playwright/test";
import { DataExplorer, TestAccount, generateUniqueName } from "../fx"; import { DataExplorer, TEST_AUTOSCALE_THROUGHPUT_RU, TestAccount, generateUniqueName } from "../fx";
test("Gremlin graph CRUD", async ({ page }) => { test("Gremlin graph CRUD", async ({ page }) => {
const databaseId = generateUniqueName("db"); const databaseId = generateUniqueName("db");
@@ -16,6 +16,7 @@ test("Gremlin graph CRUD", async ({ page }) => {
await panel.getByPlaceholder("Type a new database id").fill(databaseId); await panel.getByPlaceholder("Type a new database id").fill(databaseId);
await panel.getByRole("textbox", { name: "Graph id, Example Graph1" }).fill(graphId); await panel.getByRole("textbox", { name: "Graph id, Example Graph1" }).fill(graphId);
await panel.getByRole("textbox", { name: "Partition key" }).fill("/pk"); await panel.getByRole("textbox", { name: "Partition key" }).fill("/pk");
await panel.getByTestId("autoscaleRUInput").fill(TEST_AUTOSCALE_THROUGHPUT_RU.toString());
await okButton.click(); await okButton.click();
}, },
{ closeTimeout: 5 * 60 * 1000 }, { closeTimeout: 5 * 60 * 1000 },
+2 -1
View File
@@ -1,6 +1,6 @@
import { expect, test } from "@playwright/test"; import { expect, test } from "@playwright/test";
import { DataExplorer, TestAccount, generateUniqueName } from "../fx"; import { DataExplorer, TEST_AUTOSCALE_THROUGHPUT_RU, TestAccount, generateUniqueName } from "../fx";
( (
[ [
@@ -21,6 +21,7 @@ import { DataExplorer, TestAccount, generateUniqueName } from "../fx";
await panel.getByPlaceholder("Type a new database id").fill(databaseId); await panel.getByPlaceholder("Type a new database id").fill(databaseId);
await panel.getByRole("textbox", { name: "Collection id, Example Collection1" }).fill(collectionId); await panel.getByRole("textbox", { name: "Collection id, Example Collection1" }).fill(collectionId);
await panel.getByRole("textbox", { name: "Shard key" }).fill("pk"); await panel.getByRole("textbox", { name: "Shard key" }).fill("pk");
await panel.getByTestId("autoscaleRUInput").fill(TEST_AUTOSCALE_THROUGHPUT_RU.toString());
await okButton.click(); await okButton.click();
}, },
{ closeTimeout: 5 * 60 * 1000 }, { closeTimeout: 5 * 60 * 1000 },
+2 -1
View File
@@ -1,6 +1,6 @@
import { expect, test } from "@playwright/test"; import { expect, test } from "@playwright/test";
import { DataExplorer, TestAccount, generateUniqueName } from "../fx"; import { DataExplorer, TEST_AUTOSCALE_THROUGHPUT_RU, TestAccount, generateUniqueName } from "../fx";
test("SQL database and container CRUD", async ({ page }) => { test("SQL database and container CRUD", async ({ page }) => {
const databaseId = generateUniqueName("db"); const databaseId = generateUniqueName("db");
@@ -15,6 +15,7 @@ test("SQL database and container CRUD", async ({ page }) => {
await panel.getByPlaceholder("Type a new database id").fill(databaseId); await panel.getByPlaceholder("Type a new database id").fill(databaseId);
await panel.getByRole("textbox", { name: "Container id, Example Container1" }).fill(containerId); await panel.getByRole("textbox", { name: "Container id, Example Container1" }).fill(containerId);
await panel.getByRole("textbox", { name: "Partition key" }).fill("/pk"); await panel.getByRole("textbox", { name: "Partition key" }).fill("/pk");
await panel.getByTestId("autoscaleRUInput").fill(TEST_AUTOSCALE_THROUGHPUT_RU.toString());
await okButton.click(); await okButton.click();
}, },
{ closeTimeout: 5 * 60 * 1000 }, { closeTimeout: 5 * 60 * 1000 },
+2 -2
View File
@@ -1,6 +1,6 @@
import { expect, test } from "@playwright/test"; import { expect, test } from "@playwright/test";
import { DataExplorer, TestAccount, generateUniqueName } from "../fx"; import { DataExplorer, TEST_AUTOSCALE_THROUGHPUT_RU, TestAccount, generateUniqueName } from "../fx";
test("Tables CRUD", async ({ page }) => { test("Tables CRUD", async ({ page }) => {
const tableId = generateUniqueName("table"); // A unique table name IS needed because the database is shared when using Table Storage. const tableId = generateUniqueName("table"); // A unique table name IS needed because the database is shared when using Table Storage.
@@ -12,7 +12,7 @@ test("Tables CRUD", async ({ page }) => {
"New Table", "New Table",
async (panel, okButton) => { async (panel, okButton) => {
await panel.getByRole("textbox", { name: "Table id, Example Table1" }).fill(tableId); await panel.getByRole("textbox", { name: "Table id, Example Table1" }).fill(tableId);
await panel.getByLabel("Table Max RU/s").fill("1000"); await panel.getByTestId("autoscaleRUInput").fill(TEST_AUTOSCALE_THROUGHPUT_RU.toString());
await okButton.click(); await okButton.click();
}, },
{ closeTimeout: 5 * 60 * 1000 }, { closeTimeout: 5 * 60 * 1000 },