mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-05-21 04:37:28 +01:00
05f834ed57
* Initial change for online partition key change * Refactoring container copy strings so they can be locallized * Missed a file * Fixing some issues found by lint * Fixing errors * Fixing unit tests * Fixing error caused by merging from master * Fixing minor error from merge * Fixing merge error * Addressing comments * Addressing some PR comments * Minor issues * Refixing a formatting issue * Fixed localization error
429 lines
15 KiB
TypeScript
429 lines
15 KiB
TypeScript
import {
|
|
DefaultButton,
|
|
DirectionalHint,
|
|
FontWeights,
|
|
IContextualMenuProps,
|
|
IMessageBarStyles,
|
|
IconButton,
|
|
Link,
|
|
MessageBar,
|
|
MessageBarType,
|
|
PrimaryButton,
|
|
ProgressIndicator,
|
|
Stack,
|
|
Text,
|
|
} from "@fluentui/react";
|
|
import * as React from "react";
|
|
import * as ViewModels from "../../../../Contracts/ViewModels";
|
|
|
|
import { handleError } from "Common/ErrorHandlingUtils";
|
|
import {
|
|
cancelDataTransferJob,
|
|
completeDataTransferJob,
|
|
pauseDataTransferJob,
|
|
pollDataTransferJob,
|
|
resumeDataTransferJob,
|
|
} from "Common/dataAccess/dataTransfers";
|
|
import { Platform, configContext } from "ConfigContext";
|
|
import { CopyJobActions, CopyJobMigrationType } from "Explorer/ContainerCopy/Enums/CopyJobEnums";
|
|
import { useDialog } from "Explorer/Controls/Dialog";
|
|
import Explorer from "Explorer/Explorer";
|
|
import { ChangePartitionKeyPane } from "Explorer/Panes/ChangePartitionKeyPane/ChangePartitionKeyPane";
|
|
import { Keys, t } from "Localization";
|
|
import {
|
|
CosmosSqlDataTransferDataSourceSink,
|
|
DataTransferJobGetResults,
|
|
} from "Utils/arm/generatedClients/dataTransferService/types";
|
|
import { refreshDataTransferJobs, useDataTransferJobs } from "hooks/useDataTransferJobs";
|
|
import { useSidePanel } from "hooks/useSidePanel";
|
|
import { userContext } from "../../../../UserContext";
|
|
|
|
export interface PartitionKeyComponentProps {
|
|
database: ViewModels.Database;
|
|
collection: ViewModels.Collection;
|
|
explorer: Explorer;
|
|
isReadOnly?: boolean; // true: cannot change partition key
|
|
}
|
|
|
|
const darkThemeMessageBarStyles: Partial<IMessageBarStyles> = {
|
|
root: {
|
|
selectors: {
|
|
"&.ms-MessageBar--warning": {
|
|
backgroundColor: "var(--colorStatusWarningBackground1)",
|
|
border: "1px solid var(--colorStatusWarningBorder1)",
|
|
},
|
|
".ms-MessageBar-icon": {
|
|
color: "var(--colorNeutralForeground1)",
|
|
},
|
|
".ms-MessageBar-text": {
|
|
color: "var(--colorNeutralForeground1)",
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({
|
|
database,
|
|
collection,
|
|
explorer,
|
|
isReadOnly,
|
|
}) => {
|
|
const { dataTransferJobs } = useDataTransferJobs();
|
|
const [portalDataTransferJob, setPortalDataTransferJob] = React.useState<DataTransferJobGetResults>(null);
|
|
|
|
React.useEffect(() => {
|
|
if (isReadOnly) {
|
|
return;
|
|
}
|
|
|
|
const loadDataTransferJobs = refreshDataTransferOperations;
|
|
loadDataTransferJobs();
|
|
}, [isReadOnly]);
|
|
|
|
React.useEffect(() => {
|
|
const currentJob = findPortalDataTransferJob();
|
|
setPortalDataTransferJob(currentJob);
|
|
startPollingforUpdate(currentJob);
|
|
}, [dataTransferJobs]);
|
|
|
|
const isHierarchicalPartitionedContainer = (): boolean => collection.partitionKey?.kind === "MultiHash";
|
|
|
|
const getPartitionKeyValue = (): string => {
|
|
return (collection.partitionKeyProperties || []).map((property) => "/" + property).join(", ");
|
|
};
|
|
|
|
const partitionKeyName = t(Keys.controls.settings.partitionKey.partitionKey);
|
|
const partitionKeyValue = getPartitionKeyValue();
|
|
|
|
const textHeadingStyle = {
|
|
root: { fontWeight: FontWeights.semibold, fontSize: 16, color: "var(--colorNeutralForeground1)" },
|
|
};
|
|
|
|
const textSubHeadingStyle = {
|
|
root: { fontWeight: FontWeights.semibold, color: "var(--colorNeutralForeground1)" },
|
|
};
|
|
const textSubHeadingStyle1 = {
|
|
root: { color: "var(--colorNeutralForeground1)" },
|
|
};
|
|
const startPollingforUpdate = async (currentJob: DataTransferJobGetResults) => {
|
|
if (isCurrentJobInProgress(currentJob)) {
|
|
const jobName = currentJob?.properties?.jobName;
|
|
try {
|
|
await pollDataTransferJob(
|
|
jobName,
|
|
userContext.subscriptionId,
|
|
userContext.resourceGroup,
|
|
userContext.databaseAccount.name,
|
|
);
|
|
} catch (error) {
|
|
handleError(error, "ChangePartitionKey", `Failed to complete data transfer job ${jobName}`);
|
|
}
|
|
}
|
|
};
|
|
|
|
const cancelRunningDataTransferJob = async (currentJob: DataTransferJobGetResults) => {
|
|
await cancelDataTransferJob(
|
|
userContext.subscriptionId,
|
|
userContext.resourceGroup,
|
|
userContext.databaseAccount.name,
|
|
currentJob?.properties?.jobName,
|
|
);
|
|
};
|
|
|
|
const pauseRunningDataTransferJob = async (currentJob: DataTransferJobGetResults) => {
|
|
await pauseDataTransferJob(
|
|
userContext.subscriptionId,
|
|
userContext.resourceGroup,
|
|
userContext.databaseAccount.name,
|
|
currentJob?.properties?.jobName,
|
|
);
|
|
};
|
|
|
|
const resumePausedDataTransferJob = async (currentJob: DataTransferJobGetResults) => {
|
|
await resumeDataTransferJob(
|
|
userContext.subscriptionId,
|
|
userContext.resourceGroup,
|
|
userContext.databaseAccount.name,
|
|
currentJob?.properties?.jobName,
|
|
);
|
|
startPollingforUpdate(currentJob);
|
|
};
|
|
|
|
const completeOnlineDataTransferJob = async (currentJob: DataTransferJobGetResults) => {
|
|
await completeDataTransferJob(
|
|
userContext.subscriptionId,
|
|
userContext.resourceGroup,
|
|
userContext.databaseAccount.name,
|
|
currentJob?.properties?.jobName,
|
|
);
|
|
};
|
|
|
|
const isOnlineJob = (currentJob: DataTransferJobGetResults): boolean => {
|
|
const mode = (currentJob?.properties?.mode ?? "").toLowerCase();
|
|
return mode === CopyJobMigrationType.Online;
|
|
};
|
|
|
|
const showActionConfirmationDialog = (
|
|
currentJob: DataTransferJobGetResults,
|
|
action: CopyJobActions,
|
|
onConfirm: () => void,
|
|
): void => {
|
|
const jobName = currentJob?.properties?.jobName;
|
|
const dialogBody =
|
|
action === CopyJobActions.cancel ? (
|
|
<Stack tokens={{ childrenGap: 10 }}>
|
|
<Stack.Item>
|
|
{t(Keys.controls.settings.partitionKeyEditor.confirmCancel1)}
|
|
<br />
|
|
<b>{jobName}</b>
|
|
</Stack.Item>
|
|
<Stack.Item>{t(Keys.controls.settings.partitionKeyEditor.confirmCancel2)}</Stack.Item>
|
|
</Stack>
|
|
) : action === CopyJobActions.complete ? (
|
|
<Stack tokens={{ childrenGap: 10 }}>
|
|
<Stack.Item>
|
|
{t(Keys.controls.settings.partitionKeyEditor.confirmComplete1)}
|
|
<br />
|
|
<b>{jobName}</b>
|
|
</Stack.Item>
|
|
<Stack.Item>{t(Keys.controls.settings.partitionKeyEditor.confrimComplete2)}</Stack.Item>
|
|
</Stack>
|
|
) : null;
|
|
|
|
useDialog
|
|
.getState()
|
|
.showOkCancelModalDialog("", null, t(Keys.common.confirm), onConfirm, t(Keys.common.cancel), null, dialogBody);
|
|
};
|
|
|
|
const getOnlineJobMenuProps = (currentJob: DataTransferJobGetResults): IContextualMenuProps => {
|
|
const jobStatus = currentJob?.properties?.status;
|
|
const isPaused = jobStatus === "Paused";
|
|
|
|
const items: IContextualMenuProps["items"] = [];
|
|
|
|
if (!isPaused) {
|
|
items.push({
|
|
key: CopyJobActions.pause,
|
|
text: t(Keys.containerCopy.monitorJobs.actions.pause),
|
|
iconProps: { iconName: "Pause" },
|
|
onClick: () => {
|
|
pauseRunningDataTransferJob(currentJob);
|
|
},
|
|
});
|
|
}
|
|
|
|
if (isPaused) {
|
|
items.push({
|
|
key: CopyJobActions.resume,
|
|
text: t(Keys.containerCopy.monitorJobs.actions.resume),
|
|
iconProps: { iconName: "Play" },
|
|
onClick: () => {
|
|
resumePausedDataTransferJob(currentJob);
|
|
},
|
|
});
|
|
}
|
|
|
|
items.push({
|
|
key: CopyJobActions.cancel,
|
|
text: t(Keys.common.cancel),
|
|
iconProps: { iconName: "Cancel" },
|
|
onClick: () =>
|
|
showActionConfirmationDialog(currentJob, CopyJobActions.cancel, () => cancelRunningDataTransferJob(currentJob)),
|
|
});
|
|
|
|
items.push({
|
|
key: CopyJobActions.complete,
|
|
text: t(Keys.containerCopy.monitorJobs.actions.complete),
|
|
iconProps: { iconName: "CheckMark" },
|
|
onClick: () =>
|
|
showActionConfirmationDialog(currentJob, CopyJobActions.complete, () =>
|
|
completeOnlineDataTransferJob(currentJob),
|
|
),
|
|
});
|
|
|
|
return {
|
|
items,
|
|
directionalHint: DirectionalHint.leftTopEdge,
|
|
directionalHintFixed: false,
|
|
};
|
|
};
|
|
|
|
const isCurrentJobInProgress = (currentJob: DataTransferJobGetResults) => {
|
|
const jobStatus = currentJob?.properties?.status;
|
|
return (
|
|
jobStatus &&
|
|
jobStatus !== "Completed" &&
|
|
jobStatus !== "Cancelled" &&
|
|
jobStatus !== "Failed" &&
|
|
jobStatus !== "Faulted"
|
|
);
|
|
};
|
|
|
|
const refreshDataTransferOperations = async () => {
|
|
await refreshDataTransferJobs(
|
|
userContext.subscriptionId,
|
|
userContext.resourceGroup,
|
|
userContext.databaseAccount.name,
|
|
);
|
|
};
|
|
|
|
const findPortalDataTransferJob = (): DataTransferJobGetResults => {
|
|
return dataTransferJobs.find((feed: DataTransferJobGetResults) => {
|
|
const sourceSink: CosmosSqlDataTransferDataSourceSink = feed?.properties
|
|
?.source as CosmosSqlDataTransferDataSourceSink;
|
|
return sourceSink.databaseName === collection.databaseId && sourceSink.containerName === collection.id();
|
|
});
|
|
};
|
|
|
|
const getProgressDescription = (): string => {
|
|
const processedCount = portalDataTransferJob?.properties?.processedCount;
|
|
const totalCount = portalDataTransferJob?.properties?.totalCount;
|
|
const processedCountString =
|
|
totalCount > 0
|
|
? t(Keys.controls.settings.partitionKeyEditor.documentsProcessed, {
|
|
processedCount: String(processedCount),
|
|
totalCount: String(totalCount),
|
|
})
|
|
: "";
|
|
return `${portalDataTransferJob?.properties?.status} ${processedCountString}`;
|
|
};
|
|
|
|
const startPartitionkeyChangeWorkflow = () => {
|
|
useSidePanel.getState().openSidePanel(
|
|
t(Keys.controls.settings.partitionKeyEditor.changePartitionKey, {
|
|
partitionKeyName: t(Keys.controls.settings.partitionKey.partitionKey).toLowerCase(),
|
|
}),
|
|
<ChangePartitionKeyPane
|
|
sourceDatabase={database}
|
|
sourceCollection={collection}
|
|
explorer={explorer}
|
|
onClose={refreshDataTransferOperations}
|
|
/>,
|
|
);
|
|
};
|
|
|
|
const getPercentageComplete = () => {
|
|
const jobStatus = portalDataTransferJob?.properties?.status;
|
|
const isCompleted = jobStatus === "Completed";
|
|
if (isCompleted) {
|
|
return 1;
|
|
}
|
|
const processedCount = portalDataTransferJob?.properties?.processedCount;
|
|
const totalCount = portalDataTransferJob?.properties?.totalCount;
|
|
const isJobInProgress = isCurrentJobInProgress(portalDataTransferJob);
|
|
return isJobInProgress ? (totalCount > 0 ? processedCount / totalCount : null) : 0;
|
|
};
|
|
|
|
return (
|
|
<Stack tokens={{ childrenGap: 20 }} styles={{ root: { maxWidth: 600 } }}>
|
|
<Stack tokens={{ childrenGap: 10 }}>
|
|
{!isReadOnly && (
|
|
<Text styles={textHeadingStyle}>
|
|
{t(Keys.controls.settings.partitionKeyEditor.changePartitionKey, {
|
|
partitionKeyName: partitionKeyName.toLowerCase(),
|
|
})}
|
|
</Text>
|
|
)}
|
|
<Stack horizontal tokens={{ childrenGap: 20 }}>
|
|
<Stack tokens={{ childrenGap: 5 }}>
|
|
<Text styles={textSubHeadingStyle}>
|
|
{t(Keys.controls.settings.partitionKeyEditor.currentPartitionKey, {
|
|
partitionKeyName: partitionKeyName.toLowerCase(),
|
|
})}
|
|
</Text>
|
|
<Text styles={textSubHeadingStyle}>{t(Keys.controls.settings.partitionKeyEditor.partitioning)}</Text>
|
|
</Stack>
|
|
<Stack tokens={{ childrenGap: 5 }} data-test="partition-key-values">
|
|
<Text styles={textSubHeadingStyle1}>{partitionKeyValue}</Text>
|
|
<Text styles={textSubHeadingStyle1}>
|
|
{isHierarchicalPartitionedContainer()
|
|
? t(Keys.controls.settings.partitionKeyEditor.hierarchical)
|
|
: t(Keys.controls.settings.partitionKeyEditor.nonHierarchical)}
|
|
</Text>
|
|
</Stack>
|
|
</Stack>
|
|
</Stack>
|
|
|
|
{!isReadOnly && (
|
|
<>
|
|
<MessageBar
|
|
data-test="partition-key-warning"
|
|
messageBarType={MessageBarType.warning}
|
|
messageBarIconProps={{ iconName: "WarningSolid", className: "messageBarWarningIcon" }}
|
|
styles={darkThemeMessageBarStyles}
|
|
>
|
|
{t(Keys.controls.settings.partitionKeyEditor.safeguardWarning)}
|
|
<Link
|
|
href="https://learn.microsoft.com/azure/cosmos-db/container-copy#how-does-container-copy-work"
|
|
target="_blank"
|
|
underline
|
|
style={{ color: "var(--colorBrandForeground1)" }}
|
|
>
|
|
{t(Keys.common.learnMore)}
|
|
</Link>
|
|
</MessageBar>
|
|
<Text styles={{ root: { color: "var(--colorNeutralForeground1)" } }}>
|
|
{t(Keys.controls.settings.partitionKeyEditor.changeDescription)}
|
|
</Text>
|
|
{configContext.platform !== Platform.Emulator && (
|
|
<PrimaryButton
|
|
data-test="change-partition-key-button"
|
|
styles={{ root: { width: "fit-content" } }}
|
|
text={t(Keys.controls.settings.partitionKeyEditor.changeButton)}
|
|
onClick={startPartitionkeyChangeWorkflow}
|
|
disabled={isCurrentJobInProgress(portalDataTransferJob)}
|
|
/>
|
|
)}
|
|
{portalDataTransferJob && (
|
|
<Stack>
|
|
<Text styles={textHeadingStyle}>
|
|
{t(Keys.controls.settings.partitionKeyEditor.changeJob, { partitionKeyName })}
|
|
</Text>
|
|
<Stack
|
|
horizontal
|
|
tokens={{ childrenGap: 20 }}
|
|
styles={{
|
|
root: {
|
|
alignItems: "center",
|
|
},
|
|
}}
|
|
>
|
|
<ProgressIndicator
|
|
label={portalDataTransferJob?.properties?.jobName}
|
|
description={getProgressDescription()}
|
|
percentComplete={getPercentageComplete()}
|
|
styles={{
|
|
root: {
|
|
width: "85%",
|
|
},
|
|
}}
|
|
></ProgressIndicator>
|
|
{isCurrentJobInProgress(portalDataTransferJob) &&
|
|
(isOnlineJob(portalDataTransferJob) ? (
|
|
<IconButton
|
|
data-test="online-job-action-menu"
|
|
role="button"
|
|
iconProps={{
|
|
iconName: "More",
|
|
styles: { root: { fontSize: "20px", fontWeight: "bold" } },
|
|
}}
|
|
menuProps={getOnlineJobMenuProps(portalDataTransferJob)}
|
|
menuIconProps={{ iconName: "", className: "hidden" }}
|
|
ariaLabel={t(Keys.containerCopy.monitorJobs.columns.actions)}
|
|
title={t(Keys.containerCopy.monitorJobs.columns.actions)}
|
|
/>
|
|
) : (
|
|
<DefaultButton
|
|
text={t(Keys.controls.settings.partitionKeyEditor.cancelButton)}
|
|
onClick={() => cancelRunningDataTransferJob(portalDataTransferJob)}
|
|
/>
|
|
))}
|
|
</Stack>
|
|
</Stack>
|
|
)}
|
|
</>
|
|
)}
|
|
</Stack>
|
|
);
|
|
};
|