mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-10-13 15:28:05 +01:00
Initial dev for container copy
This commit is contained in:
parent
d924824536
commit
c83f4fc431
17
images/ContainerCopy/copy-jobs.svg
Normal file
17
images/ContainerCopy/copy-jobs.svg
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<svg width="96" height="104" viewBox="0 0 96 104" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path opacity="0.2" d="M80.5008 81.2203L41.2637 58.2012L35.7705 61.9941L74.6152 84.6208L80.5008 81.2203Z" fill="#AAAAAA"/>
|
||||||
|
<path opacity="0.2" d="M60.2283 92.5992L20.9912 69.5801L15.498 73.373L54.3428 95.9997L60.2283 92.5992Z" fill="#AAAAAA"/>
|
||||||
|
<path d="M63.7596 30.9969L74.8768 37.4057L74.746 82.1359L35.7705 59.7708L35.9013 3.00781L63.7596 19.095V30.9969Z" fill="#C9C9C9"/>
|
||||||
|
<path d="M35.9014 3.00818L41.0022 0L68.8605 16.0872L63.7597 19.0954L35.9014 3.00818Z" fill="#AAAAAA"/>
|
||||||
|
<path d="M74.8769 37.4067L79.9777 34.5293L79.8469 79.2596L74.7461 82.2677L74.8769 37.4067Z" fill="#AAAAAA"/>
|
||||||
|
<path d="M43.4872 42.245L54.6043 48.6537L54.4735 93.384L15.498 71.0188L15.6288 14.2559L43.4872 30.3431V42.245Z" fill="#F4F4F4"/>
|
||||||
|
<path d="M15.6289 14.2562L20.7297 11.248L48.5881 27.3352L43.4872 30.3434L15.6289 14.2562Z" fill="#DCDCDC"/>
|
||||||
|
<path d="M54.6044 48.6547L59.7052 45.7773L59.5745 90.5076L54.4736 93.5158L54.6044 48.6547Z" fill="#DCDCDC"/>
|
||||||
|
<path d="M63.7598 19.0961L68.8606 16.0879L79.9778 34.5293L74.8769 37.4067L63.7598 19.0961Z" fill="#C9C9C9"/>
|
||||||
|
<path d="M63.7598 19.0957L74.8769 37.4063L63.7598 30.9976V19.0957Z" fill="#DCDCDC"/>
|
||||||
|
<path d="M43.4873 30.3441L48.5881 27.3359L59.7053 45.7774L54.6045 48.6548L43.4873 30.3441Z" fill="#F4F4F4"/>
|
||||||
|
<path d="M43.4873 30.3438L54.6045 48.6544L43.4873 42.2457V30.3438Z" fill="#C9C9C9"/>
|
||||||
|
<path d="M46.8751 52.4595V55.9693L23.2275 42.1367V38.627L46.8751 52.4595Z" fill="#C9C9C9"/>
|
||||||
|
<path d="M46.8751 59.0658V62.5756L23.2275 48.6914V45.1816L46.8751 59.0658Z" fill="#C9C9C9"/>
|
||||||
|
<path d="M46.8751 65.3621V68.8719L23.2275 54.9877V51.6328L46.8751 65.3621Z" fill="#C9C9C9"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.7 KiB |
@ -1,7 +1,7 @@
|
|||||||
import { TagNames, WorkloadType } from "Common/Constants";
|
import { TagNames, WorkloadType } from "Common/Constants";
|
||||||
import { Tags } from "Contracts/DataModels";
|
import { Tags } from "Contracts/DataModels";
|
||||||
import { isFabric } from "Platform/Fabric/FabricUtil";
|
import { isFabric } from "Platform/Fabric/FabricUtil";
|
||||||
import { userContext } from "../UserContext";
|
import { ApiType, userContext } from "../UserContext";
|
||||||
|
|
||||||
function isVirtualNetworkFilterEnabled() {
|
function isVirtualNetworkFilterEnabled() {
|
||||||
return userContext.databaseAccount?.properties?.isVirtualNetworkFilterEnabled;
|
return userContext.databaseAccount?.properties?.isVirtualNetworkFilterEnabled;
|
||||||
@ -33,3 +33,33 @@ export function isGlobalSecondaryIndexEnabled(): boolean {
|
|||||||
!isFabric() && userContext.apiType === "SQL" && userContext.databaseAccount?.properties?.enableMaterializedViews
|
!isFabric() && userContext.apiType === "SQL" && userContext.databaseAccount?.properties?.enableMaterializedViews
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getDatabaseEndpoint = (apiType: ApiType): string => {
|
||||||
|
switch (apiType) {
|
||||||
|
case "Mongo":
|
||||||
|
return "mongodbDatabases";
|
||||||
|
case "Cassandra":
|
||||||
|
return "cassandraKeyspaces";
|
||||||
|
case "Gremlin":
|
||||||
|
return "gremlinDatabases";
|
||||||
|
case "Tables":
|
||||||
|
return "tables";
|
||||||
|
default:
|
||||||
|
case "SQL":
|
||||||
|
return "sqlDatabases";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getCollectionEndpoint = (apiType: ApiType): string => {
|
||||||
|
switch (apiType) {
|
||||||
|
case "Mongo":
|
||||||
|
return "collections";
|
||||||
|
case "Cassandra":
|
||||||
|
return "tables";
|
||||||
|
case "Gremlin":
|
||||||
|
return "graphs";
|
||||||
|
default:
|
||||||
|
case "SQL":
|
||||||
|
return "containers";
|
||||||
|
}
|
||||||
|
};
|
@ -101,6 +101,24 @@ export interface Subscription {
|
|||||||
authorizationSource?: string;
|
authorizationSource?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DatabaseModel extends ArmEntity {
|
||||||
|
properties: DatabaseGetProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DatabaseGetProperties {
|
||||||
|
resource: DatabaseResource & ExtendedResourceProperties;
|
||||||
|
}
|
||||||
|
export interface DatabaseResource {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExtendedResourceProperties {
|
||||||
|
readonly _rid?: string;
|
||||||
|
readonly _self?: string;
|
||||||
|
readonly _ts?: number;
|
||||||
|
readonly _etag?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface SubscriptionPolicies {
|
export interface SubscriptionPolicies {
|
||||||
locationPlacementId: string;
|
locationPlacementId: string;
|
||||||
quotaId: string;
|
quotaId: string;
|
||||||
@ -179,7 +197,7 @@ export interface Database extends Resource {
|
|||||||
collections?: Collection[];
|
collections?: Collection[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DocumentId extends Resource {}
|
export interface DocumentId extends Resource { }
|
||||||
|
|
||||||
export interface ConflictId extends Resource {
|
export interface ConflictId extends Resource {
|
||||||
resourceId?: string;
|
resourceId?: string;
|
||||||
|
19
src/Explorer/ContainerCopy/Actions/CopyJobActions.tsx
Normal file
19
src/Explorer/ContainerCopy/Actions/CopyJobActions.tsx
Normal 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);
|
||||||
|
};
|
31
src/Explorer/ContainerCopy/CommandBar/CopyJobCommandBar.tsx
Normal file
31
src/Explorer/ContainerCopy/CommandBar/CopyJobCommandBar.tsx
Normal 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;
|
56
src/Explorer/ContainerCopy/CommandBar/Utils.ts
Normal file
56
src/Explorer/ContainerCopy/CommandBar/Utils.ts
Normal 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);
|
||||||
|
}
|
||||||
|
|
42
src/Explorer/ContainerCopy/ContainerCopyMessages.ts
Normal file
42
src/Explorer/ContainerCopy/ContainerCopyMessages.ts
Normal 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",
|
||||||
|
}
|
61
src/Explorer/ContainerCopy/Context/CopyJobContext.tsx
Normal file
61
src/Explorer/ContainerCopy/Context/CopyJobContext.tsx
Normal 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;
|
@ -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;
|
@ -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);
|
@ -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;
|
@ -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;
|
@ -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
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
@ -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;
|
@ -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>
|
||||||
|
)
|
||||||
|
);
|
@ -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>
|
||||||
|
)
|
||||||
|
);
|
@ -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>
|
||||||
|
)
|
||||||
|
);
|
@ -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 };
|
||||||
|
}
|
@ -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;
|
@ -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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -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>
|
||||||
|
);
|
@ -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;
|
@ -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 };
|
||||||
|
}
|
@ -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,
|
||||||
|
};
|
||||||
|
}
|
@ -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 };
|
||||||
|
|
@ -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;
|
@ -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;
|
99
src/Explorer/ContainerCopy/Types/index.ts
Normal file
99
src/Explorer/ContainerCopy/Types/index.ts
Normal 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;
|
||||||
|
}
|
49
src/Explorer/ContainerCopy/containerCopyStyles.less
Normal file
49
src/Explorer/ContainerCopy/containerCopyStyles.less
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
19
src/Explorer/ContainerCopy/index.tsx
Normal file
19
src/Explorer/ContainerCopy/index.tsx
Normal 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;
|
@ -8,6 +8,7 @@ export interface PanelContainerProps {
|
|||||||
panelContent?: JSX.Element;
|
panelContent?: JSX.Element;
|
||||||
isConsoleExpanded: boolean;
|
isConsoleExpanded: boolean;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
|
hasConsole: boolean
|
||||||
isConsoleAnimationFinished?: boolean;
|
isConsoleAnimationFinished?: boolean;
|
||||||
panelWidth?: string;
|
panelWidth?: string;
|
||||||
onRenderNavigationContent?: IRenderFunction<IPanelProps>;
|
onRenderNavigationContent?: IRenderFunction<IPanelProps>;
|
||||||
@ -86,6 +87,9 @@ export class PanelContainerComponent extends React.Component<PanelContainerProps
|
|||||||
};
|
};
|
||||||
|
|
||||||
private getPanelHeight = (): string => {
|
private getPanelHeight = (): string => {
|
||||||
|
if (!this.props.hasConsole) {
|
||||||
|
return window.innerHeight + "px";
|
||||||
|
}
|
||||||
const notificationConsole = document.getElementById("explorerNotificationConsole");
|
const notificationConsole = document.getElementById("explorerNotificationConsole");
|
||||||
if (notificationConsole) {
|
if (notificationConsole) {
|
||||||
return window.innerHeight - notificationConsole.clientHeight + "px";
|
return window.innerHeight - notificationConsole.clientHeight + "px";
|
||||||
@ -102,9 +106,10 @@ export class PanelContainerComponent extends React.Component<PanelContainerProps
|
|||||||
export const SidePanel: React.FC = () => {
|
export const SidePanel: React.FC = () => {
|
||||||
const isConsoleExpanded = useNotificationConsole((state) => state.isExpanded);
|
const isConsoleExpanded = useNotificationConsole((state) => state.isExpanded);
|
||||||
const isConsoleAnimationFinished = useNotificationConsole((state) => state.consoleAnimationFinished);
|
const isConsoleAnimationFinished = useNotificationConsole((state) => state.consoleAnimationFinished);
|
||||||
const { isOpen, panelContent, panelWidth, headerText } = useSidePanel((state) => {
|
const { isOpen, hasConsole, panelContent, panelWidth, headerText } = useSidePanel((state) => {
|
||||||
return {
|
return {
|
||||||
isOpen: state.isOpen,
|
isOpen: state.isOpen,
|
||||||
|
hasConsole: state.hasConsole,
|
||||||
panelContent: state.panelContent,
|
panelContent: state.panelContent,
|
||||||
headerText: state.headerText,
|
headerText: state.headerText,
|
||||||
panelWidth: state.panelWidth,
|
panelWidth: state.panelWidth,
|
||||||
@ -114,6 +119,7 @@ export const SidePanel: React.FC = () => {
|
|||||||
// This component only exists so we can use hooks and pass them down to a non-functional component
|
// This component only exists so we can use hooks and pass them down to a non-functional component
|
||||||
return (
|
return (
|
||||||
<PanelContainerComponent
|
<PanelContainerComponent
|
||||||
|
hasConsole={hasConsole}
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
panelContent={panelContent}
|
panelContent={panelContent}
|
||||||
headerText={headerText}
|
headerText={headerText}
|
||||||
|
51
src/Main.tsx
51
src/Main.tsx
@ -20,9 +20,12 @@ import "../externals/jquery.typeahead.min.css";
|
|||||||
import "../externals/jquery.typeahead.min.js";
|
import "../externals/jquery.typeahead.min.js";
|
||||||
// Image Dependencies
|
// Image Dependencies
|
||||||
import { Platform } from "ConfigContext";
|
import { Platform } from "ConfigContext";
|
||||||
|
import ContainerCopyPanel from "Explorer/ContainerCopy";
|
||||||
|
import Explorer from "Explorer/Explorer";
|
||||||
import { QueryCopilotCarousel } from "Explorer/QueryCopilot/CopilotCarousel";
|
import { QueryCopilotCarousel } from "Explorer/QueryCopilot/CopilotCarousel";
|
||||||
import { SidebarContainer } from "Explorer/Sidebar";
|
import { SidebarContainer } from "Explorer/Sidebar";
|
||||||
import { KeyboardShortcutRoot } from "KeyboardShortcuts";
|
import { KeyboardShortcutRoot } from "KeyboardShortcuts";
|
||||||
|
import { userContext } from "UserContext";
|
||||||
import "allotment/dist/style.css";
|
import "allotment/dist/style.css";
|
||||||
import "../images/CosmosDB_rgb_ui_lighttheme.ico";
|
import "../images/CosmosDB_rgb_ui_lighttheme.ico";
|
||||||
import hdeConnectImage from "../images/HdeConnectCosmosDB.svg";
|
import hdeConnectImage from "../images/HdeConnectCosmosDB.svg";
|
||||||
@ -75,30 +78,25 @@ const App: React.FunctionComponent = () => {
|
|||||||
}
|
}
|
||||||
StyleConstants.updateStyles();
|
StyleConstants.updateStyles();
|
||||||
const explorer = useKnockoutExplorer(config?.platform);
|
const explorer = useKnockoutExplorer(config?.platform);
|
||||||
|
// console.log("Using config: ", config);
|
||||||
|
|
||||||
if (!explorer) {
|
if (!explorer) {
|
||||||
return <LoadingExplorer />;
|
return <LoadingExplorer />;
|
||||||
}
|
}
|
||||||
|
// console.log("Using explorer: ", explorer);
|
||||||
|
// console.log("Using userContext: ", userContext);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<KeyboardShortcutRoot>
|
<KeyboardShortcutRoot>
|
||||||
<div className="flexContainer" aria-hidden="false" data-test="DataExplorerRoot">
|
<div className="flexContainer" aria-hidden="false" data-test="DataExplorerRoot">
|
||||||
<div id="divExplorer" className="flexContainer hideOverflows">
|
{
|
||||||
<div id="freeTierTeachingBubble"> </div>
|
userContext.features.enableContainerCopy && userContext.apiType === "SQL" ? (
|
||||||
{/* Main Command Bar - Start */}
|
<ContainerCopyPanel container={explorer} />
|
||||||
<CommandBar container={explorer} />
|
) : (
|
||||||
{/* Collections Tree and Tabs - Begin */}
|
<DivExplorer explorer={explorer} />
|
||||||
<SidebarContainer explorer={explorer} />
|
)
|
||||||
{/* Collections Tree and Tabs - End */}
|
}
|
||||||
<div
|
|
||||||
className="dataExplorerErrorConsoleContainer"
|
|
||||||
role="contentinfo"
|
|
||||||
aria-label="Notification console"
|
|
||||||
id="explorerNotificationConsole"
|
|
||||||
>
|
|
||||||
<NotificationConsole />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<SidePanel />
|
<SidePanel />
|
||||||
<Dialog />
|
<Dialog />
|
||||||
{<QuickstartCarousel isOpen={isCarouselOpen} />}
|
{<QuickstartCarousel isOpen={isCarouselOpen} />}
|
||||||
@ -113,6 +111,27 @@ const App: React.FunctionComponent = () => {
|
|||||||
const mainElement = document.getElementById("Main");
|
const mainElement = document.getElementById("Main");
|
||||||
ReactDOM.render(<App />, mainElement);
|
ReactDOM.render(<App />, mainElement);
|
||||||
|
|
||||||
|
function DivExplorer({ explorer }: { explorer: Explorer }): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div id="divExplorer" className="flexContainer hideOverflows">
|
||||||
|
<div id="freeTierTeachingBubble"> </div>
|
||||||
|
{/* Main Command Bar - Start */}
|
||||||
|
<CommandBar container={explorer} />
|
||||||
|
{/* Collections Tree and Tabs - Begin */}
|
||||||
|
<SidebarContainer explorer={explorer} />
|
||||||
|
{/* Collections Tree and Tabs - End */}
|
||||||
|
<div
|
||||||
|
className="dataExplorerErrorConsoleContainer"
|
||||||
|
role="contentinfo"
|
||||||
|
aria-label="Notification console"
|
||||||
|
id="explorerNotificationConsole"
|
||||||
|
>
|
||||||
|
<NotificationConsole />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function LoadingExplorer(): JSX.Element {
|
function LoadingExplorer(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<div className="splashLoaderContainer">
|
<div className="splashLoaderContainer">
|
||||||
|
@ -39,6 +39,7 @@ export type Features = {
|
|||||||
readonly copilotChatFixedMonacoEditorHeight: boolean;
|
readonly copilotChatFixedMonacoEditorHeight: boolean;
|
||||||
readonly enablePriorityBasedExecution: boolean;
|
readonly enablePriorityBasedExecution: boolean;
|
||||||
readonly disableConnectionStringLogin: boolean;
|
readonly disableConnectionStringLogin: boolean;
|
||||||
|
readonly enableContainerCopy: boolean;
|
||||||
readonly enableCloudShell: boolean;
|
readonly enableCloudShell: boolean;
|
||||||
|
|
||||||
// can be set via both flight and feature flag
|
// can be set via both flight and feature flag
|
||||||
@ -111,6 +112,7 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
|
|||||||
copilotChatFixedMonacoEditorHeight: "true" === get("copilotchatfixedmonacoeditorheight"),
|
copilotChatFixedMonacoEditorHeight: "true" === get("copilotchatfixedmonacoeditorheight"),
|
||||||
enablePriorityBasedExecution: "true" === get("enableprioritybasedexecution"),
|
enablePriorityBasedExecution: "true" === get("enableprioritybasedexecution"),
|
||||||
disableConnectionStringLogin: "true" === get("disableconnectionstringlogin"),
|
disableConnectionStringLogin: "true" === get("disableconnectionstringlogin"),
|
||||||
|
enableContainerCopy: "true" === get("enablecontainercopy"),
|
||||||
enableCloudShell: true,
|
enableCloudShell: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
82
src/hooks/useDataContainers.tsx
Normal file
82
src/hooks/useDataContainers.tsx
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import { DatabaseModel } from "Contracts/DataModels";
|
||||||
|
import useSWR from "swr";
|
||||||
|
import { getCollectionEndpoint, getDatabaseEndpoint } from "../Common/DatabaseAccountUtility";
|
||||||
|
import { configContext } from "../ConfigContext";
|
||||||
|
import { FetchDataContainersListParams } from "../Explorer/ContainerCopy/Types";
|
||||||
|
import { ApiType } from "../UserContext";
|
||||||
|
|
||||||
|
const apiVersion = "2023-09-15";
|
||||||
|
|
||||||
|
const buildReadDataContainersListUrl = (params: FetchDataContainersListParams): string => {
|
||||||
|
const { subscriptionId, resourceGroupName, accountName, databaseName, apiType } = params;
|
||||||
|
const databaseEndpoint = getDatabaseEndpoint(apiType);
|
||||||
|
const collectionEndpoint = getCollectionEndpoint(apiType);
|
||||||
|
|
||||||
|
let armEndpoint = configContext.ARM_ENDPOINT;
|
||||||
|
if (armEndpoint.endsWith("/")) {
|
||||||
|
armEndpoint = armEndpoint.slice(0, -1);
|
||||||
|
}
|
||||||
|
return `${armEndpoint}/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/${databaseEndpoint}/${databaseName}/${collectionEndpoint}?api-version=${apiVersion}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchDataContainersList = async (
|
||||||
|
armToken: string,
|
||||||
|
subscriptionId: string,
|
||||||
|
resourceGroupName: string,
|
||||||
|
accountName: string,
|
||||||
|
databaseName: string,
|
||||||
|
apiType: ApiType
|
||||||
|
): Promise<DatabaseModel[]> => {
|
||||||
|
const uri = buildReadDataContainersListUrl({
|
||||||
|
armToken,
|
||||||
|
subscriptionId,
|
||||||
|
resourceGroupName,
|
||||||
|
accountName,
|
||||||
|
databaseName,
|
||||||
|
apiType
|
||||||
|
});
|
||||||
|
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();
|
||||||
|
return data.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useDataContainers(
|
||||||
|
armToken: string,
|
||||||
|
subscriptionId: string,
|
||||||
|
resourceGroupName: string,
|
||||||
|
accountName: string,
|
||||||
|
databaseName: string,
|
||||||
|
apiType: ApiType
|
||||||
|
): DatabaseModel[] | undefined {
|
||||||
|
const { data } = useSWR(
|
||||||
|
() => (
|
||||||
|
armToken && subscriptionId && resourceGroupName && accountName && databaseName && apiType ? [
|
||||||
|
"fetchContainersLinkedToDatabases",
|
||||||
|
armToken, subscriptionId, resourceGroupName, accountName, databaseName, apiType
|
||||||
|
] : undefined
|
||||||
|
),
|
||||||
|
(_, armToken, subscriptionId, resourceGroupName, accountName, databaseName, apiType) => fetchDataContainersList(
|
||||||
|
armToken,
|
||||||
|
subscriptionId,
|
||||||
|
resourceGroupName,
|
||||||
|
accountName,
|
||||||
|
databaseName,
|
||||||
|
apiType
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
64
src/hooks/useDatabases.tsx
Normal file
64
src/hooks/useDatabases.tsx
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import { DatabaseModel } from "Contracts/DataModels";
|
||||||
|
import useSWR from "swr";
|
||||||
|
import { getDatabaseEndpoint } from "../Common/DatabaseAccountUtility";
|
||||||
|
import { configContext } from "../ConfigContext";
|
||||||
|
import { FetchDatabasesListParams } from "../Explorer/ContainerCopy/Types";
|
||||||
|
import { ApiType } from "../UserContext";
|
||||||
|
|
||||||
|
const apiVersion = "2023-09-15";
|
||||||
|
const buildReadDatabasesListUrl = (params: FetchDatabasesListParams): string => {
|
||||||
|
const { subscriptionId, resourceGroupName, accountName, apiType } = params;
|
||||||
|
const databaseEndpoint = getDatabaseEndpoint(apiType);
|
||||||
|
|
||||||
|
let armEndpoint = configContext.ARM_ENDPOINT;
|
||||||
|
if (armEndpoint.endsWith("/")) {
|
||||||
|
armEndpoint = armEndpoint.slice(0, -1);
|
||||||
|
}
|
||||||
|
return `${armEndpoint}/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/${databaseEndpoint}?api-version=${apiVersion}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchDatabasesList = async (armToken: string, subscriptionId: string, resourceGroupName: string, accountName: string, apiType: ApiType): Promise<DatabaseModel[]> => {
|
||||||
|
const uri = buildReadDatabasesListUrl({ armToken, subscriptionId, resourceGroupName, accountName, apiType });
|
||||||
|
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 databases");
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return data.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useDatabases(
|
||||||
|
armToken: string,
|
||||||
|
subscriptionId: string,
|
||||||
|
resourceGroupName: string,
|
||||||
|
accountName: string,
|
||||||
|
apiType: ApiType
|
||||||
|
): DatabaseModel[] | undefined {
|
||||||
|
const { data } = useSWR(
|
||||||
|
() => (
|
||||||
|
armToken && subscriptionId && resourceGroupName && accountName && apiType ? [
|
||||||
|
"fetchDatabasesLinkedToResource",
|
||||||
|
armToken, subscriptionId, resourceGroupName, accountName, apiType
|
||||||
|
] : undefined
|
||||||
|
),
|
||||||
|
(_, armToken, subscriptionId, resourceGroupName, accountName, apiType) => fetchDatabasesList(
|
||||||
|
armToken,
|
||||||
|
subscriptionId,
|
||||||
|
resourceGroupName,
|
||||||
|
accountName,
|
||||||
|
apiType
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
@ -959,6 +959,13 @@ function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) {
|
|||||||
if (inputs.features) {
|
if (inputs.features) {
|
||||||
Object.assign(userContext.features, extractFeatures(new URLSearchParams(inputs.features)));
|
Object.assign(userContext.features, extractFeatures(new URLSearchParams(inputs.features)));
|
||||||
}
|
}
|
||||||
|
// somehow we need to make enableContainerCopy true for platform: portal & apiType: SQL
|
||||||
|
// Ideally we need to pass this as a feature flag from portal or in a query param
|
||||||
|
// Then we will fetch the value from query param and set this to true
|
||||||
|
// For now setting it to true unconditionally
|
||||||
|
if (userContext.apiType === "SQL") {
|
||||||
|
Object.assign(userContext.features, { enableContainerCopy: true });
|
||||||
|
}
|
||||||
|
|
||||||
if (inputs.flights) {
|
if (inputs.flights) {
|
||||||
if (inputs.flights.indexOf(Flights.AutoscaleTest) !== -1) {
|
if (inputs.flights.indexOf(Flights.AutoscaleTest) !== -1) {
|
||||||
|
@ -3,15 +3,19 @@ import create, { UseStore } from "zustand";
|
|||||||
export interface SidePanelState {
|
export interface SidePanelState {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
panelWidth: string;
|
panelWidth: string;
|
||||||
|
hasConsole: boolean;
|
||||||
panelContent?: JSX.Element;
|
panelContent?: JSX.Element;
|
||||||
headerText?: string;
|
headerText?: string;
|
||||||
openSidePanel: (headerText: string, panelContent: JSX.Element, panelWidth?: string, onClose?: () => void) => void;
|
openSidePanel: (headerText: string, panelContent: JSX.Element, panelWidth?: string, onClose?: () => void) => void;
|
||||||
closeSidePanel: () => void;
|
closeSidePanel: () => void;
|
||||||
|
setPanelHasConsole: (hasConsole: boolean) => void;
|
||||||
getRef?: React.RefObject<HTMLElement>; // Optional ref for focusing the last element.
|
getRef?: React.RefObject<HTMLElement>; // Optional ref for focusing the last element.
|
||||||
}
|
}
|
||||||
export const useSidePanel: UseStore<SidePanelState> = create((set) => ({
|
export const useSidePanel: UseStore<SidePanelState> = create((set) => ({
|
||||||
isOpen: false,
|
isOpen: false,
|
||||||
panelWidth: "440px",
|
panelWidth: "440px",
|
||||||
|
hasConsole: true,
|
||||||
|
setPanelHasConsole: (hasConsole: boolean) => set((state) => ({ ...state, hasConsole })),
|
||||||
openSidePanel: (headerText, panelContent, panelWidth = "440px") =>
|
openSidePanel: (headerText, panelContent, panelWidth = "440px") =>
|
||||||
set((state) => ({ ...state, headerText, panelContent, panelWidth, isOpen: true })),
|
set((state) => ({ ...state, headerText, panelContent, panelWidth, isOpen: true })),
|
||||||
closeSidePanel: () => {
|
closeSidePanel: () => {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user