mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-01-08 03:57:31 +00:00
added copyjob pre-requsite screen along with it's validations
This commit is contained in:
42
src/Common/ShimmerTree/index.tsx
Normal file
42
src/Common/ShimmerTree/index.tsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { Shimmer, ShimmerElementType, Stack } from "@fluentui/react";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
export interface IndentLevel {
|
||||||
|
level: number,
|
||||||
|
width?: string
|
||||||
|
}
|
||||||
|
interface ShimmerTreeProps {
|
||||||
|
indentLevels: IndentLevel[];
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ShimmerTree = ({ indentLevels, style = {} }: ShimmerTreeProps) => {
|
||||||
|
/**
|
||||||
|
* indentLevels - Array of indent levels for shimmer tree
|
||||||
|
* 0 - Root
|
||||||
|
* 1 - Level 1
|
||||||
|
* 2 - Level 2
|
||||||
|
* 3 - Level 3
|
||||||
|
* n - Level n
|
||||||
|
* */
|
||||||
|
const renderShimmers = (indent: IndentLevel) => (
|
||||||
|
<Shimmer
|
||||||
|
key={Math.random()}
|
||||||
|
shimmerElements={[
|
||||||
|
{ type: ShimmerElementType.gap, width: `${indent.level * 20}px` }, // Indent for hierarchy
|
||||||
|
{ type: ShimmerElementType.line, height: 16, width: indent.width || "100%" },
|
||||||
|
]}
|
||||||
|
style={{ marginBottom: 8 }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack tokens={{ childrenGap: 8 }} style={{ width: "50%", ...style }} data-testid="shimmer-stack">
|
||||||
|
{
|
||||||
|
indentLevels.map((indentLevel: IndentLevel) => renderShimmers(indentLevel))
|
||||||
|
}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ShimmerTree;
|
||||||
@@ -2,6 +2,7 @@ import React from "react";
|
|||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
import { useAADAuth } from "../../../hooks/useAADAuth";
|
import { useAADAuth } from "../../../hooks/useAADAuth";
|
||||||
import { useConfig } from "../../../hooks/useConfig";
|
import { useConfig } from "../../../hooks/useConfig";
|
||||||
|
import { CopyJobMigrationType } from "../Enums";
|
||||||
import { CopyJobContextProviderType, CopyJobContextState, CopyJobFlowType } from "../Types";
|
import { CopyJobContextProviderType, CopyJobContextState, CopyJobFlowType } from "../Types";
|
||||||
|
|
||||||
export const CopyJobContext = React.createContext<CopyJobContextProviderType>(null);
|
export const CopyJobContext = React.createContext<CopyJobContextProviderType>(null);
|
||||||
@@ -20,7 +21,7 @@ interface CopyJobContextProviderProps {
|
|||||||
const getInitialCopyJobState = (): CopyJobContextState => {
|
const getInitialCopyJobState = (): CopyJobContextState => {
|
||||||
return {
|
return {
|
||||||
jobName: "",
|
jobName: "",
|
||||||
migrationType: "offline",
|
migrationType: CopyJobMigrationType.Offline,
|
||||||
source: {
|
source: {
|
||||||
subscription: null,
|
subscription: null,
|
||||||
account: null,
|
account: null,
|
||||||
@@ -33,6 +34,7 @@ const getInitialCopyJobState = (): CopyJobContextState => {
|
|||||||
databaseId: "",
|
databaseId: "",
|
||||||
containerId: "",
|
containerId: "",
|
||||||
},
|
},
|
||||||
|
sourceReadAccessFromTarget: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import { Link, Stack, Text, Toggle } from "@fluentui/react";
|
import { Link, Stack, Text, Toggle } from "@fluentui/react";
|
||||||
import React, { useMemo } from "react";
|
import React, { useMemo } from "react";
|
||||||
|
import { updateSystemIdentity } from "../../../../../Utils/arm/identityUtils";
|
||||||
import ContainerCopyMessages from "../../../ContainerCopyMessages";
|
import ContainerCopyMessages from "../../../ContainerCopyMessages";
|
||||||
import { useCopyJobContext } from "../../../Context/CopyJobContext";
|
import { useCopyJobContext } from "../../../Context/CopyJobContext";
|
||||||
import { buildResourceLink } from "../../../CopyJobUtils";
|
import { buildResourceLink } from "../../../CopyJobUtils";
|
||||||
import InfoTooltip from "../Components/InfoTooltip";
|
import InfoTooltip from "../Components/InfoTooltip";
|
||||||
import PopoverMessage from "../Components/PopoverContainer";
|
import PopoverMessage from "../Components/PopoverContainer";
|
||||||
|
import useManagedIdentity from "./hooks/useManagedIdentity";
|
||||||
|
import { PermissionSectionConfig } from "./hooks/usePermissionsSection";
|
||||||
import useToggle from "./hooks/useToggle";
|
import useToggle from "./hooks/useToggle";
|
||||||
|
|
||||||
const managedIdentityTooltip = ContainerCopyMessages.addManagedIdentity.managedIdentityTooltip;
|
const managedIdentityTooltip = ContainerCopyMessages.addManagedIdentity.managedIdentityTooltip;
|
||||||
@@ -12,9 +15,12 @@ const userAssignedTooltip = ContainerCopyMessages.addManagedIdentity.userAssigne
|
|||||||
|
|
||||||
const textStyle = { display: "flex", alignItems: "center" };
|
const textStyle = { display: "flex", alignItems: "center" };
|
||||||
|
|
||||||
const AddManagedIdentity: React.FC = () => {
|
type AddManagedIdentityProps = Partial<PermissionSectionConfig>;
|
||||||
|
|
||||||
|
const AddManagedIdentity: React.FC<AddManagedIdentityProps> = () => {
|
||||||
const { copyJobState } = useCopyJobContext();
|
const { copyJobState } = useCopyJobContext();
|
||||||
const [systemAssigned, onToggle] = useToggle(false);
|
const [systemAssigned, onToggle] = useToggle(false);
|
||||||
|
const { loading, handleAddSystemIdentity } = useManagedIdentity(updateSystemIdentity);
|
||||||
|
|
||||||
const manageIdentityLink = useMemo(() => {
|
const manageIdentityLink = useMemo(() => {
|
||||||
const { target } = copyJobState;
|
const { target } = copyJobState;
|
||||||
@@ -44,10 +50,12 @@ const AddManagedIdentity: React.FC = () => {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<PopoverMessage
|
<PopoverMessage
|
||||||
|
isLoading={loading}
|
||||||
visible={systemAssigned}
|
visible={systemAssigned}
|
||||||
title={ContainerCopyMessages.addManagedIdentity.enablementTitle}
|
title={ContainerCopyMessages.addManagedIdentity.enablementTitle}
|
||||||
onCancel={() => onToggle(null, false)}
|
onCancel={() => onToggle(null, false)}
|
||||||
onPrimary={() => console.log('Primary action taken')}
|
onPrimary={handleAddSystemIdentity}
|
||||||
|
|
||||||
>
|
>
|
||||||
{ContainerCopyMessages.addManagedIdentity.enablementDescription(copyJobState.target?.account?.name)}
|
{ContainerCopyMessages.addManagedIdentity.enablementDescription(copyJobState.target?.account?.name)}
|
||||||
</PopoverMessage>
|
</PopoverMessage>
|
||||||
|
|||||||
@@ -1,16 +1,45 @@
|
|||||||
import { ITooltipHostStyles, Stack, Toggle } from "@fluentui/react";
|
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 ContainerCopyMessages from "../../../ContainerCopyMessages";
|
||||||
|
import { useCopyJobContext } from "../../../Context/CopyJobContext";
|
||||||
import InfoTooltip from "../Components/InfoTooltip";
|
import InfoTooltip from "../Components/InfoTooltip";
|
||||||
import PopoverMessage from "../Components/PopoverContainer";
|
import PopoverMessage from "../Components/PopoverContainer";
|
||||||
|
import { PermissionSectionConfig } from "./hooks/usePermissionsSection";
|
||||||
import useToggle from "./hooks/useToggle";
|
import useToggle from "./hooks/useToggle";
|
||||||
|
|
||||||
const TooltipContent = ContainerCopyMessages.readPermissionAssigned.tooltip;
|
const TooltipContent = ContainerCopyMessages.readPermissionAssigned.tooltip;
|
||||||
const hostStyles: Partial<ITooltipHostStyles> = { root: { display: 'inline-block' } };
|
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 [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 (
|
return (
|
||||||
<Stack className="defaultManagedIdentityContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}>
|
<Stack className="defaultManagedIdentityContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}>
|
||||||
<div className="toggle-label">
|
<div className="toggle-label">
|
||||||
@@ -28,10 +57,11 @@ const AddReadPermissionToDefaultIdentity: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<PopoverMessage
|
<PopoverMessage
|
||||||
|
isLoading={loading}
|
||||||
visible={readPermissionAssigned}
|
visible={readPermissionAssigned}
|
||||||
title={ContainerCopyMessages.readPermissionAssigned.popoverTitle}
|
title={ContainerCopyMessages.readPermissionAssigned.popoverTitle}
|
||||||
onCancel={() => onToggle(null, false)}
|
onCancel={() => onToggle(null, false)}
|
||||||
onPrimary={() => console.log('Primary action taken')}
|
onPrimary={handleAddReadPermission}
|
||||||
>
|
>
|
||||||
{ContainerCopyMessages.readPermissionAssigned.popoverDescription}
|
{ContainerCopyMessages.readPermissionAssigned.popoverDescription}
|
||||||
</PopoverMessage>
|
</PopoverMessage>
|
||||||
|
|||||||
@@ -1,14 +1,19 @@
|
|||||||
import { Stack, Toggle } from "@fluentui/react";
|
import { Stack, Toggle } from "@fluentui/react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { updateDefaultIdentity } from "../../../../../Utils/arm/identityUtils";
|
||||||
import ContainerCopyMessages from "../../../ContainerCopyMessages";
|
import ContainerCopyMessages from "../../../ContainerCopyMessages";
|
||||||
import InfoTooltip from "../Components/InfoTooltip";
|
import InfoTooltip from "../Components/InfoTooltip";
|
||||||
import PopoverMessage from "../Components/PopoverContainer";
|
import PopoverMessage from "../Components/PopoverContainer";
|
||||||
|
import useManagedIdentity from "./hooks/useManagedIdentity";
|
||||||
|
import { PermissionSectionConfig } from "./hooks/usePermissionsSection";
|
||||||
import useToggle from "./hooks/useToggle";
|
import useToggle from "./hooks/useToggle";
|
||||||
|
|
||||||
const managedIdentityTooltip = ContainerCopyMessages.defaultManagedIdentity.tooltip;
|
const managedIdentityTooltip = ContainerCopyMessages.defaultManagedIdentity.tooltip;
|
||||||
|
type AddManagedIdentityProps = Partial<PermissionSectionConfig>;
|
||||||
|
|
||||||
const DefaultManagedIdentity: React.FC = () => {
|
const DefaultManagedIdentity: React.FC<AddManagedIdentityProps> = () => {
|
||||||
const [defaultSystemAssigned, onToggle] = useToggle(false);
|
const [defaultSystemAssigned, onToggle] = useToggle(false);
|
||||||
|
const { loading, handleAddSystemIdentity } = useManagedIdentity(updateDefaultIdentity);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack className="defaultManagedIdentityContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}>
|
<Stack className="defaultManagedIdentityContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}>
|
||||||
@@ -27,10 +32,11 @@ const DefaultManagedIdentity: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<PopoverMessage
|
<PopoverMessage
|
||||||
|
isLoading={loading}
|
||||||
visible={defaultSystemAssigned}
|
visible={defaultSystemAssigned}
|
||||||
title={ContainerCopyMessages.defaultManagedIdentity.popoverTitle}
|
title={ContainerCopyMessages.defaultManagedIdentity.popoverTitle}
|
||||||
onCancel={() => onToggle(null, false)}
|
onCancel={() => onToggle(null, false)}
|
||||||
onPrimary={() => console.log('Primary action taken')}
|
onPrimary={handleAddSystemIdentity}
|
||||||
>
|
>
|
||||||
{ContainerCopyMessages.defaultManagedIdentity.popoverDescription}
|
{ContainerCopyMessages.defaultManagedIdentity.popoverDescription}
|
||||||
</PopoverMessage>
|
</PopoverMessage>
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ import React from "react";
|
|||||||
import ContainerCopyMessages from "../../../ContainerCopyMessages";
|
import ContainerCopyMessages from "../../../ContainerCopyMessages";
|
||||||
import { useCopyJobContext } from "../../../Context/CopyJobContext";
|
import { useCopyJobContext } from "../../../Context/CopyJobContext";
|
||||||
import { buildResourceLink } from "../../../CopyJobUtils";
|
import { buildResourceLink } from "../../../CopyJobUtils";
|
||||||
|
import { PermissionSectionConfig } from "./hooks/usePermissionsSection";
|
||||||
import useWindowOpenMonitor from "./hooks/useWindowOpenMonitor";
|
import useWindowOpenMonitor from "./hooks/useWindowOpenMonitor";
|
||||||
|
|
||||||
const OnlineCopyEnabled: React.FC = () => {
|
type AddManagedIdentityProps = Partial<PermissionSectionConfig>;
|
||||||
|
const OnlineCopyEnabled: React.FC<AddManagedIdentityProps> = () => {
|
||||||
const { copyJobState: { source } = {} } = useCopyJobContext();
|
const { copyJobState: { source } = {} } = useCopyJobContext();
|
||||||
const sourceAccountLink = buildResourceLink(source?.account);
|
const sourceAccountLink = buildResourceLink(source?.account);
|
||||||
const onlineCopyUrl = `${sourceAccountLink}/Features`;
|
const onlineCopyUrl = `${sourceAccountLink}/Features`;
|
||||||
|
|||||||
@@ -1,18 +1,44 @@
|
|||||||
import { PrimaryButton, Stack } from "@fluentui/react";
|
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 ContainerCopyMessages from "../../../ContainerCopyMessages";
|
||||||
import { useCopyJobContext } from "../../../Context/CopyJobContext";
|
import { useCopyJobContext } from "../../../Context/CopyJobContext";
|
||||||
import { buildResourceLink } from "../../../CopyJobUtils";
|
import { buildResourceLink } from "../../../CopyJobUtils";
|
||||||
|
import { PermissionSectionConfig } from "./hooks/usePermissionsSection";
|
||||||
import useWindowOpenMonitor from "./hooks/useWindowOpenMonitor";
|
import useWindowOpenMonitor from "./hooks/useWindowOpenMonitor";
|
||||||
|
|
||||||
const PointInTimeRestore: React.FC = () => {
|
type AddManagedIdentityProps = Partial<PermissionSectionConfig>;
|
||||||
const { copyJobState: { source } = {} } = useCopyJobContext();
|
const PointInTimeRestore: React.FC<AddManagedIdentityProps> = () => {
|
||||||
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
|
const { copyJobState: { source } = {}, setCopyJobState } = useCopyJobContext();
|
||||||
const sourceAccountLink = buildResourceLink(source?.account);
|
const sourceAccountLink = buildResourceLink(source?.account);
|
||||||
const pitrUrl = `${sourceAccountLink}/backupRestore`;
|
const pitrUrl = `${sourceAccountLink}/backupRestore`;
|
||||||
|
|
||||||
const onWindowClosed = () => {
|
const onWindowClosed = useCallback(async () => {
|
||||||
console.log('Point-in-time restore window closed');
|
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);
|
const openWindowAndMonitor = useWindowOpenMonitor(pitrUrl, onWindowClosed);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -21,7 +47,9 @@ const PointInTimeRestore: React.FC = () => {
|
|||||||
{ContainerCopyMessages.pointInTimeRestore.description}
|
{ContainerCopyMessages.pointInTimeRestore.description}
|
||||||
</div>
|
</div>
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
text={ContainerCopyMessages.pointInTimeRestore.buttonText}
|
text={loading ? "" : ContainerCopyMessages.pointInTimeRestore.buttonText}
|
||||||
|
{...(loading ? { iconProps: { iconName: "SyncStatusSolid" } } : {})}
|
||||||
|
disabled={loading}
|
||||||
onClick={openWindowAndMonitor}
|
onClick={openWindowAndMonitor}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</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 { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { RoleDefinitionType, useRoleDefinitions } from "hooks/useRoleDefinition";
|
import {
|
||||||
import { useMemo } from "react";
|
fetchRoleAssignments,
|
||||||
|
fetchRoleDefinitions,
|
||||||
|
RoleDefinitionType
|
||||||
|
} from "../../../../../../Utils/arm/RbacUtils";
|
||||||
import ContainerCopyMessages from "../../../../ContainerCopyMessages";
|
import ContainerCopyMessages from "../../../../ContainerCopyMessages";
|
||||||
import {
|
import {
|
||||||
BackupPolicyType,
|
BackupPolicyType,
|
||||||
@@ -9,6 +12,7 @@ import {
|
|||||||
IdentityType
|
IdentityType
|
||||||
} from "../../../../Enums";
|
} from "../../../../Enums";
|
||||||
import { CopyJobContextState } from "../../../../Types";
|
import { CopyJobContextState } from "../../../../Types";
|
||||||
|
import { useCopyJobPrerequisitesCache } from "../../../Utils/useCopyJobPrerequisitesCache";
|
||||||
import AddManagedIdentity from "../AddManagedIdentity";
|
import AddManagedIdentity from "../AddManagedIdentity";
|
||||||
import AddReadPermissionToDefaultIdentity from "../AddReadPermissionToDefaultIdentity";
|
import AddReadPermissionToDefaultIdentity from "../AddReadPermissionToDefaultIdentity";
|
||||||
import DefaultManagedIdentity from "../DefaultManagedIdentity";
|
import DefaultManagedIdentity from "../DefaultManagedIdentity";
|
||||||
@@ -18,13 +22,14 @@ import PointInTimeRestore from "../PointInTimeRestore";
|
|||||||
export interface PermissionSectionConfig {
|
export interface PermissionSectionConfig {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
Component: React.FC;
|
Component: React.ComponentType
|
||||||
disabled?: boolean;
|
disabled: boolean;
|
||||||
completed?: boolean;
|
completed?: boolean;
|
||||||
|
validate?: (state: CopyJobContextState, armToken?: string) => boolean | Promise<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Section IDs for maintainability
|
// Section IDs for maintainability
|
||||||
const SECTION_IDS = {
|
export const SECTION_IDS = {
|
||||||
addManagedIdentity: "addManagedIdentity",
|
addManagedIdentity: "addManagedIdentity",
|
||||||
defaultManagedIdentity: "defaultManagedIdentity",
|
defaultManagedIdentity: "defaultManagedIdentity",
|
||||||
readPermissionAssigned: "readPermissionAssigned",
|
readPermissionAssigned: "readPermissionAssigned",
|
||||||
@@ -37,16 +42,46 @@ const PERMISSION_SECTIONS_CONFIG: PermissionSectionConfig[] = [
|
|||||||
id: SECTION_IDS.addManagedIdentity,
|
id: SECTION_IDS.addManagedIdentity,
|
||||||
title: ContainerCopyMessages.addManagedIdentity.title,
|
title: ContainerCopyMessages.addManagedIdentity.title,
|
||||||
Component: AddManagedIdentity,
|
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,
|
id: SECTION_IDS.defaultManagedIdentity,
|
||||||
title: ContainerCopyMessages.defaultManagedIdentity.title,
|
title: ContainerCopyMessages.defaultManagedIdentity.title,
|
||||||
Component: DefaultManagedIdentity,
|
Component: DefaultManagedIdentity,
|
||||||
|
disabled: true,
|
||||||
|
validate: (state: CopyJobContextState) => {
|
||||||
|
const targetAccountDefaultIdentity = (state?.target?.account?.properties?.defaultIdentity ?? "").toLowerCase();
|
||||||
|
return targetAccountDefaultIdentity === DefaultIdentityType.SystemAssignedIdentity;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: SECTION_IDS.readPermissionAssigned,
|
id: SECTION_IDS.readPermissionAssigned,
|
||||||
title: ContainerCopyMessages.readPermissionAssigned.title,
|
title: ContainerCopyMessages.readPermissionAssigned.title,
|
||||||
Component: AddReadPermissionToDefaultIdentity,
|
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,
|
id: SECTION_IDS.pointInTimeRestore,
|
||||||
title: ContainerCopyMessages.pointInTimeRestore.title,
|
title: ContainerCopyMessages.pointInTimeRestore.title,
|
||||||
Component: PointInTimeRestore,
|
Component: PointInTimeRestore,
|
||||||
|
disabled: true,
|
||||||
|
validate: (state: CopyJobContextState) => {
|
||||||
|
const sourceAccountBackupPolicy = state?.source?.account?.properties?.backupPolicy?.type ?? "";
|
||||||
|
return sourceAccountBackupPolicy === BackupPolicyType.Continuous;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: SECTION_IDS.onlineCopyEnabled,
|
id: SECTION_IDS.onlineCopyEnabled,
|
||||||
title: ContainerCopyMessages.onlineCopyEnabled.title,
|
title: ContainerCopyMessages.onlineCopyEnabled.title,
|
||||||
Component: OnlineCopyEnabled,
|
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.
|
* 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(
|
return roleDefinitions?.some(
|
||||||
role =>
|
role =>
|
||||||
role.name === "00000000-0000-0000-0000-000000000001" ||
|
role.name === "00000000-0000-0000-0000-000000000001" ||
|
||||||
@@ -86,105 +130,76 @@ export function checkUserHasReaderRole(roleDefinitions: RoleDefinitionType[]): b
|
|||||||
const usePermissionSections = (
|
const usePermissionSections = (
|
||||||
state: CopyJobContextState,
|
state: CopyJobContextState,
|
||||||
armToken: string,
|
armToken: string,
|
||||||
principalId: string
|
|
||||||
): PermissionSectionConfig[] => {
|
): 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 sectionToValidate = useMemo(() => {
|
||||||
const targetAccountIdentityType = useMemo(
|
const baseSections = [...PERMISSION_SECTIONS_CONFIG];
|
||||||
() => (target?.account?.identity?.type ?? "").toLowerCase(),
|
if (state.migrationType === CopyJobMigrationType.Online) {
|
||||||
[target?.account?.identity?.type]
|
return [...baseSections, ...PERMISSION_SECTIONS_FOR_ONLINE_JOBS];
|
||||||
);
|
|
||||||
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]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Fetch role assignments and definitions
|
|
||||||
const roleAssigned = useRoleAssignments(
|
|
||||||
armToken,
|
|
||||||
source?.subscription?.subscriptionId,
|
|
||||||
source?.account?.resourceGroup,
|
|
||||||
source?.account?.name,
|
|
||||||
principalId
|
|
||||||
);
|
|
||||||
|
|
||||||
const roleDefinitions = useRoleDefinitions(
|
|
||||||
armToken,
|
|
||||||
roleAssigned ?? []
|
|
||||||
);
|
|
||||||
|
|
||||||
const hasReaderRole = useMemo(
|
|
||||||
() => checkUserHasReaderRole(roleDefinitions ?? []),
|
|
||||||
[roleDefinitions]
|
|
||||||
);
|
|
||||||
|
|
||||||
// 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
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
if (
|
return baseSections;
|
||||||
section.id === SECTION_IDS.defaultManagedIdentity &&
|
}, [state.migrationType]);
|
||||||
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(() => {
|
const memoizedValidationCache = useMemo(() => {
|
||||||
if (state.migrationType !== CopyJobMigrationType.Online) return [];
|
if (state.migrationType === CopyJobMigrationType.Offline) {
|
||||||
return PERMISSION_SECTIONS_FOR_ONLINE_JOBS.map(section => {
|
validationCache.delete(SECTION_IDS.pointInTimeRestore);
|
||||||
if (
|
validationCache.delete(SECTION_IDS.onlineCopyEnabled);
|
||||||
section.id === SECTION_IDS.pointInTimeRestore &&
|
|
||||||
sourceAccountBackupPolicy === BackupPolicyType.Continuous
|
|
||||||
) {
|
|
||||||
return {
|
|
||||||
...section,
|
|
||||||
disabled: true,
|
|
||||||
completed: true
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
return section;
|
return validationCache;
|
||||||
});
|
}, [state.migrationType]);
|
||||||
}, [state.migrationType, sourceAccountBackupPolicy]);
|
|
||||||
|
|
||||||
// Combine and memoize final sections
|
useEffect(() => {
|
||||||
const permissionSections = useMemo(
|
const validateSections = async () => {
|
||||||
() => [...getBaseSections, ...getOnlineSections],
|
if (isValidatingRef.current) return;
|
||||||
[getBaseSections, getOnlineSections]
|
|
||||||
);
|
|
||||||
|
|
||||||
return permissionSections;
|
isValidatingRef.current = true;
|
||||||
|
const result: PermissionSectionConfig[] = [];
|
||||||
|
const newValidationCache = new Map(memoizedValidationCache);
|
||||||
|
|
||||||
|
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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setValidationCache(newValidationCache);
|
||||||
|
setPermissionSections(result);
|
||||||
|
isValidatingRef.current = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
validateSections();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
isValidatingRef.current = false;
|
||||||
|
}
|
||||||
|
}, [state, armToken, sectionToValidate]);
|
||||||
|
|
||||||
|
return permissionSections ?? [];
|
||||||
};
|
};
|
||||||
|
|
||||||
export default usePermissionSections;
|
export default usePermissionSections;
|
||||||
@@ -1,10 +1,17 @@
|
|||||||
import { Image, Stack, Text } from "@fluentui/react";
|
import { Image, Stack, Text } from "@fluentui/react";
|
||||||
import { Accordion, AccordionHeader, AccordionItem, AccordionPanel } from "@fluentui/react-components";
|
import {
|
||||||
import React from "react";
|
Accordion,
|
||||||
|
AccordionHeader,
|
||||||
|
AccordionItem,
|
||||||
|
AccordionPanel
|
||||||
|
} from "@fluentui/react-components";
|
||||||
|
import React, { useEffect } from "react";
|
||||||
import CheckmarkIcon from "../../../../../../images/successfulPopup.svg";
|
import CheckmarkIcon from "../../../../../../images/successfulPopup.svg";
|
||||||
import WarningIcon from "../../../../../../images/warning.svg";
|
import WarningIcon from "../../../../../../images/warning.svg";
|
||||||
|
import ShimmerTree, { IndentLevel } from "../../../../../Common/ShimmerTree";
|
||||||
import ContainerCopyMessages from "../../../ContainerCopyMessages";
|
import ContainerCopyMessages from "../../../ContainerCopyMessages";
|
||||||
import { useCopyJobContext } from "../../../Context/CopyJobContext";
|
import { useCopyJobContext } from "../../../Context/CopyJobContext";
|
||||||
|
import { CopyJobMigrationType } from "../../../Enums";
|
||||||
import usePermissionSections, { PermissionSectionConfig } from "./hooks/usePermissionsSection";
|
import usePermissionSections, { PermissionSectionConfig } from "./hooks/usePermissionsSection";
|
||||||
|
|
||||||
const PermissionSection: React.FC<PermissionSectionConfig> = ({
|
const PermissionSection: React.FC<PermissionSectionConfig> = ({
|
||||||
@@ -32,20 +39,49 @@ const PermissionSection: React.FC<PermissionSectionConfig> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const AssignPermissions = () => {
|
const AssignPermissions = () => {
|
||||||
const { armToken, principalId, copyJobState } = useCopyJobContext();
|
const { armToken, copyJobState } = useCopyJobContext();
|
||||||
const permissionSections = usePermissionSections(copyJobState, armToken, principalId);
|
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 (
|
return (
|
||||||
<Stack className="assignPermissionsContainer" tokens={{ childrenGap: 15 }}>
|
<Stack className="assignPermissionsContainer" tokens={{ childrenGap: 15 }}>
|
||||||
<span>
|
<span>
|
||||||
{ContainerCopyMessages.assignPermissions.description}
|
{ContainerCopyMessages.assignPermissions.description}
|
||||||
</span>
|
</span>
|
||||||
<Accordion className="permissionsAccordion" collapsible>
|
{
|
||||||
|
permissionSections?.length === 0 ? (
|
||||||
|
<ShimmerTree indentLevels={indentLevels} style={{ width: '100%' }} />
|
||||||
|
) : (
|
||||||
|
<Accordion
|
||||||
|
className="permissionsAccordion"
|
||||||
|
collapsible
|
||||||
|
openItems={openItems}
|
||||||
|
>
|
||||||
{
|
{
|
||||||
permissionSections.map(section => (
|
permissionSections.map(section => (
|
||||||
<PermissionSection key={section.id} {...section} />
|
<PermissionSection key={section.id} {...section} />
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
</Accordion>
|
</Accordion>
|
||||||
|
)
|
||||||
|
}
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,26 +2,33 @@ import { DefaultButton, PrimaryButton, Stack, Text } from "@fluentui/react";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
interface PopoverContainerProps {
|
interface PopoverContainerProps {
|
||||||
|
isLoading?: boolean;
|
||||||
title?: string;
|
title?: string;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
onPrimary: () => void;
|
onPrimary: () => void;
|
||||||
onCancel: () => 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 (
|
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 variant="mediumPlus" style={{ fontWeight: 600 }}>{title}</Text>
|
||||||
<Text>{children}</Text>
|
<Text>{children}</Text>
|
||||||
<Stack horizontal tokens={{ childrenGap: 20 }}>
|
<Stack horizontal tokens={{ childrenGap: 20 }}>
|
||||||
<PrimaryButton text="Yes" onClick={onPrimary} />
|
<PrimaryButton
|
||||||
<DefaultButton text="No" onClick={onCancel} />
|
text={isLoading ? "" : "Yes"}
|
||||||
|
{...(isLoading ? { iconProps: { iconName: "SyncStatusSolid" } } : {})}
|
||||||
|
onClick={onPrimary}
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
<DefaultButton text="No" onClick={onCancel} disabled={isLoading} />
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
interface PopoverMessageProps {
|
interface PopoverMessageProps {
|
||||||
|
isLoading?: boolean;
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
title: string;
|
title: string;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
@@ -29,10 +36,10 @@ interface PopoverMessageProps {
|
|||||||
children: React.ReactNode;
|
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;
|
if (!visible) return null;
|
||||||
return (
|
return (
|
||||||
<PopoverContainer title={title} onCancel={onCancel} onPrimary={onPrimary}>
|
<PopoverContainer title={title} onCancel={onCancel} onPrimary={onPrimary} isLoading={isLoading}>
|
||||||
{children}
|
{children}
|
||||||
</PopoverContainer>
|
</PopoverContainer>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { submitCreateCopyJob } from "Explorer/ContainerCopy/Actions/CopyJobActions";
|
|
||||||
import { useCallback, useMemo, useReducer } from "react";
|
import { useCallback, useMemo, useReducer } from "react";
|
||||||
import { useSidePanel } from "../../../../hooks/useSidePanel";
|
import { useSidePanel } from "../../../../hooks/useSidePanel";
|
||||||
|
import { submitCreateCopyJob } from "../../Actions/CopyJobActions";
|
||||||
import { CopyJobContextState } from "../../Types";
|
import { CopyJobContextState } from "../../Types";
|
||||||
|
import { useCopyJobPrerequisitesCache } from "./useCopyJobPrerequisitesCache";
|
||||||
import { SCREEN_KEYS, useCreateCopyJobScreensList } from "./useCreateCopyJobScreensList";
|
import { SCREEN_KEYS, useCreateCopyJobScreensList } from "./useCreateCopyJobScreensList";
|
||||||
|
|
||||||
type NavigationState = {
|
type NavigationState = {
|
||||||
@@ -34,14 +35,18 @@ function navigationReducer(state: NavigationState, action: Action): NavigationSt
|
|||||||
|
|
||||||
export function useCopyJobNavigation(copyJobState: CopyJobContextState) {
|
export function useCopyJobNavigation(copyJobState: CopyJobContextState) {
|
||||||
const screens = useCreateCopyJobScreensList();
|
const screens = useCreateCopyJobScreensList();
|
||||||
|
const { validationCache: cache } = useCopyJobPrerequisitesCache();
|
||||||
const [state, dispatch] = useReducer(navigationReducer, { screenHistory: [SCREEN_KEYS.SelectAccount] });
|
const [state, dispatch] = useReducer(navigationReducer, { screenHistory: [SCREEN_KEYS.SelectAccount] });
|
||||||
|
|
||||||
const currentScreenKey = state.screenHistory[state.screenHistory.length - 1];
|
const currentScreenKey = state.screenHistory[state.screenHistory.length - 1];
|
||||||
const currentScreen = screens.find((screen) => screen.key === currentScreenKey);
|
const currentScreen = screens.find((screen) => screen.key === currentScreenKey);
|
||||||
|
|
||||||
const isPrimaryDisabled = useMemo(
|
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(() => {
|
const primaryBtnText = useMemo(() => {
|
||||||
if (currentScreenKey === SCREEN_KEYS.PreviewCopyJob) {
|
if (currentScreenKey === SCREEN_KEYS.PreviewCopyJob) {
|
||||||
@@ -53,13 +58,16 @@ export function useCopyJobNavigation(copyJobState: CopyJobContextState) {
|
|||||||
const isPreviousDisabled = state.screenHistory.length <= 1;
|
const isPreviousDisabled = state.screenHistory.length <= 1;
|
||||||
|
|
||||||
const handlePrimary = useCallback(() => {
|
const handlePrimary = useCallback(() => {
|
||||||
if (currentScreenKey === SCREEN_KEYS.SelectAccount) {
|
const transitions = {
|
||||||
dispatch({ type: "NEXT", nextScreen: SCREEN_KEYS.SelectSourceAndTargetContainers });
|
[SCREEN_KEYS.SelectAccount]: SCREEN_KEYS.AssignPermissions,
|
||||||
}
|
[SCREEN_KEYS.AssignPermissions]: SCREEN_KEYS.SelectSourceAndTargetContainers,
|
||||||
if (currentScreenKey === SCREEN_KEYS.SelectSourceAndTargetContainers) {
|
[SCREEN_KEYS.SelectSourceAndTargetContainers]: SCREEN_KEYS.PreviewCopyJob,
|
||||||
dispatch({ type: "NEXT", nextScreen: SCREEN_KEYS.PreviewCopyJob });
|
};
|
||||||
}
|
|
||||||
if (currentScreenKey === SCREEN_KEYS.PreviewCopyJob) {
|
const nextScreen = transitions[currentScreenKey];
|
||||||
|
if (nextScreen) {
|
||||||
|
dispatch({ type: "NEXT", nextScreen });
|
||||||
|
} else if (currentScreenKey === SCREEN_KEYS.PreviewCopyJob) {
|
||||||
submitCreateCopyJob(copyJobState);
|
submitCreateCopyJob(copyJobState);
|
||||||
}
|
}
|
||||||
}, [currentScreenKey, 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 = {
|
type Validation = {
|
||||||
validate: (state: CopyJobContextState) => boolean;
|
validate: (state: CopyJobContextState | Map<string, boolean>) => boolean;
|
||||||
message: string;
|
message: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -61,7 +61,18 @@ function useCreateCopyJobScreensList() {
|
|||||||
{
|
{
|
||||||
key: SCREEN_KEYS.AssignPermissions,
|
key: SCREEN_KEYS.AssignPermissions,
|
||||||
component: <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 {
|
export interface CopyJobContextState {
|
||||||
jobName: string;
|
jobName: string;
|
||||||
migrationType: CopyJobMigrationType;
|
migrationType: CopyJobMigrationType;
|
||||||
|
sourceReadAccessFromTarget: boolean;
|
||||||
// source details
|
// source details
|
||||||
source: {
|
source: {
|
||||||
subscription: Subscription;
|
subscription: Subscription;
|
||||||
|
|||||||
@@ -60,13 +60,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.popover-container {
|
||||||
|
button[disabled] {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
}
|
||||||
.foreground {
|
.foreground {
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
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%, -5%);
|
transform: translate(0%, -9%);
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
128
src/Utils/arm/RbacUtils.ts
Normal file
128
src/Utils/arm/RbacUtils.ts
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
import { configContext } from "ConfigContext";
|
||||||
|
import { armRequest } from "Utils/arm/request";
|
||||||
|
|
||||||
|
export type FetchAccountDetailsParams = {
|
||||||
|
armToken: string;
|
||||||
|
subscriptionId: string;
|
||||||
|
resourceGroupName: string;
|
||||||
|
accountName: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RoleAssignmentPropertiesType = {
|
||||||
|
roleDefinitionId: string;
|
||||||
|
principalId: string;
|
||||||
|
scope: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RoleAssignmentType = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
properties: RoleAssignmentPropertiesType;
|
||||||
|
type: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type RoleDefinitionDataActions = {
|
||||||
|
dataActions: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RoleDefinitionType = {
|
||||||
|
assignableScopes: string[];
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
permissions: RoleDefinitionDataActions[];
|
||||||
|
resourceGroup: string;
|
||||||
|
roleName: string;
|
||||||
|
type: string;
|
||||||
|
typePropertiesType: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const apiVersion = "2025-04-15";
|
||||||
|
|
||||||
|
const getArmBaseUrl = (): string => {
|
||||||
|
const base = configContext.ARM_ENDPOINT;
|
||||||
|
return base.endsWith("/") ? base.slice(0, -1) : base;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createAuthHeaders = (armToken: string): Headers => {
|
||||||
|
const headers = new Headers();
|
||||||
|
headers.append("Authorization", `Bearer ${armToken}`);
|
||||||
|
headers.append("Content-Type", "application/json");
|
||||||
|
return headers;
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildArmUrl = (path: string): string =>
|
||||||
|
`${getArmBaseUrl()}${path}?api-version=${apiVersion}`;
|
||||||
|
|
||||||
|
const handleResponse = async (response: Response, context: string) => {
|
||||||
|
if (!response.ok) {
|
||||||
|
const body = await response.text().catch(() => "");
|
||||||
|
throw new Error(
|
||||||
|
`Failed to fetch ${context}. Status: ${response.status}. ${body || ""}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchRoleAssignments = async (
|
||||||
|
armToken: string,
|
||||||
|
subscriptionId: string,
|
||||||
|
resourceGroupName: string,
|
||||||
|
accountName: string,
|
||||||
|
principalId: string
|
||||||
|
): Promise<RoleAssignmentType[]> => {
|
||||||
|
const uri = buildArmUrl(
|
||||||
|
`/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/sqlRoleAssignments`
|
||||||
|
);
|
||||||
|
|
||||||
|
const response = await fetch(uri, { method: "GET", headers: createAuthHeaders(armToken) });
|
||||||
|
const data = await handleResponse(response, "role assignments");
|
||||||
|
|
||||||
|
return (data.value || []).filter(
|
||||||
|
(assignment: RoleAssignmentType) =>
|
||||||
|
assignment?.properties?.principalId === principalId
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchRoleDefinitions = async (
|
||||||
|
armToken: string,
|
||||||
|
roleAssignments: RoleAssignmentType[]
|
||||||
|
): Promise<RoleDefinitionType[]> => {
|
||||||
|
const roleDefinitionIds = roleAssignments.map(assignment => assignment.properties.roleDefinitionId);
|
||||||
|
const uniqueRoleDefinitionIds = Array.from(new Set(roleDefinitionIds));
|
||||||
|
|
||||||
|
const headers = createAuthHeaders(armToken);
|
||||||
|
const roleDefinitionUris = uniqueRoleDefinitionIds.map((id) => buildArmUrl(id));
|
||||||
|
|
||||||
|
const promises = roleDefinitionUris.map((url) => fetch(url, { method: "GET", headers }));
|
||||||
|
const responses = await Promise.all(promises);
|
||||||
|
|
||||||
|
const roleDefinitions = await Promise.all(
|
||||||
|
responses.map((res, i) => handleResponse(res, `role definition ${uniqueRoleDefinitionIds[i]}`))
|
||||||
|
);
|
||||||
|
|
||||||
|
return roleDefinitions;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const assignRole = async (
|
||||||
|
subscriptionId: string,
|
||||||
|
resourceGroupName: string,
|
||||||
|
accountName: string,
|
||||||
|
principalId: string
|
||||||
|
): Promise<RoleAssignmentType> => {
|
||||||
|
const accountScope = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}`;
|
||||||
|
const roleDefinitionId = `${accountScope}/sqlRoleDefinitions/00000000-0000-0000-0000-000000000001`;
|
||||||
|
const roleAssignmentName = crypto.randomUUID();
|
||||||
|
const path = `${accountScope}/sqlRoleAssignments/${roleAssignmentName}`;
|
||||||
|
|
||||||
|
const body = {
|
||||||
|
properties: {
|
||||||
|
roleDefinitionId,
|
||||||
|
scope: `${accountScope}/`,
|
||||||
|
principalId
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const response: RoleAssignmentType = await armRequest({
|
||||||
|
host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
};
|
||||||
38
src/Utils/arm/databaseAccountUtils.ts
Normal file
38
src/Utils/arm/databaseAccountUtils.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { DatabaseAccount } from "Contracts/DataModels";
|
||||||
|
import { userContext } from "UserContext";
|
||||||
|
import { configContext } from "../../ConfigContext";
|
||||||
|
|
||||||
|
const apiVersion = "2025-04-15";
|
||||||
|
export type FetchAccountDetailsParams = {
|
||||||
|
subscriptionId: string;
|
||||||
|
resourceGroupName: string;
|
||||||
|
accountName: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildUrl = (params: FetchAccountDetailsParams): string => {
|
||||||
|
const { subscriptionId, resourceGroupName, accountName } = params;
|
||||||
|
|
||||||
|
let armEndpoint = configContext.ARM_ENDPOINT;
|
||||||
|
if (armEndpoint.endsWith("/")) {
|
||||||
|
armEndpoint = armEndpoint.slice(0, -1);
|
||||||
|
}
|
||||||
|
return `${armEndpoint}/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}?api-version=${apiVersion}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchDatabaseAccount(
|
||||||
|
subscriptionId: string,
|
||||||
|
resourceGroupName: string,
|
||||||
|
accountName: string
|
||||||
|
) {
|
||||||
|
const headers = new Headers();
|
||||||
|
headers.append("Authorization", userContext.authorizationToken);
|
||||||
|
headers.append("Content-Type", "application/json");
|
||||||
|
const uri = buildUrl({ subscriptionId, resourceGroupName, accountName });
|
||||||
|
const response = await fetch(uri, { method: "GET", headers: headers });
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Error fetching database account: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
const account: DatabaseAccount = await response.json();
|
||||||
|
return account;
|
||||||
|
}
|
||||||
56
src/Utils/arm/identityUtils.ts
Normal file
56
src/Utils/arm/identityUtils.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { DatabaseAccount } from "Contracts/DataModels";
|
||||||
|
import { configContext } from "../../ConfigContext";
|
||||||
|
import { fetchDatabaseAccount } from "./databaseAccountUtils";
|
||||||
|
import { armRequest } from "./request";
|
||||||
|
|
||||||
|
const apiVersion = "2025-04-15";
|
||||||
|
|
||||||
|
const updateIdentity = async (
|
||||||
|
subscriptionId: string,
|
||||||
|
resourceGroupName: string,
|
||||||
|
accountName: string,
|
||||||
|
body: object
|
||||||
|
): Promise<DatabaseAccount> => {
|
||||||
|
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}`;
|
||||||
|
const response: { status: string } = await armRequest({
|
||||||
|
host: configContext.ARM_ENDPOINT, path, method: "PATCH", apiVersion, body
|
||||||
|
});
|
||||||
|
if (response.status === "Succeeded") {
|
||||||
|
const account = await fetchDatabaseAccount(subscriptionId, resourceGroupName, accountName);
|
||||||
|
return account;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateSystemIdentity = async (
|
||||||
|
subscriptionId: string,
|
||||||
|
resourceGroupName: string,
|
||||||
|
accountName: string
|
||||||
|
): Promise<DatabaseAccount> => {
|
||||||
|
const body = {
|
||||||
|
identity: {
|
||||||
|
type: "SystemAssigned"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const updatedAccount = await updateIdentity(subscriptionId, resourceGroupName, accountName, body);
|
||||||
|
return updatedAccount;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateDefaultIdentity = async (
|
||||||
|
subscriptionId: string,
|
||||||
|
resourceGroupName: string,
|
||||||
|
accountName: string
|
||||||
|
): Promise<DatabaseAccount> => {
|
||||||
|
const body = {
|
||||||
|
properties: {
|
||||||
|
defaultIdentity: "SystemAssignedIdentity"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const updatedAccount = await updateIdentity(subscriptionId, resourceGroupName, accountName, body);
|
||||||
|
return updatedAccount;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export { updateDefaultIdentity, updateSystemIdentity };
|
||||||
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
import useSWR from "swr";
|
|
||||||
import { configContext } from "../ConfigContext";
|
|
||||||
|
|
||||||
const apiVersion = "2025-04-15";
|
|
||||||
|
|
||||||
export type FetchAccountDetailsParams = {
|
|
||||||
armToken: string;
|
|
||||||
subscriptionId: string;
|
|
||||||
resourceGroupName: string;
|
|
||||||
accountName: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type RoleAssignmentPropertiesType = {
|
|
||||||
roleDefinitionId: string;
|
|
||||||
principalId: string;
|
|
||||||
scope: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type RoleAssignmentType = {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
properties: RoleAssignmentPropertiesType;
|
|
||||||
type: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const buildRoleAssignmentsListUrl = (params: FetchAccountDetailsParams): string => {
|
|
||||||
const { subscriptionId, resourceGroupName, accountName } = params;
|
|
||||||
|
|
||||||
let armEndpoint = configContext.ARM_ENDPOINT;
|
|
||||||
if (armEndpoint.endsWith("/")) {
|
|
||||||
armEndpoint = armEndpoint.slice(0, -1);
|
|
||||||
}
|
|
||||||
return `${armEndpoint}/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/sqlRoleAssignments?api-version=${apiVersion}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchRoleAssignments = async (
|
|
||||||
armToken: string,
|
|
||||||
subscriptionId: string,
|
|
||||||
resourceGroupName: string,
|
|
||||||
accountName: string,
|
|
||||||
principalId: string
|
|
||||||
): Promise<RoleAssignmentType[]> => {
|
|
||||||
const uri = buildRoleAssignmentsListUrl({
|
|
||||||
armToken,
|
|
||||||
subscriptionId,
|
|
||||||
resourceGroupName,
|
|
||||||
accountName
|
|
||||||
});
|
|
||||||
const headers = new Headers();
|
|
||||||
const bearer = `Bearer ${armToken}`;
|
|
||||||
headers.append("Authorization", bearer);
|
|
||||||
headers.append("Content-Type", "application/json");
|
|
||||||
|
|
||||||
const response = await fetch(uri, {
|
|
||||||
method: "GET",
|
|
||||||
headers: headers
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error("Failed to fetch containers");
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
const assignments = data.value;
|
|
||||||
const rolesAssignedToLoggedinUser = assignments.filter((assignment: RoleAssignmentType) => assignment?.properties?.principalId === principalId);
|
|
||||||
return rolesAssignedToLoggedinUser;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function useRoleAssignments(
|
|
||||||
armToken: string,
|
|
||||||
subscriptionId: string,
|
|
||||||
resourceGroupName: string,
|
|
||||||
accountName: string,
|
|
||||||
principalId: string
|
|
||||||
): RoleAssignmentType[] | undefined {
|
|
||||||
const { data } = useSWR(
|
|
||||||
() => (
|
|
||||||
armToken && subscriptionId && resourceGroupName && accountName && principalId ? [
|
|
||||||
"fetchRoleAssignmentsLinkedToAccount",
|
|
||||||
armToken, subscriptionId, resourceGroupName, accountName, principalId
|
|
||||||
] : undefined
|
|
||||||
),
|
|
||||||
(_, armToken, subscriptionId, resourceGroupName, accountName, principalId) => fetchRoleAssignments(
|
|
||||||
armToken,
|
|
||||||
subscriptionId,
|
|
||||||
resourceGroupName,
|
|
||||||
accountName,
|
|
||||||
principalId
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
import useSWR from "swr";
|
|
||||||
import { configContext } from "../ConfigContext";
|
|
||||||
import { RoleAssignmentType } from "./useRoleAssignments";
|
|
||||||
|
|
||||||
type RoleDefinitionDataActions = {
|
|
||||||
dataActions: string[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type RoleDefinitionType = {
|
|
||||||
assignableScopes: string[];
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
permissions: RoleDefinitionDataActions[];
|
|
||||||
resourceGroup: string;
|
|
||||||
roleName: string;
|
|
||||||
type: string;
|
|
||||||
typePropertiesType: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const apiVersion = "2025-04-15";
|
|
||||||
const buildRoleDefinitionUrl = (roleDefinitionId: string): string => {
|
|
||||||
let armEndpoint = configContext.ARM_ENDPOINT;
|
|
||||||
if (armEndpoint.endsWith("/")) {
|
|
||||||
armEndpoint = armEndpoint.slice(0, -1);
|
|
||||||
}
|
|
||||||
return `${armEndpoint}${roleDefinitionId}?api-version=${apiVersion}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchRoleDefinitions = async (
|
|
||||||
armToken: string,
|
|
||||||
roleAssignments: RoleAssignmentType[],
|
|
||||||
): Promise<RoleDefinitionType[]> => {
|
|
||||||
const roleDefinitionIds = roleAssignments.map(assignment => assignment.properties.roleDefinitionId);
|
|
||||||
const uniqueRoleDefinitionIds = Array.from(new Set(roleDefinitionIds));
|
|
||||||
|
|
||||||
const roleDefinitionUris = uniqueRoleDefinitionIds.map(roleDefinitionId => buildRoleDefinitionUrl(roleDefinitionId));
|
|
||||||
const headers = {
|
|
||||||
Authorization: `Bearer ${armToken}`,
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
};
|
|
||||||
const promises = roleDefinitionUris.map(uri => fetch(uri, { method: "GET", headers }));
|
|
||||||
const responses = await Promise.all(promises);
|
|
||||||
for (const response of responses) {
|
|
||||||
if (!response.ok) throw new Error("Failed to fetch role definitions");
|
|
||||||
}
|
|
||||||
const roleDefinitions = await Promise.all(responses.map(r => r.json()));
|
|
||||||
return roleDefinitions;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function useRoleDefinitions(
|
|
||||||
armToken: string,
|
|
||||||
roleAssignments: RoleAssignmentType[],
|
|
||||||
): RoleDefinitionType[] | undefined {
|
|
||||||
const { data } = useSWR(
|
|
||||||
() => (
|
|
||||||
armToken && roleAssignments?.length ? [
|
|
||||||
"fetchRoleDefinitionsForTheAssignments",
|
|
||||||
armToken, roleAssignments
|
|
||||||
] : undefined
|
|
||||||
),
|
|
||||||
(_, armToken, roleAssignments) => fetchRoleDefinitions(
|
|
||||||
armToken,
|
|
||||||
roleAssignments
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user