Initial dev for container copy

This commit is contained in:
Bikram Choudhury
2025-10-10 17:20:24 +05:30
parent d924824536
commit c83f4fc431
37 changed files with 1436 additions and 19 deletions

View File

@@ -0,0 +1,19 @@
import React from "react";
import { useSidePanel } from "../../../hooks/useSidePanel";
import ContainerCopyMessages from "../ContainerCopyMessages";
import CreateCopyJobScreensProvider from "../CreateCopyJob/Screens/CreateCopyJobScreensProvider";
import { CopyJobContextState } from "../Types";
export const openCreateCopyJobPanel = () => {
const sidePanelState = useSidePanel.getState()
sidePanelState.setPanelHasConsole(false);
sidePanelState.openSidePanel(
ContainerCopyMessages.createCopyJobPanelTitle,
<CreateCopyJobScreensProvider />,
"600px"
);
}
export const submitCreateCopyJob = (state: CopyJobContextState) => {
console.log("Submitting create copy job with state:", state);
};

View File

@@ -0,0 +1,31 @@
import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react";
import React from "react";
import { StyleConstants } from "../../../Common/StyleConstants";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import * as CommandBarUtil from "../../Menus/CommandBar/CommandBarUtil";
import { ContainerCopyProps } from "../Types";
import { getCommandBarButtons } from "./Utils";
const backgroundColor = StyleConstants.BaseLight;
const rootStyle = {
root: {
backgroundColor: backgroundColor,
},
};
const CopyJobCommandBar: React.FC<ContainerCopyProps> = ({ container }) => {
const commandBarItems: CommandButtonComponentProps[] = getCommandBarButtons(container);
const controlButtons: ICommandBarItemProps[] = CommandBarUtil.convertButton(commandBarItems, backgroundColor);
return (
<div className="commandBarContainer">
<FluentCommandBar
ariaLabel="Use left and right arrow keys to navigate between commands"
styles={rootStyle}
items={controlButtons}
/>
</div>
);
}
export default CopyJobCommandBar;

View File

@@ -0,0 +1,56 @@
import AddIcon from "../../../../images/Add.svg";
import FeedbackIcon from "../../../../images/Feedback-Command.svg";
import RefreshIcon from "../../../../images/refresh-cosmos.svg";
import { configContext, Platform } from "../../../ConfigContext";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../Explorer";
import * as Actions from "../Actions/CopyJobActions";
import ContainerCopyMessages from "../ContainerCopyMessages";
import { CopyJobCommandBarBtnType } from "../Types";
function getCopyJobBtns(): CopyJobCommandBarBtnType[] {
const buttons: CopyJobCommandBarBtnType[] = [
{
key: "createCopyJob",
iconSrc: AddIcon,
label: ContainerCopyMessages.createCopyJobButtonLabel,
ariaLabel: ContainerCopyMessages.createCopyJobButtonAriaLabel,
onClick: Actions.openCreateCopyJobPanel,
},
{
key: "refresh",
iconSrc: RefreshIcon,
label: ContainerCopyMessages.refreshButtonLabel,
ariaLabel: ContainerCopyMessages.refreshButtonAriaLabel,
onClick: () => { },
},
];
if (configContext.platform === Platform.Portal) {
buttons.push({
key: "feedback",
iconSrc: FeedbackIcon,
label: ContainerCopyMessages.feedbackButtonLabel,
ariaLabel: ContainerCopyMessages.feedbackButtonAriaLabel,
onClick: () => { },
});
}
return buttons;
}
function btnMapper(config: CopyJobCommandBarBtnType): CommandButtonComponentProps {
return {
iconSrc: config.iconSrc,
iconAlt: config.label,
onCommandClick: config.onClick,
commandButtonLabel: undefined as string | undefined,
ariaLabel: config.ariaLabel,
tooltipText: config.label,
hasPopup: false,
disabled: config.disabled ?? false,
};
}
export function getCommandBarButtons(_container: Explorer): CommandButtonComponentProps[] {
return getCopyJobBtns().map(btnMapper);
}

View File

