added copyjob pre-requsite screen along with it's validations

This commit is contained in:
Bikram Choudhury
2025-10-21 18:53:02 +05:30
parent a23a7791d4
commit c504d97f7c
21 changed files with 626 additions and 304 deletions

View File

@@ -2,6 +2,7 @@ import React from "react";
import { userContext } from "UserContext";
import { useAADAuth } from "../../../hooks/useAADAuth";
import { useConfig } from "../../../hooks/useConfig";
import { CopyJobMigrationType } from "../Enums";
import { CopyJobContextProviderType, CopyJobContextState, CopyJobFlowType } from "../Types";
export const CopyJobContext = React.createContext<CopyJobContextProviderType>(null);
@@ -20,7 +21,7 @@ interface CopyJobContextProviderProps {
const getInitialCopyJobState = (): CopyJobContextState => {
return {
jobName: "",
migrationType: "offline",
migrationType: CopyJobMigrationType.Offline,
source: {
subscription: null,
account: null,
@@ -33,6 +34,7 @@ const getInitialCopyJobState = (): CopyJobContextState => {
databaseId: "",
containerId: "",
},
sourceReadAccessFromTarget: false
}
}

View File

@@ -1,10 +1,13 @@
import { Link, Stack, Text, Toggle } from "@fluentui/react";
import React, { useMemo } from "react";
import { updateSystemIdentity } from "../../../../../Utils/arm/identityUtils";
import ContainerCopyMessages from "../../../ContainerCopyMessages";
import { useCopyJobContext } from "../../../Context/CopyJobContext";
import { buildResourceLink } from "../../../CopyJobUtils";
import InfoTooltip from "../Components/InfoTooltip";
import PopoverMessage from "../Components/PopoverContainer";
import useManagedIdentity from "./hooks/useManagedIdentity";
import { PermissionSectionConfig } from "./hooks/usePermissionsSection";
import useToggle from "./hooks/useToggle";
const managedIdentityTooltip = ContainerCopyMessages.addManagedIdentity.managedIdentityTooltip;
@@ -12,9 +15,12 @@ const userAssignedTooltip = ContainerCopyMessages.addManagedIdentity.userAssigne
const textStyle = { display: "flex", alignItems: "center" };
const AddManagedIdentity: React.FC = () => {
type AddManagedIdentityProps = Partial<PermissionSectionConfig>;
const AddManagedIdentity: React.FC<AddManagedIdentityProps> = () => {
const { copyJobState } = useCopyJobContext();
const [systemAssigned, onToggle] = useToggle(false);
const { loading, handleAddSystemIdentity } = useManagedIdentity(updateSystemIdentity);
const manageIdentityLink = useMemo(() => {
const { target } = copyJobState;
@@ -44,10 +50,12 @@ const AddManagedIdentity: React.FC = () => {
</Link>
</div>
<PopoverMessage
isLoading={loading}
visible={systemAssigned}
title={ContainerCopyMessages.addManagedIdentity.enablementTitle}
onCancel={() => onToggle(null, false)}
onPrimary={() => console.log('Primary action taken')}
onPrimary={handleAddSystemIdentity}
>
{ContainerCopyMessages.addManagedIdentity.enablementDescription(copyJobState.target?.account?.name)}
</PopoverMessage>

View File

@@ -1,16 +1,45 @@
import { ITooltipHostStyles, Stack, Toggle } from "@fluentui/react";
import React from "react";
import React, { useCallback } from "react";
import { assignRole } from "../../../../../Utils/arm/RbacUtils";
import ContainerCopyMessages from "../../../ContainerCopyMessages";
import { useCopyJobContext } from "../../../Context/CopyJobContext";
import InfoTooltip from "../Components/InfoTooltip";
import PopoverMessage from "../Components/PopoverContainer";
import { PermissionSectionConfig } from "./hooks/usePermissionsSection";
import useToggle from "./hooks/useToggle";
const TooltipContent = ContainerCopyMessages.readPermissionAssigned.tooltip;
const hostStyles: Partial<ITooltipHostStyles> = { root: { display: 'inline-block' } };
type AddManagedIdentityProps = Partial<PermissionSectionConfig>;
const AddReadPermissionToDefaultIdentity: React.FC = () => {
const AddReadPermissionToDefaultIdentity: React.FC<AddManagedIdentityProps> = () => {
const [loading, setLoading] = React.useState(false);
const { copyJobState, setCopyJobState } = useCopyJobContext();
const [readPermissionAssigned, onToggle] = useToggle(false);
const handleAddReadPermission = useCallback(async () => {
const { source, target } = copyJobState;
try {
setLoading(true);
const assignedRole = await assignRole(
source?.subscription?.subscriptionId,
source?.account?.resourceGroup,
source?.account?.name,
target?.account?.identity?.principalId!,
);
if (assignedRole) {
setCopyJobState((prevState) => ({
...prevState,
sourceReadAccessFromTarget: true,
}));
}
} catch (error) {
console.error("Error assigning read permission to default identity:", error);
} finally {
setLoading(false);
}
}, [copyJobState]);
return (
<Stack className="defaultManagedIdentityContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}>
<div className="toggle-label">
@@ -28,10 +57,11 @@ const AddReadPermissionToDefaultIdentity: React.FC = () => {
}}
/>
<PopoverMessage
isLoading={loading}
visible={readPermissionAssigned}
title={ContainerCopyMessages.readPermissionAssigned.popoverTitle}
onCancel={() => onToggle(null, false)}
onPrimary={() => console.log('Primary action taken')}
onPrimary={handleAddReadPermission}
>
{ContainerCopyMessages.readPermissionAssigned.popoverDescription}
</PopoverMessage>

View File

@@ -1,14 +1,19 @@
import { Stack, Toggle } from "@fluentui/react";
import React from "react";
import { updateDefaultIdentity } from "../../../../../Utils/arm/identityUtils";
import ContainerCopyMessages from "../../../ContainerCopyMessages";
import InfoTooltip from "../Components/InfoTooltip";
import PopoverMessage from "../Components/PopoverContainer";
import useManagedIdentity from "./hooks/useManagedIdentity";
import { PermissionSectionConfig } from "./hooks/usePermissionsSection";
import useToggle from "./hooks/useToggle";
const managedIdentityTooltip = ContainerCopyMessages.defaultManagedIdentity.tooltip;
type AddManagedIdentityProps = Partial<PermissionSectionConfig>;
const DefaultManagedIdentity: React.FC = () => {
const DefaultManagedIdentity: React.FC<AddManagedIdentityProps> = () => {
const [defaultSystemAssigned, onToggle] = useToggle(false);
const { loading, handleAddSystemIdentity } = useManagedIdentity(updateDefaultIdentity);
return (
<Stack className="defaultManagedIdentityContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}>
@@ -27,10 +32,11 @@ const DefaultManagedIdentity: React.FC = () => {
}}
/>
<PopoverMessage
isLoading={loading}
visible={defaultSystemAssigned}
title={ContainerCopyMessages.defaultManagedIdentity.popoverTitle}
onCancel={() => onToggle(null, false)}
onPrimary={() => console.log('Primary action taken')}
onPrimary={handleAddSystemIdentity}
>
{ContainerCopyMessages.defaultManagedIdentity.popoverDescription}
</PopoverMessage>

View File

@@ -3,9 +3,11 @@ import React from "react";
import ContainerCopyMessages from "../../../ContainerCopyMessages";
import { useCopyJobContext } from "../../../Context/CopyJobContext";
import { buildResourceLink } from "../../../CopyJobUtils";
import { PermissionSectionConfig } from "./hooks/usePermissionsSection";
import useWindowOpenMonitor from "./hooks/useWindowOpenMonitor";
const OnlineCopyEnabled: React.FC = () => {
type AddManagedIdentityProps = Partial<PermissionSectionConfig>;
const OnlineCopyEnabled: React.FC<AddManagedIdentityProps> = () => {
const { copyJobState: { source } = {} } = useCopyJobContext();
const sourceAccountLink = buildResourceLink(source?.account);
const onlineCopyUrl = `${sourceAccountLink}/Features`;

View File

@@ -1,18 +1,44 @@
import { PrimaryButton, Stack } from "@fluentui/react";
import React from "react";
import React, { useCallback, useState } from "react";
import { fetchDatabaseAccount } from "Utils/arm/databaseAccountUtils";
import ContainerCopyMessages from "../../../ContainerCopyMessages";
import { useCopyJobContext } from "../../../Context/CopyJobContext";
import { buildResourceLink } from "../../../CopyJobUtils";
import { PermissionSectionConfig } from "./hooks/usePermissionsSection";
import useWindowOpenMonitor from "./hooks/useWindowOpenMonitor";
const PointInTimeRestore: React.FC = () => {
const { copyJobState: { source } = {} } = useCopyJobContext();
type AddManagedIdentityProps = Partial<PermissionSectionConfig>;
const PointInTimeRestore: React.FC<AddManagedIdentityProps> = () => {
const [loading, setLoading] = useState<boolean>(false);
const { copyJobState: { source } = {}, setCopyJobState } = useCopyJobContext();
const sourceAccountLink = buildResourceLink(source?.account);
const pitrUrl = `${sourceAccountLink}/backupRestore`;
const onWindowClosed = () => {
console.log('Point-in-time restore window closed');
};
const onWindowClosed = useCallback(async () => {
try {
setLoading(true);
const account = await fetchDatabaseAccount(
source?.subscription?.subscriptionId,
source?.account?.resourceGroup,
source?.account?.name
);
/* account.properties = {
backupPolicy: {
type: "Continuous"
}
} */
if (account) {
setCopyJobState((prevState) => ({
...prevState,
source: { ...prevState.source, account: account }
}));
}
} catch (error) {
console.error("Error fetching database account after PITR window closed:", error);
} finally {
setLoading(false);
}
}, [])
const openWindowAndMonitor = useWindowOpenMonitor(pitrUrl, onWindowClosed);
return (
@@ -21,7 +47,9 @@ const PointInTimeRestore: React.FC = () => {
{ContainerCopyMessages.pointInTimeRestore.description}
</div>
<PrimaryButton
text={ContainerCopyMessages.pointInTimeRestore.buttonText}
text={loading ? "" : ContainerCopyMessages.pointInTimeRestore.buttonText}
{...(loading ? { iconProps: { iconName: "SyncStatusSolid" } } : {})}
disabled={loading}
onClick={openWindowAndMonitor}
/>
</Stack>

View File

@@ -0,0 +1,49 @@
import { DatabaseAccount } from "Contracts/DataModels";
import { useCallback, useState } from "react";
import { useCopyJobContext } from "../../../../Context/CopyJobContext";
interface UseManagedIdentityUpdaterParams {
updateIdentityFn: (
subscriptionId: string,
resourceGroup?: string,
accountName?: string
) => Promise<DatabaseAccount | undefined>;
}
interface UseManagedIdentityUpdaterReturn {
loading: boolean;
handleAddSystemIdentity: () => Promise<void>;
}
const useManagedIdentity = (
updateIdentityFn: UseManagedIdentityUpdaterParams["updateIdentityFn"]
): UseManagedIdentityUpdaterReturn => {
const { copyJobState, setCopyJobState } = useCopyJobContext();
const [loading, setLoading] = useState<boolean>(false);
const handleAddSystemIdentity = useCallback(async (): Promise<void> => {
try {
setLoading(true);
const { target } = copyJobState;
const updatedAccount = await updateIdentityFn(
target.subscriptionId,
target.account?.resourceGroup,
target.account?.name
);
if (updatedAccount) {
setCopyJobState((prevState) => ({
...prevState,
target: { ...prevState.target, account: updatedAccount }
}));
}
} catch (error) {
console.error("Error enabling system-assigned managed identity:", error);
} finally {
setLoading(false);
}
}, [copyJobState, updateIdentityFn, setCopyJobState]);
return { loading, handleAddSystemIdentity };
};
export default useManagedIdentity;

View File

@@ -1,6 +1,9 @@
import { useRoleAssignments } from "hooks/useRoleAssignments";
import { RoleDefinitionType, useRoleDefinitions } from "hooks/useRoleDefinition";
import { useMemo } from "react";
import { useEffect, useMemo, useRef, useState } from "react";
import {
fetchRoleAssignments,
fetchRoleDefinitions,
RoleDefinitionType
} from "../../../../../../Utils/arm/RbacUtils";
import ContainerCopyMessages from "../../../../ContainerCopyMessages";
import {
BackupPolicyType,
@@ -9,6 +12,7 @@ import {
IdentityType
} from "../../../../Enums";
import { CopyJobContextState } from "../../../../Types";
import { useCopyJobPrerequisitesCache } from "../../../Utils/useCopyJobPrerequisitesCache";
import AddManagedIdentity from "../AddManagedIdentity";
import AddReadPermissionToDefaultIdentity from "../AddReadPermissionToDefaultIdentity";
import DefaultManagedIdentity from "../DefaultManagedIdentity";
@@ -18,13 +22,14 @@ import PointInTimeRestore from "../PointInTimeRestore";
export interface PermissionSectionConfig {
id: string;
title: string;
Component: React.FC;
disabled?: boolean;
Component: React.ComponentType
disabled: boolean;
completed?: boolean;
validate?: (state: CopyJobContextState, armToken?: string) => boolean | Promise<boolean>;
}
// Section IDs for maintainability
const SECTION_IDS = {
export const SECTION_IDS = {
addManagedIdentity: "addManagedIdentity",
defaultManagedIdentity: "defaultManagedIdentity",
readPermissionAssigned: "readPermissionAssigned",
@@ -37,16 +42,46 @@ const PERMISSION_SECTIONS_CONFIG: PermissionSectionConfig[] = [
id: SECTION_IDS.addManagedIdentity,
title: ContainerCopyMessages.addManagedIdentity.title,
Component: AddManagedIdentity,
disabled: true,
validate: (state: CopyJobContextState) => {
const targetAccountIdentityType = (state?.target?.account?.identity?.type ?? "").toLowerCase();
return (
targetAccountIdentityType === IdentityType.SystemAssigned ||
targetAccountIdentityType === IdentityType.UserAssigned
);
}
},
{
id: SECTION_IDS.defaultManagedIdentity,
title: ContainerCopyMessages.defaultManagedIdentity.title,
Component: DefaultManagedIdentity,
disabled: true,
validate: (state: CopyJobContextState) => {
const targetAccountDefaultIdentity = (state?.target?.account?.properties?.defaultIdentity ?? "").toLowerCase();
return targetAccountDefaultIdentity === DefaultIdentityType.SystemAssignedIdentity;
}
},
{
id: SECTION_IDS.readPermissionAssigned,
title: ContainerCopyMessages.readPermissionAssigned.title,
Component: AddReadPermissionToDefaultIdentity,
disabled: true,
validate: async (state: CopyJobContextState, armToken?: string) => {
const principalId = state?.target?.account?.identity?.principalId;
const rolesAssigned = await fetchRoleAssignments(
armToken,
state.source?.subscription?.subscriptionId,
state.source?.account?.resourceGroup,
state.source?.account?.name,
principalId
);
const roleDefinitions = await fetchRoleDefinitions(
armToken,
rolesAssigned ?? []
);
return checkTargetHasReaderRoleOnSource(roleDefinitions ?? []);
}
}
];
@@ -55,11 +90,20 @@ const PERMISSION_SECTIONS_FOR_ONLINE_JOBS: PermissionSectionConfig[] = [
id: SECTION_IDS.pointInTimeRestore,
title: ContainerCopyMessages.pointInTimeRestore.title,
Component: PointInTimeRestore,
disabled: true,
validate: (state: CopyJobContextState) => {
const sourceAccountBackupPolicy = state?.source?.account?.properties?.backupPolicy?.type ?? "";
return sourceAccountBackupPolicy === BackupPolicyType.Continuous;
}
},
{
id: SECTION_IDS.onlineCopyEnabled,
title: ContainerCopyMessages.onlineCopyEnabled.title,
Component: OnlineCopyEnabled,
disabled: true,
validate: (_state: CopyJobContextState) => {
return false;
}
}
];
@@ -67,7 +111,7 @@ const PERMISSION_SECTIONS_FOR_ONLINE_JOBS: PermissionSectionConfig[] = [
/**
* Checks if the user has the Reader role based on role definitions.
*/
export function checkUserHasReaderRole(roleDefinitions: RoleDefinitionType[]): boolean {
export function checkTargetHasReaderRoleOnSource(roleDefinitions: RoleDefinitionType[]): boolean {
return roleDefinitions?.some(
role =>
role.name === "00000000-0000-0000-0000-000000000001" ||
@@ -86,105 +130,76 @@ export function checkUserHasReaderRole(roleDefinitions: RoleDefinitionType[]): b
const usePermissionSections = (
state: CopyJobContextState,
armToken: string,
principalId: string
): PermissionSectionConfig[] => {
const { source, target } = state;
const { validationCache, setValidationCache } = useCopyJobPrerequisitesCache();
const [permissionSections, setPermissionSections] = useState<PermissionSectionConfig[] | null>(null);
const isValidatingRef = useRef(false);
// Memoize identity types and backup policy
const targetAccountIdentityType = useMemo(
() => (target?.account?.identity?.type ?? "").toLowerCase(),
[target?.account?.identity?.type]
);
const targetAccountDefaultIdentityType = useMemo(
() => (target?.account?.properties?.defaultIdentity ?? "").toLowerCase(),
[target?.account?.properties?.defaultIdentity]
);
const sourceAccountBackupPolicy = useMemo(
() => source?.account?.properties?.backupPolicy?.type ?? "",
[source?.account?.properties?.backupPolicy?.type]
);
const sectionToValidate = useMemo(() => {
const baseSections = [...PERMISSION_SECTIONS_CONFIG];
if (state.migrationType === CopyJobMigrationType.Online) {
return [...baseSections, ...PERMISSION_SECTIONS_FOR_ONLINE_JOBS];
}
return baseSections;
}, [state.migrationType]);
// Fetch role assignments and definitions
const roleAssigned = useRoleAssignments(
armToken,
source?.subscription?.subscriptionId,
source?.account?.resourceGroup,
source?.account?.name,
principalId
);
const memoizedValidationCache = useMemo(() => {
if (state.migrationType === CopyJobMigrationType.Offline) {
validationCache.delete(SECTION_IDS.pointInTimeRestore);
validationCache.delete(SECTION_IDS.onlineCopyEnabled);
}
return validationCache;
}, [state.migrationType]);
const roleDefinitions = useRoleDefinitions(
armToken,
roleAssigned ?? []
);
useEffect(() => {
const validateSections = async () => {
if (isValidatingRef.current) return;
const hasReaderRole = useMemo(
() => checkUserHasReaderRole(roleDefinitions ?? []),
[roleDefinitions]
);
isValidatingRef.current = true;
const result: PermissionSectionConfig[] = [];
const newValidationCache = new Map(memoizedValidationCache);
// Decouple section state logic for testability
const getBaseSections = useMemo(() => {
return PERMISSION_SECTIONS_CONFIG.map(section => {
if (
section.id === SECTION_IDS.addManagedIdentity &&
(targetAccountIdentityType === IdentityType.SystemAssigned ||
targetAccountIdentityType === IdentityType.UserAssigned)
) {
return {
...section,
disabled: true,
completed: true
};
for (let i = 0; i < sectionToValidate.length; i++) {
const section = sectionToValidate[i];
// Check if this section was already validated and passed
if (newValidationCache.has(section.id) && newValidationCache.get(section.id) === true) {
result.push({ ...section, completed: true });
continue;
}
// We've reached the first non-cached section - validate it
if (section.validate) {
const isValid = await section.validate(state, armToken);
newValidationCache.set(section.id, isValid);
result.push({ ...section, completed: isValid });
// Stop validation if current section failed
if (!isValid) {
for (let j = i + 1; j < sectionToValidate.length; j++) {
result.push({ ...sectionToValidate[j], completed: false });
}
break;
}
}
else {
// Section has no validate method
newValidationCache.set(section.id, false);
result.push({ ...section, completed: false });
}
}
if (
section.id === SECTION_IDS.defaultManagedIdentity &&
targetAccountDefaultIdentityType === DefaultIdentityType.SystemAssignedIdentity
) {
return {
...section,
disabled: true,
completed: true
};
}
if (
section.id === SECTION_IDS.readPermissionAssigned &&
hasReaderRole
) {
return {
...section,
disabled: true,
completed: true
};
}
return section;
});
}, [targetAccountIdentityType, targetAccountDefaultIdentityType, hasReaderRole]);
const getOnlineSections = useMemo(() => {
if (state.migrationType !== CopyJobMigrationType.Online) return [];
return PERMISSION_SECTIONS_FOR_ONLINE_JOBS.map(section => {
if (
section.id === SECTION_IDS.pointInTimeRestore &&
sourceAccountBackupPolicy === BackupPolicyType.Continuous
) {
return {
...section,
disabled: true,
completed: true
};
}
return section;
});
}, [state.migrationType, sourceAccountBackupPolicy]);
setValidationCache(newValidationCache);
setPermissionSections(result);
isValidatingRef.current = false;
}
// Combine and memoize final sections
const permissionSections = useMemo(
() => [...getBaseSections, ...getOnlineSections],
[getBaseSections, getOnlineSections]
);
validateSections();
return permissionSections;
return () => {
isValidatingRef.current = false;
}
}, [state, armToken, sectionToValidate]);
return permissionSections ?? [];
};
export default usePermissionSections;

View File

@@ -1,10 +1,17 @@
import { Image, Stack, Text } from "@fluentui/react";
import { Accordion, AccordionHeader, AccordionItem, AccordionPanel } from "@fluentui/react-components";
import React from "react";
import {
Accordion,
AccordionHeader,
AccordionItem,
AccordionPanel
} from "@fluentui/react-components";
import React, { useEffect } from "react";
import CheckmarkIcon from "../../../../../../images/successfulPopup.svg";
import WarningIcon from "../../../../../../images/warning.svg";
import ShimmerTree, { IndentLevel } from "../../../../../Common/ShimmerTree";
import ContainerCopyMessages from "../../../ContainerCopyMessages";
import { useCopyJobContext } from "../../../Context/CopyJobContext";
import { CopyJobMigrationType } from "../../../Enums";
import usePermissionSections, { PermissionSectionConfig } from "./hooks/usePermissionsSection";
const PermissionSection: React.FC<PermissionSectionConfig> = ({
@@ -32,20 +39,49 @@ const PermissionSection: React.FC<PermissionSectionConfig> = ({
);
const AssignPermissions = () => {
const { armToken, principalId, copyJobState } = useCopyJobContext();
const permissionSections = usePermissionSections(copyJobState, armToken, principalId);
const { armToken, copyJobState } = useCopyJobContext();
const permissionSections = usePermissionSections(copyJobState, armToken);
const [openItems, setOpenItems] = React.useState<string[]>([]);
const indentLevels = React.useMemo<IndentLevel[]>(
() => Array(copyJobState.migrationType === CopyJobMigrationType.Online ? 5 : 3).fill({ level: 0, width: "100%" }),
[]
);
/* const onMoveToNextSection: AccordionToggleEventHandler<string> = useCallback((_event, data) => {
setOpenItems(data.openItems);
}, []); */
useEffect(() => {
const firstIncompleteSection = permissionSections.find(section => !section.completed);
const nextOpenItems = firstIncompleteSection ? [firstIncompleteSection.id] : [];
if (JSON.stringify(openItems) !== JSON.stringify(nextOpenItems)) {
setOpenItems(nextOpenItems);
}
}, [permissionSections]);
return (
<Stack className="assignPermissionsContainer" tokens={{ childrenGap: 15 }}>
<span>
{ContainerCopyMessages.assignPermissions.description}
</span>
<Accordion className="permissionsAccordion" collapsible>
{
permissionSections.map(section => (
<PermissionSection key={section.id} {...section} />
))
}
</Accordion>
{
permissionSections?.length === 0 ? (
<ShimmerTree indentLevels={indentLevels} style={{ width: '100%' }} />
) : (
<Accordion
className="permissionsAccordion"
collapsible
openItems={openItems}
>
{
permissionSections.map(section => (
<PermissionSection key={section.id} {...section} />
))
}
</Accordion>
)
}
</Stack>
);
};

View File

@@ -2,26 +2,33 @@ import { DefaultButton, PrimaryButton, Stack, Text } from "@fluentui/react";
import React from "react";
interface PopoverContainerProps {
isLoading?: boolean;
title?: string;
children?: React.ReactNode;
onPrimary: () => void;
onCancel: () => void;
}
const PopoverContainer: React.FC<PopoverContainerProps> = React.memo(({ title, children, onPrimary, onCancel }) => {
const PopoverContainer: React.FC<PopoverContainerProps> = React.memo(({ isLoading = false, title, children, onPrimary, onCancel }) => {
return (
<Stack className="foreground" tokens={{ childrenGap: 20 }} style={{ maxWidth: 450 }}>
<Stack className={`popover-container foreground ${isLoading ? "loading" : ""}`} tokens={{ childrenGap: 20 }} style={{ maxWidth: 450 }}>
<Text variant="mediumPlus" style={{ fontWeight: 600 }}>{title}</Text>
<Text>{children}</Text>
<Stack horizontal tokens={{ childrenGap: 20 }}>
<PrimaryButton text="Yes" onClick={onPrimary} />
<DefaultButton text="No" onClick={onCancel} />
<PrimaryButton
text={isLoading ? "" : "Yes"}
{...(isLoading ? { iconProps: { iconName: "SyncStatusSolid" } } : {})}
onClick={onPrimary}
disabled={isLoading}
/>
<DefaultButton text="No" onClick={onCancel} disabled={isLoading} />
</Stack>
</Stack>
);
});
interface PopoverMessageProps {
isLoading?: boolean;
visible: boolean;
title: string;
onCancel: () => void;
@@ -29,10 +36,10 @@ interface PopoverMessageProps {
children: React.ReactNode;
}
const PopoverMessage: React.FC<PopoverMessageProps> = ({ visible, title, onCancel, onPrimary, children }) => {
const PopoverMessage: React.FC<PopoverMessageProps> = ({ isLoading = false, visible, title, onCancel, onPrimary, children }) => {
if (!visible) return null;
return (
<PopoverContainer title={title} onCancel={onCancel} onPrimary={onPrimary}>
<PopoverContainer title={title} onCancel={onCancel} onPrimary={onPrimary} isLoading={isLoading}>
{children}
</PopoverContainer>
);

View File

@@ -1,7 +1,8 @@
import { submitCreateCopyJob } from "Explorer/ContainerCopy/Actions/CopyJobActions";
import { useCallback, useMemo, useReducer } from "react";
import { useSidePanel } from "../../../../hooks/useSidePanel";
import { submitCreateCopyJob } from "../../Actions/CopyJobActions";
import { CopyJobContextState } from "../../Types";
import { useCopyJobPrerequisitesCache } from "./useCopyJobPrerequisitesCache";
import { SCREEN_KEYS, useCreateCopyJobScreensList } from "./useCreateCopyJobScreensList";
type NavigationState = {
@@ -34,14 +35,18 @@ function navigationReducer(state: NavigationState, action: Action): NavigationSt
export function useCopyJobNavigation(copyJobState: CopyJobContextState) {
const screens = useCreateCopyJobScreensList();
const { validationCache: cache } = useCopyJobPrerequisitesCache();
const [state, dispatch] = useReducer(navigationReducer, { screenHistory: [SCREEN_KEYS.SelectAccount] });
const currentScreenKey = state.screenHistory[state.screenHistory.length - 1];
const currentScreen = screens.find((screen) => screen.key === currentScreenKey);
const isPrimaryDisabled = useMemo(
() => !currentScreen?.validations.every((v) => v.validate(copyJobState)),
[currentScreen.key, copyJobState]
() => {
const context = currentScreenKey === SCREEN_KEYS.AssignPermissions ? cache : copyJobState;
return !currentScreen?.validations.every((v) => v.validate(context));
},
[currentScreen.key, copyJobState, cache]
);
const primaryBtnText = useMemo(() => {
if (currentScreenKey === SCREEN_KEYS.PreviewCopyJob) {
@@ -53,13 +58,16 @@ export function useCopyJobNavigation(copyJobState: CopyJobContextState) {
const isPreviousDisabled = state.screenHistory.length <= 1;
const handlePrimary = useCallback(() => {
if (currentScreenKey === SCREEN_KEYS.SelectAccount) {
dispatch({ type: "NEXT", nextScreen: SCREEN_KEYS.SelectSourceAndTargetContainers });
}
if (currentScreenKey === SCREEN_KEYS.SelectSourceAndTargetContainers) {
dispatch({ type: "NEXT", nextScreen: SCREEN_KEYS.PreviewCopyJob });
}
if (currentScreenKey === SCREEN_KEYS.PreviewCopyJob) {
const transitions = {
[SCREEN_KEYS.SelectAccount]: SCREEN_KEYS.AssignPermissions,
[SCREEN_KEYS.AssignPermissions]: SCREEN_KEYS.SelectSourceAndTargetContainers,
[SCREEN_KEYS.SelectSourceAndTargetContainers]: SCREEN_KEYS.PreviewCopyJob,
};
const nextScreen = transitions[currentScreenKey];
if (nextScreen) {
dispatch({ type: "NEXT", nextScreen });
} else if (currentScreenKey === SCREEN_KEYS.PreviewCopyJob) {
submitCreateCopyJob(copyJobState);
}
}, [currentScreenKey, copyJobState]);

View File

@@ -0,0 +1,11 @@
import create from "zustand";
interface CopyJobPrerequisitesCacheState {
validationCache: Map<string, boolean>;
setValidationCache: (cache: Map<string, boolean>) => void;
}
export const useCopyJobPrerequisitesCache = create<CopyJobPrerequisitesCacheState>((set) => ({
validationCache: new Map<string, boolean>(),
setValidationCache: (cache) => set({ validationCache: cache })
}));

View File

@@ -13,7 +13,7 @@ const SCREEN_KEYS = {
};
type Validation = {
validate: (state: CopyJobContextState) => boolean;
validate: (state: CopyJobContextState | Map<string, boolean>) => boolean;
message: string;
};
@@ -61,7 +61,18 @@ function useCreateCopyJobScreensList() {
{
key: SCREEN_KEYS.AssignPermissions,
component: <AssignPermissions />,
validations: [],
validations: [
{
validate: (cache: Map<string, boolean>) => {
const cacheValuesIterator = Array.from(cache.values());
if (cacheValuesIterator.length === 0) return false;
const allValid = cacheValuesIterator.every((isValid: boolean) => isValid);
return allValid;
},
message: "Please ensure all previous steps are valid to proceed",
}
],
},
],
[]

View File

@@ -58,6 +58,7 @@ export interface DatabaseContainerSectionProps {
export interface CopyJobContextState {
jobName: string;
migrationType: CopyJobMigrationType;
sourceReadAccessFromTarget: boolean;
// source details
source: {
subscription: Subscription;

View File

@@ -60,13 +60,18 @@
}
}
}
.popover-container {
button[disabled] {
cursor: not-allowed;
opacity: 0.8;
}
}
.foreground {
z-index: 10;
background-color: white;
padding: 20px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
// transform: translate(0%, -5%);
transform: translate(0%, -9%);
position: absolute;
}
}