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,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

View File

@ -1,7 +1,7 @@
import { TagNames, WorkloadType } from "Common/Constants";
import { Tags } from "Contracts/DataModels";
import { isFabric } from "Platform/Fabric/FabricUtil";
import { userContext } from "../UserContext";
import { ApiType, userContext } from "../UserContext";
function isVirtualNetworkFilterEnabled() {
return userContext.databaseAccount?.properties?.isVirtualNetworkFilterEnabled;
@ -33,3 +33,33 @@ export function isGlobalSecondaryIndexEnabled(): boolean {
!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";
}
};

View File

@ -101,6 +101,24 @@ export interface Subscription {
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 {
locationPlacementId: string;
quotaId: string;
@ -179,7 +197,7 @@ export interface Database extends Resource {
collections?: Collection[];
}
export interface DocumentId extends Resource {}
export interface DocumentId extends Resource { }
export interface ConflictId extends Resource {
resourceId?: string;

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;

View File

@ -8,6 +8,7 @@ export interface PanelContainerProps {
panelContent?: JSX.Element;
isConsoleExpanded: boolean;
isOpen: boolean;
hasConsole: boolean
isConsoleAnimationFinished?: boolean;
panelWidth?: string;
onRenderNavigationContent?: IRenderFunction<IPanelProps>;
@ -86,6 +87,9 @@ export class PanelContainerComponent extends React.Component<PanelContainerProps
};
private getPanelHeight = (): string => {
if (!this.props.hasConsole) {
return window.innerHeight + "px";
}
const notificationConsole = document.getElementById("explorerNotificationConsole");
if (notificationConsole) {
return window.innerHeight - notificationConsole.clientHeight + "px";
@ -102,9 +106,10 @@ export class PanelContainerComponent extends React.Component<PanelContainerProps
export const SidePanel: React.FC = () => {
const isConsoleExpanded = useNotificationConsole((state) => state.isExpanded);
const isConsoleAnimationFinished = useNotificationConsole((state) => state.consoleAnimationFinished);
const { isOpen, panelContent, panelWidth, headerText } = useSidePanel((state) => {
const { isOpen, hasConsole, panelContent, panelWidth, headerText } = useSidePanel((state) => {
return {
isOpen: state.isOpen,
hasConsole: state.hasConsole,
panelContent: state.panelContent,
headerText: state.headerText,
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
return (
<PanelContainerComponent
hasConsole={hasConsole}
isOpen={isOpen}
panelContent={panelContent}
headerText={headerText}

View File

@ -20,9 +20,12 @@ import "../externals/jquery.typeahead.min.css";
import "../externals/jquery.typeahead.min.js";
// Image Dependencies
import { Platform } from "ConfigContext";
import ContainerCopyPanel from "Explorer/ContainerCopy";
import Explorer from "Explorer/Explorer";
import { QueryCopilotCarousel } from "Explorer/QueryCopilot/CopilotCarousel";
import { SidebarContainer } from "Explorer/Sidebar";
import { KeyboardShortcutRoot } from "KeyboardShortcuts";
import { userContext } from "UserContext";
import "allotment/dist/style.css";
import "../images/CosmosDB_rgb_ui_lighttheme.ico";
import hdeConnectImage from "../images/HdeConnectCosmosDB.svg";
@ -75,14 +78,41 @@ const App: React.FunctionComponent = () => {
}
StyleConstants.updateStyles();
const explorer = useKnockoutExplorer(config?.platform);
// console.log("Using config: ", config);
if (!explorer) {
return <LoadingExplorer />;
}
// console.log("Using explorer: ", explorer);
// console.log("Using userContext: ", userContext);
return (
<KeyboardShortcutRoot>
<div className="flexContainer" aria-hidden="false" data-test="DataExplorerRoot">
{
userContext.features.enableContainerCopy && userContext.apiType === "SQL" ? (
<ContainerCopyPanel container={explorer} />
) : (
<DivExplorer explorer={explorer} />
)
}
<SidePanel />
<Dialog />
{<QuickstartCarousel isOpen={isCarouselOpen} />}
{<SQLQuickstartTutorial />}
{<MongoQuickstartTutorial />}
{<QueryCopilotCarousel isOpen={isCopilotCarouselOpen} explorer={explorer} />}
</div>
</KeyboardShortcutRoot>
);
};
const mainElement = document.getElementById("Main");
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 */}
@ -99,19 +129,8 @@ const App: React.FunctionComponent = () => {
<NotificationConsole />
</div>
</div>
<SidePanel />
<Dialog />
{<QuickstartCarousel isOpen={isCarouselOpen} />}
{<SQLQuickstartTutorial />}
{<MongoQuickstartTutorial />}
{<QueryCopilotCarousel isOpen={isCopilotCarouselOpen} explorer={explorer} />}
</div>
</KeyboardShortcutRoot>
);
};
const mainElement = document.getElementById("Main");
ReactDOM.render(<App />, mainElement);
}
function LoadingExplorer(): JSX.Element {
return (

View File

@ -39,6 +39,7 @@ export type Features = {
readonly copilotChatFixedMonacoEditorHeight: boolean;
readonly enablePriorityBasedExecution: boolean;
readonly disableConnectionStringLogin: boolean;
readonly enableContainerCopy: boolean;
readonly enableCloudShell: boolean;
// 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"),
enablePriorityBasedExecution: "true" === get("enableprioritybasedexecution"),
disableConnectionStringLogin: "true" === get("disableconnectionstringlogin"),
enableContainerCopy: "true" === get("enablecontainercopy"),
enableCloudShell: true,
};
}

View 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;
}

View 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;
}

View File

@ -959,6 +959,13 @@ function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) {
if (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.indexOf(Flights.AutoscaleTest) !== -1) {

View File

@ -3,15 +3,19 @@ import create, { UseStore } from "zustand";
export interface SidePanelState {
isOpen: boolean;
panelWidth: string;
hasConsole: boolean;
panelContent?: JSX.Element;
headerText?: string;
openSidePanel: (headerText: string, panelContent: JSX.Element, panelWidth?: string, onClose?: () => void) => void;
closeSidePanel: () => void;
setPanelHasConsole: (hasConsole: boolean) => void;
getRef?: React.RefObject<HTMLElement>; // Optional ref for focusing the last element.
}
export const useSidePanel: UseStore<SidePanelState> = create((set) => ({
isOpen: false,
panelWidth: "440px",
hasConsole: true,
setPanelHasConsole: (hasConsole: boolean) => set((state) => ({ ...state, hasConsole })),
openSidePanel: (headerText, panelContent, panelWidth = "440px") =>
set((state) => ({ ...state, headerText, panelContent, panelWidth, isOpen: true })),
closeSidePanel: () => {