@@ -0,0 +1,42 @@
export default {
// Copy Job Command Bar
feedbackButtonLabel: "Feedback",
feedbackButtonAriaLabel: "Provide feedback on copy jobs",
refreshButtonLabel: "Refresh",
refreshButtonAriaLabel: "Refresh copy jobs",
createCopyJobButtonLabel: "Create Copy Job",
createCopyJobButtonAriaLabel: "Create a new container copy job",
// No Copy Jobs Found
noCopyJobsTitle: "No copy jobs to show",
createCopyJobButtonText: "Create a container copy job",
// Create Copy Job Panel
createCopyJobPanelTitle: "Copy container",
// Select Account Screen
selectAccountDescription: "Please select a source account from which to copy.",
subscriptionDropdownLabel: "Subscription",
subscriptionDropdownPlaceholder: "Select a subscription",
sourceAccountDropdownLabel: "Account",
sourceAccountDropdownPlaceholder: "Select an account",
migrationTypeCheckboxLabel: "Copy container in offline mode",
// Select Source and Target Containers Screen
selectSourceAndTargetContainersDescription: "Please select a source container and a destination container to copy to.",
sourceContainerSubHeading: "Source container",
targetContainerSubHeading: "Destination container",
databaseDropdownLabel: "Database",
databaseDropdownPlaceholder: "Select a database",
containerDropdownLabel: "Container",
containerDropdownPlaceholder: "Select a container",
// Preview and Create Screen
jobNameLabel: "Job name",
sourceSubscriptionLabel: "Source subscription",
sourceAccountLabel: "Source account",
sourceDatabaseLabel: "Source database",
sourceContainerLabel: "Source container",
targetDatabaseLabel: "Destination database",
targetContainerLabel: "Destination container",
}

View File

@@ -0,0 +1,61 @@
import React from "react";
import { userContext } from "UserContext";
import { useAADAuth } from "../../../hooks/useAADAuth";
import { useConfig } from "../../../hooks/useConfig";
import { CopyJobContextProviderType, CopyJobContextState, CopyJobFlowType } from "../Types";
export const CopyJobContext = React.createContext<CopyJobContextProviderType>(null);
export const useCopyJobContext = (): CopyJobContextProviderType => {
const context = React.useContext(CopyJobContext);
if (!context) {
throw new Error("useCopyJobContext must be used within a CopyJobContextProvider");
}
return context;
}
interface CopyJobContextProviderProps {
children: React.ReactNode;
}
const getInitialCopyJobState = (): CopyJobContextState => {
return {
jobName: "",
migrationType: "offline",
source: {
subscription: null,
account: null,
databaseId: "",
containerId: "",
},
target: {
subscriptionId: userContext.subscriptionId || "",
account: userContext.databaseAccount || null,
databaseId: "",
containerId: "",
},
}
}
const CopyJobContextProvider: React.FC<CopyJobContextProviderProps> = (props) => {
const config = useConfig();
const { isLoggedIn, armToken } = useAADAuth(config);
const [copyJobState, setCopyJobState] = React.useState<CopyJobContextState>(getInitialCopyJobState());
const [flow, setFlow] = React.useState<CopyJobFlowType | null>(null);
if (!isLoggedIn || !armToken) {
// Add a shimmer or loader here
return null;
}
const resetCopyJobState = () => {
setCopyJobState(getInitialCopyJobState());
}
return (
<CopyJobContext.Provider value={{ armToken, copyJobState, setCopyJobState, flow, setFlow, resetCopyJobState }}>
{props.children}
</CopyJobContext.Provider>
);
}
export default CopyJobContextProvider;

View File

@@ -0,0 +1,27 @@
import { Stack } from "@fluentui/react";
import React from "react";
interface FieldRowProps {
label?: string;
children: React.ReactNode;
labelClassName?: string;
}
const FieldRow: React.FC<FieldRowProps> = ({ label = "", children, labelClassName = "" }) => {
return (
<Stack horizontal horizontalAlign="space-between" className="flex-row">
{
label && (
<Stack.Item align="center" className="flex-fixed-width">
<label className={`field-label ${labelClassName}`}>{label}: </label>
</Stack.Item>
)
}
<Stack.Item align="center" className="flex-grow-col">
{children}
</Stack.Item>
</Stack>
);
};
export default FieldRow;

View File

@@ -0,0 +1,28 @@
import { DefaultButton, PrimaryButton, Stack } from "@fluentui/react";
import React from "react";
type NavigationControlsProps = {
primaryBtnText: string;
onPrimary: () => void;
onPrevious: () => void;
onCancel: () => void;
isPrimaryDisabled: boolean;
isPreviousDisabled: boolean;
};
const NavigationControls: React.FC<NavigationControlsProps> = ({
primaryBtnText,
onPrimary,
onPrevious,
onCancel,
isPrimaryDisabled,
isPreviousDisabled,
}) => (
<Stack horizontal tokens={{ childrenGap: 20 }}>
<PrimaryButton text={primaryBtnText} onClick={onPrimary} allowDisabledFocus disabled={isPrimaryDisabled} />
<DefaultButton text="Previous" onClick={onPrevious} allowDisabledFocus disabled={isPreviousDisabled} />
<DefaultButton text="Cancel" onClick={onCancel} />
</Stack>
);
export default React.memo(NavigationControls);

View File

