mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-30 14:22:05 +00:00
added copyjob pre-requsite screen along with it's validations
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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`;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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 })
|
||||
}));
|
||||
@@ -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",
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
[]
|
||||
|
||||
@@ -58,6 +58,7 @@ export interface DatabaseContainerSectionProps {
|
||||
export interface CopyJobContextState {
|
||||
jobName: string;
|
||||
migrationType: CopyJobMigrationType;
|
||||
sourceReadAccessFromTarget: boolean;
|
||||
// source details
|
||||
source: {
|
||||
subscription: Subscription;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user