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 { 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";
|
||||
}
|
||||
};
|
@ -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;
|
||||
|
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;
|
||||
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}
|
||||
|
51
src/Main.tsx
51
src/Main.tsx
@ -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,30 +78,25 @@ 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">
|
||||
<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>
|
||||
{
|
||||
userContext.features.enableContainerCopy && userContext.apiType === "SQL" ? (
|
||||
<ContainerCopyPanel container={explorer} />
|
||||
) : (
|
||||
<DivExplorer explorer={explorer} />
|
||||
)
|
||||
}
|
||||
|
||||
<SidePanel />
|
||||
<Dialog />
|
||||
{<QuickstartCarousel isOpen={isCarouselOpen} />}
|
||||
@ -113,6 +111,27 @@ const App: React.FunctionComponent = () => {
|
||||
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 */}
|
||||
<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 {
|
||||
return (
|
||||
<div className="splashLoaderContainer">
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
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) {
|
||||
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) {
|
||||
|
@ -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: () => {
|
||||
|
Loading…
x
Reference in New Issue
Block a user