@@ -0,0 +1,36 @@
import { Stack } from "@fluentui/react";
import React from "react";
import { useCopyJobContext } from "../../Context/CopyJobContext";
import { useCopyJobNavigation } from "../Utils/useCopyJobNavigation";
import NavigationControls from "./Components/NavigationControls";
const CreateCopyJobScreens: React.FC = () => {
const { copyJobState } = useCopyJobContext();
const {
currentScreen,
isPrimaryDisabled,
isPreviousDisabled,
handlePrimary,
handlePrevious,
handleCancel,
primaryBtnText
} = useCopyJobNavigation(copyJobState);
return (
<Stack verticalAlign="space-between" className="createCopyJobScreensContainer">
<Stack.Item className="createCopyJobScreensContent">{currentScreen?.component}</Stack.Item>
<Stack.Item className="createCopyJobScreensFooter">
<NavigationControls
primaryBtnText={primaryBtnText}
onPrimary={handlePrimary}
onPrevious={handlePrevious}
onCancel={handleCancel}
isPrimaryDisabled={isPrimaryDisabled}
isPreviousDisabled={isPreviousDisabled}
/>
</Stack.Item>
</Stack>
);
};
export default CreateCopyJobScreens;

View File

@@ -0,0 +1,13 @@
import React from "react";
import CopyJobContextProvider from "../../Context/CopyJobContext";
import CreateCopyJobScreens from "./CreateCopyJobScreens";
const CreateCopyJobScreensProvider = () => {
return (
<CopyJobContextProvider>
<CreateCopyJobScreens />
</CopyJobContextProvider>
);
};
export default CreateCopyJobScreensProvider;

View File

@@ -0,0 +1,31 @@
import { IColumn } from "@fluentui/react";
import ContainerCopyMessages from "../../../../ContainerCopyMessages";
export const getPreviewCopyJobDetailsListColumns = function (): IColumn[] {
return [
{
key: 'sourcedbname',
name: ContainerCopyMessages.sourceDatabaseLabel,
fieldName: 'sourceDatabaseName',
minWidth: 100
},
{
key: 'sourcecolname',
name: ContainerCopyMessages.sourceContainerLabel,
fieldName: 'sourceContainerName',
minWidth: 100
},
{
key: 'targetdbname',
name: ContainerCopyMessages.targetDatabaseLabel,
fieldName: 'targetDatabaseName',
minWidth: 100
},
{
key: 'targetcolname',
name: ContainerCopyMessages.targetContainerLabel,
fieldName: 'targetContainerName',
minWidth: 100
}
];
};

View File

@@ -0,0 +1,53 @@
import { DetailsList, DetailsListLayoutMode, Stack, Text, TextField } from '@fluentui/react';
import FieldRow from 'Explorer/ContainerCopy/CreateCopyJob/Screens/Components/FieldRow';
import React from 'react';
import ContainerCopyMessages from '../../../ContainerCopyMessages';
import { useCopyJobContext } from '../../../Context/CopyJobContext';
import { getPreviewCopyJobDetailsListColumns } from './Utils/PreviewCopyJobUtils';
const PreviewCopyJob: React.FC = () => {
const { copyJobState, setCopyJobState } = useCopyJobContext();
const selectedDatabaseAndContainers = [{
sourceDatabaseName: copyJobState.source?.databaseId,
sourceContainerName: copyJobState.source?.containerId,
targetDatabaseName: copyJobState.target?.databaseId,
targetContainerName: copyJobState.target?.containerId,
}];
const jobName = copyJobState.jobName;
const onJobNameChange = (_ev?: React.FormEvent, newValue?: string) => {
setCopyJobState((prevState) => ({
...prevState,
jobName: newValue || '',
}));
};
return (
<Stack tokens={{ childrenGap: 20 }} className="previewCopyJobContainer">
<FieldRow label={ContainerCopyMessages.jobNameLabel}>
<TextField
value={jobName}
onChange={onJobNameChange}
/>
</FieldRow>
<Stack>
<Text className='bold'>{ContainerCopyMessages.sourceSubscriptionLabel}</Text>
<Text>{copyJobState.source?.subscription?.displayName}</Text>
</Stack>
<Stack>
<Text className='bold'>{ContainerCopyMessages.sourceAccountLabel}</Text>
<Text>{copyJobState.source?.account?.name}</Text>
</Stack>
<Stack>
<DetailsList
items={selectedDatabaseAndContainers}
layoutMode={DetailsListLayoutMode.justified}
checkboxVisibility={2}
columns={getPreviewCopyJobDetailsListColumns()}
/>
</Stack>
</Stack>
);
};
export default PreviewCopyJob;

View File

