mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-27 21:01:57 +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 { 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;
|
||||
}
|
||||
}
|
||||
|
||||
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