@@ -0,0 +1,28 @@
import { Dropdown } from "@fluentui/react";
import React from "react";
import ContainerCopyMessages from "../../../../ContainerCopyMessages";
import { DropdownOptionType } from "../../../../Types";
import FieldRow from "../../Components/FieldRow";
interface AccountDropdownProps {
options: DropdownOptionType[];
selectedKey?: string;
disabled: boolean;
onChange: (_ev?: React.FormEvent, option?: DropdownOptionType) => void;
}
export const AccountDropdown: React.FC<AccountDropdownProps> = React.memo(
({ options, selectedKey, disabled, onChange }) => (
<FieldRow label={ContainerCopyMessages.sourceAccountDropdownLabel}>
<Dropdown
placeholder={ContainerCopyMessages.sourceAccountDropdownPlaceholder}
ariaLabel={ContainerCopyMessages.sourceAccountDropdownLabel}
options={options}
disabled={disabled}
required
selectedKey={selectedKey}
onChange={onChange}
/>
</FieldRow>
)
);

View File

@@ -0,0 +1,20 @@
import { Checkbox, Stack } from "@fluentui/react";
import React from "react";
import ContainerCopyMessages from "../../../../ContainerCopyMessages";
interface MigrationTypeCheckboxProps {
checked: boolean;
onChange: (_ev?: React.FormEvent, checked?: boolean) => void;
}
export const MigrationTypeCheckbox: React.FC<MigrationTypeCheckboxProps> = React.memo(
({ checked, onChange }) => (
<Stack horizontal horizontalAlign="space-between" className="migrationTypeRow">
<Checkbox
label={ContainerCopyMessages.migrationTypeCheckboxLabel}
checked={checked}
onChange={onChange}
/>
</Stack>
)
);

View File

@@ -0,0 +1,26 @@
import { Dropdown } from "@fluentui/react";
import React from "react";
import ContainerCopyMessages from "../../../../ContainerCopyMessages";
import { DropdownOptionType } from "../../../../Types";
import FieldRow from "../../Components/FieldRow";
interface SubscriptionDropdownProps {
options: DropdownOptionType[];
selectedKey?: string;
onChange: (_ev?: React.FormEvent, option?: DropdownOptionType) => void;
}
export const SubscriptionDropdown: React.FC<SubscriptionDropdownProps> = React.memo(
({ options, selectedKey, onChange }) => (
<FieldRow label={ContainerCopyMessages.subscriptionDropdownLabel}>
<Dropdown
placeholder={ContainerCopyMessages.subscriptionDropdownPlaceholder}
ariaLabel={ContainerCopyMessages.subscriptionDropdownLabel}
options={options}
required
selectedKey={selectedKey}
onChange={onChange}
/>
</FieldRow>
)
);

View File

@@ -0,0 +1,77 @@
import React from "react";
import { DatabaseAccount, Subscription } from "../../../../../../Contracts/DataModels";
import { CopyJobContextProviderType, CopyJobContextState, DropdownOptionType } from "../../../../Types";
export function useDropdownOptions(
subscriptions: Subscription[],
accounts: DatabaseAccount[]
): {
subscriptionOptions: DropdownOptionType[],
accountOptions: DropdownOptionType[]
} {
const subscriptionOptions = React.useMemo(
() =>
subscriptions?.map((sub) => ({
key: sub.subscriptionId,
text: sub.displayName,
data: sub,
})) || [],
[subscriptions]
);
const accountOptions = React.useMemo(
() =>
accounts?.map((account) => ({
key: account.id,
text: account.name,
data: account,
})) || [],
[accounts]
);
return { subscriptionOptions, accountOptions };
}
type setCopyJobStateType = CopyJobContextProviderType["setCopyJobState"];
export function useEventHandlers(setCopyJobState: setCopyJobStateType) {
const handleSelectSourceAccount = React.useCallback(
(type: "subscription" | "account", data: Subscription & DatabaseAccount | undefined) => {
setCopyJobState((prevState: CopyJobContextState) => {
if (type === "subscription") {
return {
...prevState,
source: {
...prevState.source,
subscription: data || null,
account: null, // reset on subscription change
},
};
}
if (type === "account") {
return {
...prevState,
source: {
...prevState.source,
account: data || null,
},
};
}
return prevState;
});
},
[setCopyJobState]
);
const handleMigrationTypeChange = React.useCallback(
(_ev?: React.FormEvent<HTMLElement>, checked?: boolean) => {
setCopyJobState((prevState: CopyJobContextState) => ({
...prevState,
migrationType: checked ? "offline" : "online",
}));
},
[setCopyJobState]
);
return { handleSelectSourceAccount, handleMigrationTypeChange };
}

View File

@@ -0,0 +1,54 @@
import { Stack } from "@fluentui/react";
import React from "react";
import { DatabaseAccount, Subscription } from "../../../../../Contracts/DataModels";
import { useDatabaseAccounts } from "../../../../../hooks/useDatabaseAccounts";
import { useSubscriptions } from "../../../../../hooks/useSubscriptions";
import ContainerCopyMessages from "../../../ContainerCopyMessages";
import { useCopyJobContext } from "../../../Context/CopyJobContext";
import { AccountDropdown } from "./Components/AccountDropdown";
import { MigrationTypeCheckbox } from "./Components/MigrationTypeCheckbox";
import { SubscriptionDropdown } from "./Components/SubscriptionDropdown";
import { useDropdownOptions, useEventHandlers } from "./Utils/selectAccountUtils";
interface SelectAccountProps { }
const SelectAccount = React.memo(
(_props: SelectAccountProps) => {
const { armToken, copyJobState, setCopyJobState } = useCopyJobContext();
const selectedSubscriptionId = copyJobState?.source?.subscription?.subscriptionId;
const subscriptions: Subscription[] = useSubscriptions(armToken);
const accounts: DatabaseAccount[] = useDatabaseAccounts(selectedSubscriptionId, armToken);
const { subscriptionOptions, accountOptions } = useDropdownOptions(subscriptions, accounts);
const { handleSelectSourceAccount, handleMigrationTypeChange } = useEventHandlers(setCopyJobState);
const migrationTypeChecked = copyJobState?.migrationType === "offline";
return (
<Stack className="selectAccountContainer" tokens={{ childrenGap: 15 }}>
<span>{ContainerCopyMessages.selectAccountDescription}</span>
<SubscriptionDropdown
options={subscriptionOptions}
selectedKey={selectedSubscriptionId}
onChange={(_ev, option) => handleSelectSourceAccount("subscription", option?.data)}
/>
<AccountDropdown
options={accountOptions}
selectedKey={copyJobState?.source?.account?.id}
disabled={!selectedSubscriptionId}
onChange={(_ev, option) => handleSelectSourceAccount("account", option?.data)}
/>
<MigrationTypeCheckbox
checked={migrationTypeChecked}
onChange={handleMigrationTypeChange}
/>
</Stack>
);
}
);
export default SelectAccount;

View File

@@ -0,0 +1,36 @@
import { CopyJobContextState, DropdownOptionType } from "../../../../Types";
export function dropDownChangeHandler(
setCopyJobState: React.Dispatch<React.SetStateAction<CopyJobContextState>>
) {
return (type: "sourceDatabase" | "sourceContainer" | "targetDatabase" | "targetContainer") =>
(_evnt: any, option: DropdownOptionType) => {
const value = option.key;
setCopyJobState((prevState) => {
switch (type) {
case "sourceDatabase":
return {
...prevState,
source: { ...prevState.source, databaseId: value, containerId: undefined }
};
case "sourceContainer":
return {
...prevState,
source: { ...prevState.source, containerId: value }
};
case "targetDatabase":
return {
...prevState,
target: { ...prevState.target, databaseId: value, containerId: undefined }
};
case "targetContainer":
return {
...prevState,
target: { ...prevState.target, containerId: value }
};
default:
return prevState;
}
});
}
}

View File

@@ -0,0 +1,43 @@
import { Dropdown, Stack } from "@fluentui/react";
import React from "react";
import ContainerCopyMessages from "../../../../ContainerCopyMessages";
import { DatabaseContainerSectionProps } from "../../../../Types";
import FieldRow from "../../Components/FieldRow";
export const DatabaseContainerSection = ({
heading,
databaseOptions,
selectedDatabase,
databaseDisabled,
databaseOnChange,
containerOptions,
selectedContainer,
containerDisabled,
containerOnChange
}: DatabaseContainerSectionProps) => (
<Stack tokens={{ childrenGap: 15 }} className="databaseContainerSection">
<label className="subHeading">{heading}</label>
<FieldRow label={ContainerCopyMessages.databaseDropdownLabel}>
<Dropdown
placeholder={ContainerCopyMessages.databaseDropdownPlaceholder}
ariaLabel={ContainerCopyMessages.databaseDropdownLabel}
options={databaseOptions}
required
disabled={!!databaseDisabled}
selectedKey={selectedDatabase}
onChange={databaseOnChange}
/>
</FieldRow>
<FieldRow label={ContainerCopyMessages.containerDropdownLabel}>
<Dropdown
placeholder={ContainerCopyMessages.containerDropdownPlaceholder}
ariaLabel={ContainerCopyMessages.containerDropdownLabel}
options={containerOptions}
required
disabled={!!containerDisabled}
selectedKey={selectedContainer}
onChange={containerOnChange}
/>
</FieldRow>
</Stack>
);

View File

@@ -0,0 +1,80 @@
import { Stack } from "@fluentui/react";
import React from "react";
import { useDatabases } from "../../../../../hooks/useDatabases";
import { useDataContainers } from "../../../../../hooks/useDataContainers";
import ContainerCopyMessages from "../../../ContainerCopyMessages";
import { useCopyJobContext } from "../../../Context/CopyJobContext";
import { DatabaseContainerSection } from "./components/DatabaseContainerSection";
import { dropDownChangeHandler } from "./Events/DropDownChangeHandler";
import { useMemoizedSourceAndTargetData } from "./memoizedData";
interface SelectSourceAndTargetContainersProps { }
const SelectSourceAndTargetContainers = (_props: SelectSourceAndTargetContainersProps) => {
const { armToken, copyJobState, setCopyJobState } = useCopyJobContext();
const {
source,
target,
sourceDbParams,
sourceContainerParams,
targetDbParams,
targetContainerParams
} = useMemoizedSourceAndTargetData(copyJobState, armToken);
// Custom hooks
const sourceDatabases = useDatabases(...sourceDbParams) || [];
const sourceContainers = useDataContainers(...sourceContainerParams) || [];
const targetDatabases = useDatabases(...targetDbParams) || [];
const targetContainers = useDataContainers(...targetContainerParams) || [];
// Memoize option objects for dropdowns
const sourceDatabaseOptions = React.useMemo(
() => sourceDatabases.map((db: any) => ({ key: db.name, text: db.name, data: db })),
[sourceDatabases]
);
const sourceContainerOptions = React.useMemo(
() => sourceContainers.map((c: any) => ({ key: c.name, text: c.name, data: c })),
[sourceContainers]
);
const targetDatabaseOptions = React.useMemo(
() => targetDatabases.map((db: any) => ({ key: db.name, text: db.name, data: db })),
[targetDatabases]
);
const targetContainerOptions = React.useMemo(
() => targetContainers.map((c: any) => ({ key: c.name, text: c.name, data: c })),
[targetContainers]
);
const onDropdownChange = React.useCallback(dropDownChangeHandler(setCopyJobState), [setCopyJobState]);
return (
<Stack className="selectSourceAndTargetContainers" tokens={{ childrenGap: 25 }}>
<span>{ContainerCopyMessages.selectSourceAndTargetContainersDescription}</span>
<DatabaseContainerSection
heading={ContainerCopyMessages.sourceContainerSubHeading}
databaseOptions={sourceDatabaseOptions}
selectedDatabase={source?.databaseId}
databaseDisabled={false}
databaseOnChange={onDropdownChange("sourceDatabase")}
containerOptions={sourceContainerOptions}
selectedContainer={source?.containerId}
containerDisabled={!source?.databaseId}
containerOnChange={onDropdownChange("sourceContainer")}
/>
<DatabaseContainerSection
heading={ContainerCopyMessages.targetContainerSubHeading}
databaseOptions={targetDatabaseOptions}
selectedDatabase={target?.databaseId}
databaseDisabled={false}
databaseOnChange={onDropdownChange("targetDatabase")}
containerOptions={targetContainerOptions}
selectedContainer={target?.containerId}
containerDisabled={!target?.databaseId}
containerOnChange={onDropdownChange("targetContainer")}
/>
</Stack>
);
};
export default SelectSourceAndTargetContainers;

View File

@@ -0,0 +1,58 @@
import React from "react";
import { CopyJobContextState, DatabaseParams, DataContainerParams } from "../../../Types";
export function useMemoizedSourceAndTargetData(copyJobState: CopyJobContextState, armToken: string) {
const { source, target } = copyJobState ?? {};
const selectedSourceAccount = source?.account;
const selectedTargetAccount = target?.account;
const sourceDbParams = React.useMemo(
() =>
[
armToken,
source?.subscription?.subscriptionId,
selectedSourceAccount?.resourceGroup,
selectedSourceAccount?.name,
'SQL',
] as DatabaseParams,
[armToken, source?.subscription?.subscriptionId, selectedSourceAccount]
);
const sourceContainerParams = React.useMemo(
() =>
[
armToken,
source?.subscription?.subscriptionId,
selectedSourceAccount?.resourceGroup,
selectedSourceAccount?.name,
source?.databaseId,
'SQL',
] as DataContainerParams,
[armToken, source?.subscription?.subscriptionId, selectedSourceAccount, source?.databaseId]
);
const targetDbParams = React.useMemo(
() => [
armToken,
target?.subscriptionId,
selectedTargetAccount?.resourceGroup,
selectedTargetAccount?.name,
'SQL',
] as DatabaseParams,
[armToken, target?.subscriptionId, selectedTargetAccount]
);
const targetContainerParams = React.useMemo(
() => [
armToken,
target?.subscriptionId,
selectedTargetAccount?.resourceGroup,
selectedTargetAccount?.name,
target?.databaseId,
'SQL',
] as DataContainerParams,
[armToken, target?.subscriptionId, selectedTargetAccount, target?.databaseId]
);
return { source, target, sourceDbParams, sourceContainerParams, targetDbParams, targetContainerParams };
}

View File

@@ -0,0 +1,85 @@
import { submitCreateCopyJob } from "Explorer/ContainerCopy/Actions/CopyJobActions";
import { useCallback, useMemo, useReducer } from "react";
import { useSidePanel } from "../../../../hooks/useSidePanel";
import { CopyJobContextState } from "../../Types";
import { SCREEN_KEYS, useCreateCopyJobScreensList } from "./useCreateCopyJobScreensList";
type NavigationState = {
screenHistory: string[];
};
type Action =
| { type: "NEXT"; nextScreen: string }
| { type: "PREVIOUS" }
| { type: "RESET" };
function navigationReducer(state: NavigationState, action: Action): NavigationState {
switch (action.type) {
case "NEXT":
return {
screenHistory: [...state.screenHistory, action.nextScreen],
};
case "PREVIOUS":
return {
screenHistory: state.screenHistory.length > 1 ? state.screenHistory.slice(0, -1) : state.screenHistory,
};
case "RESET":
return {
screenHistory: [SCREEN_KEYS.SelectAccount],
};
default:
return state;
}
}
export function useCopyJobNavigation(copyJobState: CopyJobContextState) {
const screens = useCreateCopyJobScreensList();
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 primaryBtnText = useMemo(() => {
if (currentScreenKey === SCREEN_KEYS.PreviewCopyJob) {
return "Copy";
}
return "Next";
}, [currentScreenKey]);
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) {
submitCreateCopyJob(copyJobState);
}
}, [currentScreenKey, copyJobState]);
const handlePrevious = useCallback(() => {
dispatch({ type: "PREVIOUS" });
}, []);
const handleCancel = useCallback(() => {
dispatch({ type: "RESET" });
useSidePanel.getState().closeSidePanel();
}, []);
return {
currentScreen,
isPrimaryDisabled,
isPreviousDisabled,
handlePrimary,
handlePrevious,
handleCancel,
primaryBtnText,
};
}

View File

@@ -0,0 +1,60 @@
import React from "react";
import { CopyJobContextState } from "../../Types";
import PreviewCopyJob from "../Screens/PreviewCopyJob";
import SelectAccount from "../Screens/SelectAccount";
import SelectSourceAndTargetContainers from "../Screens/SelectSourceAndTargetContainers";
const SCREEN_KEYS = {
SelectAccount: "SelectAccount",
SelectSourceAndTargetContainers: "SelectSourceAndTargetContainers",
PreviewCopyJob: "PreviewCopyJob",
};
type Validation = {
validate: (state: CopyJobContextState) => boolean;
message: string;
};
type Screen = {
key: string;
component: React.ReactElement;
validations: Validation[];
};
function useCreateCopyJobScreensList() {
return React.useMemo<Screen[]>(
() => [
{
key: SCREEN_KEYS.SelectAccount,
component: <SelectAccount />,
validations: [
{
validate: (state) => !!state?.source?.subscription && !!state?.source?.account,
message: "Please select a subscription and account to proceed",
},
],
},
{
key: SCREEN_KEYS.SelectSourceAndTargetContainers,
component: <SelectSourceAndTargetContainers />,
validations: [
{
validate: (state) => (
!!state?.source?.databaseId && !!state?.source?.containerId && !!state?.target?.databaseId && !!state?.target?.containerId
),
message: "Please select source and target containers to proceed",
},
],
},
{
key: SCREEN_KEYS.PreviewCopyJob,
component: <PreviewCopyJob />,
validations: [],
},
],
[]
);
}
export { SCREEN_KEYS, useCreateCopyJobScreensList };

View File

@@ -0,0 +1,23 @@
import { ActionButton, Image } from '@fluentui/react';
import React, { useCallback } from 'react';
import CopyJobIcon from "../../../../../images/ContainerCopy/copy-jobs.svg";
import * as Actions from "../../Actions/CopyJobActions";
import ContainerCopyMessages from '../../ContainerCopyMessages';
interface CopyJobsNotFoundProps { }
const CopyJobsNotFound: React.FC<CopyJobsNotFoundProps> = () => {
const handleCreateCopyJob = useCallback(Actions.openCreateCopyJobPanel, []);
return (
<div className='notFoundContainer flexContainer centerContent'>
<Image src={CopyJobIcon} alt={ContainerCopyMessages.noCopyJobsTitle} width={100} height={100} />
<h4 className='noCopyJobsMessage'>{ContainerCopyMessages.noCopyJobsTitle}</h4>
<ActionButton allowDisabledFocus className='createCopyJobButton' onClick={handleCreateCopyJob}>
{ContainerCopyMessages.createCopyJobButtonText}
</ActionButton>
</div>
);
}
export default CopyJobsNotFound;

View File

@@ -0,0 +1,14 @@
import React from 'react';
import CopyJobsNotFound from '../MonitorCopyJobs/Components/CopyJobs.NotFound';
interface MonitorCopyJobsProps { }
const MonitorCopyJobs: React.FC<MonitorCopyJobsProps> = () => {
return (
<div className='monitorCopyJobs flexContainer'>
<CopyJobsNotFound />
</div>
);
}
export default MonitorCopyJobs;

View File

@@ -0,0 +1,99 @@
import { DatabaseAccount, Subscription } from "Contracts/DataModels";
import React from "react";
import { ApiType } from "UserContext";
import Explorer from "../../Explorer";
export interface ContainerCopyProps {
container: Explorer;
}
export type CopyJobCommandBarBtnType = {
key: string;
iconSrc: string;
label: string;
ariaLabel: string;
disabled?: boolean;
onClick: () => void;
};
export type CopyJobTabForwardRefHandle = {
validate: (state: CopyJobContextState) => boolean;
};
export type DropdownOptionType = {
key: string,
text: string,
data: any
};
export type FetchDatabasesListParams = {
armToken: string;
subscriptionId: string;
resourceGroupName: string;
accountName: string;
apiType?: ApiType;
};
export interface FetchDataContainersListParams extends FetchDatabasesListParams {
databaseName: string;
}
export type DatabaseParams = [
string,
string | undefined,
string | undefined,
string | undefined,
ApiType
];
export type DataContainerParams = [
string,
string | undefined,
string | undefined,
string | undefined,
string | undefined,
ApiType
];
export interface DatabaseContainerSectionProps {
heading: string,
databaseOptions: DropdownOptionType[],
selectedDatabase: string,
databaseDisabled?: boolean,
databaseOnChange: (ev: any, option: DropdownOptionType) => void,
containerOptions: DropdownOptionType[],
selectedContainer: string,
containerDisabled?: boolean,
containerOnChange: (ev: any, option: DropdownOptionType) => void
}
export interface CopyJobContextState {
jobName: string;
migrationType: "online" | "offline";
// source details
source: {
subscription: Subscription;
account: DatabaseAccount;
databaseId: string;
containerId: string;
},
// target details
target: {
subscriptionId: string;
account: DatabaseAccount;
databaseId: string;
containerId: string;
},
}
export interface CopyJobFlowType {
currentScreen: string;
}
export interface CopyJobContextProviderType {
armToken: string;
flow: CopyJobFlowType;
setFlow: React.Dispatch<React.SetStateAction<CopyJobFlowType>>;
copyJobState: CopyJobContextState | null;
setCopyJobState: React.Dispatch<React.SetStateAction<CopyJobContextState>>;
resetCopyJobState: () => void;
}

View File

@@ -0,0 +1,49 @@
@import "../../../less/Common/Constants.less";
#containerCopyWrapper {
.centerContent {
justify-content: center;
align-items: center;
}
.notFoundContainer {
.noCopyJobsMessage {
font-weight: 600;
margin: 0 auto;
color: @FocusColor;
}
button.createCopyJobButton {
color: @LinkColor;
}
}
}
.createCopyJobScreensContainer {
height: 100%;
padding: 1em 1.5em;
.bold {
font-weight: 600;
}
label.field-label {
padding: 0;
}
.flex-row {
display: flex;
flex-direction: row;
label.field-label {
font-weight: 600;
}
.flex-fixed-width {
flex: 0 0 auto;
width: 150px;
}
.flex-grow-col {
flex: 1 1 auto;
}
}
.databaseContainerSection {
label.subHeading {
font: inherit;
padding: unset;
font-weight: 600;
}
}
}

View File

@@ -0,0 +1,19 @@
import React, { Suspense } from 'react';
import CopyJobCommandBar from './CommandBar/CopyJobCommandBar';
import { ContainerCopyProps } from './Types';
import './containerCopyStyles.less';
const MonitorCopyJobs = React.lazy(() => import('./MonitorCopyJobs/MonitorCopyJobs'));
const ContainerCopyPanel: React.FC<ContainerCopyProps> = ({ container }) => {
return (
<div id="containerCopyWrapper" className="flexContainer hideOverflows">
<CopyJobCommandBar container={container} />
<Suspense fallback={<div>Loading...</div>}>
<MonitorCopyJobs />
</Suspense>
</div>
);
};
export default ContainerCopyPanel;