mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-01-06 03:00:23 +00:00
Compare commits
3 Commits
release/ig
...
users/jawe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff6fb32ad1 | ||
|
|
6b150dbfa0 | ||
|
|
bbdf0ce57e |
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -8,6 +8,8 @@ on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- hotfix/**
|
||||
- release/**
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
|
||||
8
package-lock.json
generated
8
package-lock.json
generated
@@ -10,7 +10,7 @@
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@azure/arm-cosmosdb": "9.1.0",
|
||||
"@azure/cosmos": "4.5.0",
|
||||
"@azure/cosmos": "4.7.0",
|
||||
"@azure/cosmos-language-service": "0.0.5",
|
||||
"@azure/identity": "4.5.0",
|
||||
"@azure/msal-browser": "2.14.2",
|
||||
@@ -391,9 +391,9 @@
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/@azure/cosmos": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure/cosmos/-/cosmos-4.5.0.tgz",
|
||||
"integrity": "sha512-JsTh4twb6FcwP7rJwxQiNZQ/LGtuF6gmciaxY9Rnp6/A325Lhsw/SH4R2ArpT0yCvozbZpweIwdPfUkXVBtp5w==",
|
||||
"version": "4.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure/cosmos/-/cosmos-4.7.0.tgz",
|
||||
"integrity": "sha512-a8OV7E41u/ZDaaaDAFdqTTiJ7c82jZc/+ot3XzNCIIilR25NBB+1ixzWQOAgP8SHRUIKfaUl6wAPdTuiG9I66A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@azure/abort-controller": "^2.1.2",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"@azure/arm-cosmosdb": "9.1.0",
|
||||
"@azure/cosmos": "4.5.0",
|
||||
"@azure/cosmos": "4.7.0",
|
||||
"@azure/cosmos-language-service": "0.0.5",
|
||||
"@azure/identity": "4.5.0",
|
||||
"@azure/msal-browser": "2.14.2",
|
||||
@@ -248,4 +248,4 @@
|
||||
"printWidth": 120,
|
||||
"endOfLine": "auto"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ const backendEndpoint = "https://cdb-ms-mpac-pbe.cosmos.azure.com";
|
||||
const previewSiteEndpoint = "https://dataexplorer-preview.azurewebsites.net";
|
||||
const previewStorageWebsiteEndpoint = "https://dataexplorerpreview.z5.web.core.windows.net/";
|
||||
const githubApiUrl = "https://api.github.com/repos/Azure/cosmos-explorer";
|
||||
const githubPullRequestUrl = "https://github.com/Azure/cosmos-explorer/pull";
|
||||
const azurePortalMpacEndpoint = "https://ms.portal.azure.com/";
|
||||
|
||||
const api = createProxyMiddleware({
|
||||
@@ -56,7 +57,11 @@ app.get("/pull/:pr(\\d+)", (req, res) => {
|
||||
|
||||
fetch(`${githubApiUrl}/pulls/${pr}`)
|
||||
.then((response) => response.json())
|
||||
.then(({ head: { sha } }) => {
|
||||
.then(({ head: { ref, sha } }) => {
|
||||
const prUrl = new URL(`${githubPullRequestUrl}/${pr}`);
|
||||
prUrl.hash = ref;
|
||||
search.set("feature.pr", prUrl.href);
|
||||
|
||||
const explorer = new URL(`${previewSiteEndpoint}/commit/${sha}/explorer.html`);
|
||||
explorer.search = search.toString();
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@ export class CapabilityNames {
|
||||
public static readonly EnableDataMasking: string = "EnableDataMasking";
|
||||
public static readonly EnableDynamicDataMasking: string = "EnableDynamicDataMasking";
|
||||
public static readonly EnableNoSQLFullTextSearchPreviewFeatures: string = "EnableNoSQLFullTextSearchPreviewFeatures";
|
||||
public static readonly EnableOnlineCopyFeature: string = "EnableOnlineContainerCopy";
|
||||
public static readonly EnableOnlineCopyFeature: string = "EnableOnlineCopyFeature";
|
||||
}
|
||||
|
||||
export enum CapacityMode {
|
||||
|
||||
@@ -23,10 +23,7 @@ export const handleError = (error: string | ARMError | Error, area: string, cons
|
||||
};
|
||||
|
||||
export const getErrorMessage = (error: string | Error = ""): string => {
|
||||
let errorMessage = typeof error === "string" ? error : error.message;
|
||||
if (!errorMessage) {
|
||||
errorMessage = JSON.stringify(error);
|
||||
}
|
||||
const errorMessage = typeof error === "string" ? error : error.message;
|
||||
return replaceKnownError(errorMessage);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from "react";
|
||||
import { userContext } from "UserContext";
|
||||
import { logError } from "../../../Common/Logger";
|
||||
import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||
import {
|
||||
cancel,
|
||||
@@ -160,8 +159,7 @@ export const submitCreateCopyJob = async (state: CopyJobContextState, onSuccess:
|
||||
onSuccess();
|
||||
return response;
|
||||
} catch (error) {
|
||||
const errorMessage = error.message || "Error submitting create copy job. Please try again later.";
|
||||
logError(errorMessage, "CopyJob/CopyJobActions.submitCreateCopyJob");
|
||||
console.error("Error submitting create copy job:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -200,7 +198,8 @@ export const updateCopyJobStatus = async (job: CopyJobType, action: string): Pro
|
||||
pattern,
|
||||
`'${ContainerCopyMessages.MonitorJobs.Status.InProgress}'`,
|
||||
);
|
||||
logError(`Error updating copy job status: ${normalizedErrorMessage}`, "CopyJob/CopyJobActions.updateCopyJobStatus");
|
||||
|
||||
console.error(`Error updating copy job status: ${normalizedErrorMessage}`);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -48,10 +48,8 @@ export default {
|
||||
|
||||
// Assign Permissions Screen
|
||||
assignPermissions: {
|
||||
crossAccountDescription:
|
||||
description:
|
||||
"To copy data from the source to the destination container, ensure that the managed identity of the destination account has read access to the source account by completing the following steps.",
|
||||
intraAccountOnlineDescription: (accountName: string) =>
|
||||
`Follow the steps below to enable online copy on your "${accountName}" account.`,
|
||||
},
|
||||
toggleBtn: {
|
||||
onText: "On",
|
||||
@@ -117,7 +115,7 @@ export default {
|
||||
},
|
||||
onlineCopyEnabled: {
|
||||
title: "Online copy enabled",
|
||||
description: (accountName: string) => `Enable Online copy on "${accountName}".`,
|
||||
description: (accountName: string) => `Use Azure CLI to enable Online copy on "${accountName}".`,
|
||||
hrefText: "Learn more about online copy jobs",
|
||||
href: "https://learn.microsoft.com/en-us/azure/cosmos-db/container-copy?tabs=online-copy&pivots=api-nosql#enable-online-copy",
|
||||
buttonText: "Enable Online Copy",
|
||||
|
||||
@@ -39,23 +39,16 @@ const getInitialCopyJobState = (): CopyJobContextState => {
|
||||
const CopyJobContextProvider: React.FC<CopyJobContextProviderProps> = (props) => {
|
||||
const [copyJobState, setCopyJobState] = React.useState<CopyJobContextState>(getInitialCopyJobState());
|
||||
const [flow, setFlow] = React.useState<CopyJobFlowType | null>(null);
|
||||
const [contextError, setContextError] = React.useState<string | null>(null);
|
||||
|
||||
const resetCopyJobState = () => {
|
||||
setCopyJobState(getInitialCopyJobState());
|
||||
};
|
||||
|
||||
const contextValue: CopyJobContextProviderType = {
|
||||
contextError,
|
||||
setContextError,
|
||||
copyJobState,
|
||||
setCopyJobState,
|
||||
flow,
|
||||
setFlow,
|
||||
resetCopyJobState,
|
||||
};
|
||||
|
||||
return <CopyJobContext.Provider value={contextValue}>{props.children}</CopyJobContext.Provider>;
|
||||
return (
|
||||
<CopyJobContext.Provider value={{ copyJobState, setCopyJobState, flow, setFlow, resetCopyJobState }}>
|
||||
{props.children}
|
||||
</CopyJobContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default CopyJobContextProvider;
|
||||
|
||||
@@ -106,7 +106,7 @@ export function getAccountDetailsFromResourceId(accountId: string | undefined) {
|
||||
return null;
|
||||
}
|
||||
const pattern = new RegExp(
|
||||
"/subscriptions/([^/]+)/resourceGroups/([^/]+)/providers/Microsoft\\.DocumentDB?/databaseAccounts/([^/]+)",
|
||||
"/subscriptions/([^/]+)/resourceGroups/([^/]+)/providers/Microsoft\\.DocumentDB/databaseAccounts/([^/]+)",
|
||||
"i",
|
||||
);
|
||||
const matches = accountId.match(pattern);
|
||||
@@ -114,13 +114,3 @@ export function getAccountDetailsFromResourceId(accountId: string | undefined) {
|
||||
const [_, subscriptionId, resourceGroup, accountName] = matches || [];
|
||||
return { subscriptionId, resourceGroup, accountName };
|
||||
}
|
||||
|
||||
export function isIntraAccountCopy(sourceAccountId: string | undefined, targetAccountId: string | undefined): boolean {
|
||||
const sourceAccountDetails = getAccountDetailsFromResourceId(sourceAccountId);
|
||||
const targetAccountDetails = getAccountDetailsFromResourceId(targetAccountId);
|
||||
return (
|
||||
sourceAccountDetails?.subscriptionId === targetAccountDetails?.subscriptionId &&
|
||||
sourceAccountDetails?.resourceGroup === targetAccountDetails?.resourceGroup &&
|
||||
sourceAccountDetails?.accountName === targetAccountDetails?.accountName
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Link, Stack, Text, Toggle } from "@fluentui/react";
|
||||
import React, { useCallback } from "react";
|
||||
import { logError } from "../../../../../Common/Logger";
|
||||
import { assignRole } from "../../../../../Utils/arm/RbacUtils";
|
||||
import ContainerCopyMessages from "../../../ContainerCopyMessages";
|
||||
import { useCopyJobContext } from "../../../Context/CopyJobContext";
|
||||
@@ -22,7 +21,7 @@ type AddReadPermissionToDefaultIdentityProps = Partial<PermissionSectionConfig>;
|
||||
|
||||
const AddReadPermissionToDefaultIdentity: React.FC<AddReadPermissionToDefaultIdentityProps> = () => {
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const { copyJobState, setCopyJobState, setContextError } = useCopyJobContext();
|
||||
const { copyJobState, setCopyJobState } = useCopyJobContext();
|
||||
const [readPermissionAssigned, onToggle] = useToggle(false);
|
||||
|
||||
const handleAddReadPermission = useCallback(async () => {
|
||||
@@ -49,14 +48,11 @@ const AddReadPermissionToDefaultIdentity: React.FC<AddReadPermissionToDefaultIde
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error.message || "Error assigning read permission to default identity. Please try again later.";
|
||||
logError(errorMessage, "CopyJob/AddReadPermissionToDefaultIdentity.handleAddReadPermission");
|
||||
setContextError(errorMessage);
|
||||
console.error("Error assigning read permission to default identity:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [copyJobState, setCopyJobState, setContextError]);
|
||||
}, [copyJobState, setCopyJobState]);
|
||||
|
||||
return (
|
||||
<Stack className="defaultManagedIdentityContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}>
|
||||
|
||||
@@ -6,7 +6,6 @@ import WarningIcon from "../../../../../../images/warning.svg";
|
||||
import ShimmerTree, { IndentLevel } from "../../../../../Common/ShimmerTree/ShimmerTree";
|
||||
import ContainerCopyMessages from "../../../ContainerCopyMessages";
|
||||
import { useCopyJobContext } from "../../../Context/CopyJobContext";
|
||||
import { isIntraAccountCopy } from "../../../CopyJobUtils";
|
||||
import { CopyJobMigrationType } from "../../../Enums/CopyJobEnums";
|
||||
import usePermissionSections, { PermissionSectionConfig } from "./hooks/usePermissionsSection";
|
||||
|
||||
@@ -40,8 +39,6 @@ const AssignPermissions = () => {
|
||||
[],
|
||||
);
|
||||
|
||||
const isSameAccount = isIntraAccountCopy(copyJobState?.source?.account?.id, copyJobState?.target?.account?.id);
|
||||
|
||||
useEffect(() => {
|
||||
const firstIncompleteSection = permissionSections.find((section) => !section.completed);
|
||||
const nextOpenItems = firstIncompleteSection ? [firstIncompleteSection.id] : [];
|
||||
@@ -52,13 +49,7 @@ const AssignPermissions = () => {
|
||||
|
||||
return (
|
||||
<Stack className="assignPermissionsContainer" tokens={{ childrenGap: 15 }}>
|
||||
<span>
|
||||
{isSameAccount && copyJobState.migrationType === CopyJobMigrationType.Online
|
||||
? ContainerCopyMessages.assignPermissions.intraAccountOnlineDescription(
|
||||
copyJobState?.source?.account?.name || "",
|
||||
)
|
||||
: ContainerCopyMessages.assignPermissions.crossAccountDescription}
|
||||
</span>
|
||||
<span>{ContainerCopyMessages.assignPermissions.description}</span>
|
||||
{permissionSections?.length === 0 ? (
|
||||
<ShimmerTree indentLevels={indentLevels} style={{ width: "100%" }} />
|
||||
) : (
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { Link, PrimaryButton, Stack } from "@fluentui/react";
|
||||
import { CapabilityNames } from "Common/Constants";
|
||||
import { DatabaseAccount } from "Contracts/DataModels";
|
||||
import React from "react";
|
||||
import { fetchDatabaseAccount } from "Utils/arm/databaseAccountUtils";
|
||||
import { logError } from "../../../../../Common/Logger";
|
||||
import { update as updateDatabaseAccount } from "../../../../../Utils/arm/generatedClients/cosmos/databaseAccounts";
|
||||
import ContainerCopyMessages from "../../../ContainerCopyMessages";
|
||||
import { useCopyJobContext } from "../../../Context/CopyJobContext";
|
||||
import { getAccountDetailsFromResourceId } from "../../../CopyJobUtils";
|
||||
@@ -22,10 +19,8 @@ const OnlineCopyEnabled: React.FC = () => {
|
||||
const [showRefreshButton, setShowRefreshButton] = React.useState(false);
|
||||
const intervalRef = React.useRef<NodeJS.Timeout | null>(null);
|
||||
const timeoutRef = React.useRef<NodeJS.Timeout | null>(null);
|
||||
const { setContextError, copyJobState: { source } = {}, setCopyJobState } = useCopyJobContext();
|
||||
const { copyJobState: { source } = {}, setCopyJobState } = useCopyJobContext();
|
||||
const selectedSourceAccount = source?.account;
|
||||
const sourceAccountCapabilities = selectedSourceAccount?.properties?.capabilities ?? [];
|
||||
|
||||
const {
|
||||
subscriptionId: sourceSubscriptionId,
|
||||
resourceGroup: sourceResourceGroup,
|
||||
@@ -43,24 +38,16 @@ const OnlineCopyEnabled: React.FC = () => {
|
||||
setLoading(false);
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error.message || "Error fetching source account after enabling online copy. Please try again later.";
|
||||
logError(errorMessage, "CopyJob/OnlineCopyEnabled.handleFetchAccount");
|
||||
setContextError(errorMessage);
|
||||
clearAccountFetchInterval();
|
||||
console.error("Error fetching source account after enabling online copy:", error);
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const clearAccountFetchInterval = () => {
|
||||
const clearIntervalAndShowRefresh = () => {
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current);
|
||||
intervalRef.current = null;
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const clearIntervalAndShowRefresh = () => {
|
||||
clearAccountFetchInterval();
|
||||
setShowRefreshButton(true);
|
||||
};
|
||||
|
||||
@@ -69,42 +56,18 @@ const OnlineCopyEnabled: React.FC = () => {
|
||||
handleFetchAccount();
|
||||
};
|
||||
|
||||
const handleOnlineCopyEnable = async () => {
|
||||
setLoading(true);
|
||||
setShowRefreshButton(false);
|
||||
|
||||
try {
|
||||
await updateDatabaseAccount(sourceSubscriptionId, sourceResourceGroup, sourceAccountName, {
|
||||
properties: {
|
||||
enableAllVersionsAndDeletesChangeFeed: true,
|
||||
},
|
||||
});
|
||||
|
||||
await updateDatabaseAccount(sourceSubscriptionId, sourceResourceGroup, sourceAccountName, {
|
||||
properties: {
|
||||
capabilities: [...sourceAccountCapabilities, { name: CapabilityNames.EnableOnlineCopyFeature }],
|
||||
},
|
||||
});
|
||||
|
||||
intervalRef.current = setInterval(() => {
|
||||
handleFetchAccount();
|
||||
}, 30 * 1000);
|
||||
|
||||
timeoutRef.current = setTimeout(
|
||||
() => {
|
||||
clearIntervalAndShowRefresh();
|
||||
},
|
||||
10 * 60 * 1000,
|
||||
);
|
||||
} catch (error) {
|
||||
const errorMessage = error.message || "Failed to enable online copy feature. Please try again later.";
|
||||
logError(errorMessage, "CopyJob/OnlineCopyEnabled.handleOnlineCopyEnable");
|
||||
setContextError(errorMessage);
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
intervalRef.current = setInterval(() => {
|
||||
handleFetchAccount();
|
||||
}, 30 * 1000);
|
||||
|
||||
timeoutRef.current = setTimeout(
|
||||
() => {
|
||||
clearIntervalAndShowRefresh();
|
||||
},
|
||||
15 * 60 * 1000,
|
||||
);
|
||||
|
||||
return () => {
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current);
|
||||
@@ -126,7 +89,32 @@ const OnlineCopyEnabled: React.FC = () => {
|
||||
</Link>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
{showRefreshButton ? (
|
||||
<pre style={{ backgroundColor: "#f5f5f5", padding: "10px", borderRadius: "4px", overflow: "auto" }}>
|
||||
<code>
|
||||
{`# Set shell variables
|
||||
$resourceGroupName = <azure_resource_group>
|
||||
$accountName = <azure_cosmos_db_account_name>
|
||||
$EnableOnlineContainerCopy = "EnableOnlineContainerCopy"
|
||||
|
||||
# List down existing capabilities of your account
|
||||
$cosmosdb = az cosmosdb show --resource-group $resourceGroupName --name $accountName
|
||||
|
||||
$capabilities = (($cosmosdb | ConvertFrom-Json).capabilities)
|
||||
|
||||
# Append EnableOnlineContainerCopy capability in the list of capabilities
|
||||
$capabilitiesToAdd = @()
|
||||
foreach ($item in $capabilities) {
|
||||
$capabilitiesToAdd += $item.name
|
||||
}
|
||||
$capabilitiesToAdd += $EnableOnlineContainerCopy
|
||||
|
||||
# Update Cosmos DB account
|
||||
az cosmosdb update --capabilities $capabilitiesToAdd -n $accountName -g $resourceGroupName`}
|
||||
</code>
|
||||
</pre>
|
||||
</Stack.Item>
|
||||
{showRefreshButton && (
|
||||
<Stack.Item>
|
||||
<PrimaryButton
|
||||
className="fullWidth"
|
||||
text={ContainerCopyMessages.refreshButtonLabel}
|
||||
@@ -134,16 +122,8 @@ const OnlineCopyEnabled: React.FC = () => {
|
||||
onClick={handleRefresh}
|
||||
disabled={loading}
|
||||
/>
|
||||
) : (
|
||||
<PrimaryButton
|
||||
className="fullWidth"
|
||||
text={loading ? "" : ContainerCopyMessages.onlineCopyEnabled.buttonText}
|
||||
{...(loading ? { iconProps: { iconName: "SyncStatusSolid" } } : {})}
|
||||
disabled={loading}
|
||||
onClick={handleOnlineCopyEnable}
|
||||
/>
|
||||
)}
|
||||
</Stack.Item>
|
||||
</Stack.Item>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,7 +2,6 @@ import { Link, PrimaryButton, Stack, Text } from "@fluentui/react";
|
||||
import { DatabaseAccount } from "Contracts/DataModels";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { fetchDatabaseAccount } from "Utils/arm/databaseAccountUtils";
|
||||
import { logError } from "../../../../../Common/Logger";
|
||||
import ContainerCopyMessages from "../../../ContainerCopyMessages";
|
||||
import { useCopyJobContext } from "../../../Context/CopyJobContext";
|
||||
import { buildResourceLink, getAccountDetailsFromResourceId } from "../../../CopyJobUtils";
|
||||
@@ -64,23 +63,17 @@ const PointInTimeRestore: React.FC = () => {
|
||||
setLoading(false);
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error.message || "Error fetching source account after Point-in-Time Restore. Please try again later.";
|
||||
logError(errorMessage, "CopyJob/PointInTimeRestore.handleFetchAccount");
|
||||
clearAccountFetchInterval();
|
||||
console.error("Error fetching source account after Point-in-Time Restore:", error);
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const clearAccountFetchInterval = () => {
|
||||
const clearIntervalAndShowRefresh = () => {
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current);
|
||||
intervalRef.current = null;
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const clearIntervalAndShowRefresh = () => {
|
||||
clearAccountFetchInterval();
|
||||
setShowRefreshButton(true);
|
||||
};
|
||||
|
||||
@@ -102,7 +95,7 @@ const PointInTimeRestore: React.FC = () => {
|
||||
() => {
|
||||
clearIntervalAndShowRefresh();
|
||||
},
|
||||
10 * 60 * 1000,
|
||||
15 * 60 * 1000,
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { DatabaseAccount } from "Contracts/DataModels";
|
||||
import { useCallback, useState } from "react";
|
||||
import { logError } from "../../../../../../Common/Logger";
|
||||
import { useCopyJobContext } from "../../../../Context/CopyJobContext";
|
||||
import { getAccountDetailsFromResourceId } from "../../../../CopyJobUtils";
|
||||
|
||||
@@ -20,7 +19,7 @@ interface UseManagedIdentityUpdaterReturn {
|
||||
const useManagedIdentity = (
|
||||
updateIdentityFn: UseManagedIdentityUpdaterParams["updateIdentityFn"],
|
||||
): UseManagedIdentityUpdaterReturn => {
|
||||
const { copyJobState, setCopyJobState, setContextError } = useCopyJobContext();
|
||||
const { copyJobState, setCopyJobState } = useCopyJobContext();
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
|
||||
const handleAddSystemIdentity = useCallback(async (): Promise<void> => {
|
||||
@@ -41,9 +40,7 @@ const useManagedIdentity = (
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error.message || "Error enabling system-assigned managed identity. Please try again later.";
|
||||
logError(errorMessage, "CopyJob/useManagedIdentity.handleAddSystemIdentity");
|
||||
setContextError(errorMessage);
|
||||
console.error("Error enabling system-assigned managed identity:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { CapabilityNames } from "../../../../../../Common/Constants";
|
||||
import { fetchRoleAssignments, fetchRoleDefinitions, RoleDefinitionType } from "../../../../../../Utils/arm/RbacUtils";
|
||||
import ContainerCopyMessages from "../../../../ContainerCopyMessages";
|
||||
import { getAccountDetailsFromResourceId, isIntraAccountCopy } from "../../../../CopyJobUtils";
|
||||
import { getAccountDetailsFromResourceId } from "../../../../CopyJobUtils";
|
||||
import {
|
||||
BackupPolicyType,
|
||||
CopyJobMigrationType,
|
||||
@@ -139,9 +139,7 @@ const usePermissionSections = (state: CopyJobContextState): PermissionSectionCon
|
||||
const isValidatingRef = useRef(false);
|
||||
|
||||
const sectionToValidate = useMemo(() => {
|
||||
const isSameAccount = isIntraAccountCopy(sourceAccountId, targetAccountId);
|
||||
|
||||
const baseSections = isSameAccount ? [] : [...PERMISSION_SECTIONS_CONFIG];
|
||||
const baseSections = sourceAccountId === targetAccountId ? [] : [...PERMISSION_SECTIONS_CONFIG];
|
||||
if (state.migrationType === CopyJobMigrationType.Online) {
|
||||
return [...baseSections, ...PERMISSION_SECTIONS_FOR_ONLINE_JOBS];
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { MessageBar, MessageBarType, Stack } from "@fluentui/react";
|
||||
import React from "react";
|
||||
import { useCopyJobContext } from "../../Context/CopyJobContext";
|
||||
import { useCopyJobNavigation } from "../Utils/useCopyJobNavigation";
|
||||
import NavigationControls from "./Components/NavigationControls";
|
||||
|
||||
@@ -13,23 +12,24 @@ const CreateCopyJobScreens: React.FC = () => {
|
||||
handlePrevious,
|
||||
handleCancel,
|
||||
primaryBtnText,
|
||||
error,
|
||||
setError,
|
||||
} = useCopyJobNavigation();
|
||||
const { contextError, setContextError } = useCopyJobContext();
|
||||
|
||||
return (
|
||||
<Stack verticalAlign="space-between" className="createCopyJobScreensContainer">
|
||||
<Stack.Item className="createCopyJobScreensContent">
|
||||
{contextError && (
|
||||
{error && (
|
||||
<MessageBar
|
||||
className="createCopyJobErrorMessageBar"
|
||||
messageBarType={MessageBarType.blocked}
|
||||
isMultiline={false}
|
||||
onDismiss={() => setContextError(null)}
|
||||
onDismiss={() => setError(null)}
|
||||
dismissButtonAriaLabel="Close"
|
||||
truncated={true}
|
||||
overflowButtonAriaLabel="See more"
|
||||
>
|
||||
{contextError}
|
||||
{error}
|
||||
</MessageBar>
|
||||
)}
|
||||
{currentScreen?.component}
|
||||
|
||||
@@ -2,7 +2,6 @@ import React from "react";
|
||||
import { DatabaseAccount, Subscription } from "../../../../../../Contracts/DataModels";
|
||||
import { CopyJobMigrationType } from "../../../../Enums/CopyJobEnums";
|
||||
import { CopyJobContextProviderType, CopyJobContextState, DropdownOptionType } from "../../../../Types/CopyJobTypes";
|
||||
import { useCopyJobPrerequisitesCache } from "../../../Utils/useCopyJobPrerequisitesCache";
|
||||
|
||||
export function useDropdownOptions(
|
||||
subscriptions: Subscription[],
|
||||
@@ -37,7 +36,6 @@ export function useDropdownOptions(
|
||||
type setCopyJobStateType = CopyJobContextProviderType["setCopyJobState"];
|
||||
|
||||
export function useEventHandlers(setCopyJobState: setCopyJobStateType) {
|
||||
const { setValidationCache } = useCopyJobPrerequisitesCache();
|
||||
const handleSelectSourceAccount = React.useCallback(
|
||||
(type: "subscription" | "account", data: (Subscription & DatabaseAccount) | undefined) => {
|
||||
setCopyJobState((prevState: CopyJobContextState) => {
|
||||
@@ -62,9 +60,8 @@ export function useEventHandlers(setCopyJobState: setCopyJobStateType) {
|
||||
}
|
||||
return prevState;
|
||||
});
|
||||
setValidationCache(new Map<string, boolean>());
|
||||
},
|
||||
[setCopyJobState, setValidationCache],
|
||||
[setCopyJobState],
|
||||
);
|
||||
|
||||
const handleMigrationTypeChange = React.useCallback(
|
||||
@@ -73,9 +70,8 @@ export function useEventHandlers(setCopyJobState: setCopyJobStateType) {
|
||||
...prevState,
|
||||
migrationType: checked ? CopyJobMigrationType.Offline : CopyJobMigrationType.Online,
|
||||
}));
|
||||
setValidationCache(new Map<string, boolean>());
|
||||
},
|
||||
[setCopyJobState, setValidationCache],
|
||||
[setCopyJobState],
|
||||
);
|
||||
|
||||
return { handleSelectSourceAccount, handleMigrationTypeChange };
|
||||
|
||||
@@ -2,7 +2,6 @@ import { useCallback, useMemo, useReducer, useState } from "react";
|
||||
import { useSidePanel } from "../../../../hooks/useSidePanel";
|
||||
import { submitCreateCopyJob } from "../../Actions/CopyJobActions";
|
||||
import { useCopyJobContext } from "../../Context/CopyJobContext";
|
||||
import { isIntraAccountCopy } from "../../CopyJobUtils";
|
||||
import { CopyJobMigrationType } from "../../Enums/CopyJobEnums";
|
||||
import { useCopyJobPrerequisitesCache } from "./useCopyJobPrerequisitesCache";
|
||||
import { SCREEN_KEYS, useCreateCopyJobScreensList } from "./useCreateCopyJobScreensList";
|
||||
@@ -34,7 +33,8 @@ function navigationReducer(state: NavigationState, action: Action): NavigationSt
|
||||
|
||||
export function useCopyJobNavigation() {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { copyJobState, resetCopyJobState, setContextError } = useCopyJobContext();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const { copyJobState, resetCopyJobState } = useCopyJobContext();
|
||||
const screens = useCreateCopyJobScreensList();
|
||||
const { validationCache: cache } = useCopyJobPrerequisitesCache();
|
||||
const [state, dispatch] = useReducer(navigationReducer, { screenHistory: [SCREEN_KEYS.SelectAccount] });
|
||||
@@ -71,13 +71,18 @@ export function useCopyJobNavigation() {
|
||||
containerId: container?.containerId || "",
|
||||
});
|
||||
|
||||
const isSameAccount = (
|
||||
sourceIds: ReturnType<typeof getContainerIdentifiers>,
|
||||
targetIds: ReturnType<typeof getContainerIdentifiers>,
|
||||
) => sourceIds.accountId === targetIds.accountId;
|
||||
|
||||
const areContainersIdentical = () => {
|
||||
const { source, target } = copyJobState;
|
||||
const sourceIds = getContainerIdentifiers(source);
|
||||
const targetIds = getContainerIdentifiers(target);
|
||||
|
||||
return (
|
||||
isIntraAccountCopy(sourceIds.accountId, targetIds.accountId) &&
|
||||
isSameAccount(sourceIds, targetIds) &&
|
||||
sourceIds.databaseId === targetIds.databaseId &&
|
||||
sourceIds.containerId === targetIds.containerId
|
||||
);
|
||||
@@ -85,10 +90,9 @@ export function useCopyJobNavigation() {
|
||||
|
||||
const shouldNotShowPermissionScreen = () => {
|
||||
const { source, target, migrationType } = copyJobState;
|
||||
const sourceIds = getContainerIdentifiers(source);
|
||||
const targetIds = getContainerIdentifiers(target);
|
||||
return (
|
||||
migrationType === CopyJobMigrationType.Offline && isIntraAccountCopy(sourceIds.accountId, targetIds.accountId)
|
||||
migrationType === CopyJobMigrationType.Offline &&
|
||||
isSameAccount(getContainerIdentifiers(source), getContainerIdentifiers(target))
|
||||
);
|
||||
};
|
||||
|
||||
@@ -101,7 +105,7 @@ export function useCopyJobNavigation() {
|
||||
error instanceof Error
|
||||
? error.message || "Failed to create copy job. Please try again later."
|
||||
: "Failed to create copy job. Please try again later.";
|
||||
setContextError(errorMessage);
|
||||
setError(errorMessage);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
@@ -109,13 +113,11 @@ export function useCopyJobNavigation() {
|
||||
|
||||
const handlePrimary = useCallback(() => {
|
||||
if (currentScreenKey === SCREEN_KEYS.SelectSourceAndTargetContainers && areContainersIdentical()) {
|
||||
setContextError(
|
||||
"Source and destination containers cannot be the same. Please select different containers to proceed.",
|
||||
);
|
||||
setError("Source and destination containers cannot be the same. Please select different containers to proceed.");
|
||||
return;
|
||||
}
|
||||
|
||||
setContextError(null);
|
||||
setError(null);
|
||||
const transitions = {
|
||||
[SCREEN_KEYS.SelectAccount]: shouldNotShowPermissionScreen()
|
||||
? SCREEN_KEYS.SelectSourceAndTargetContainers
|
||||
@@ -144,5 +146,7 @@ export function useCopyJobNavigation() {
|
||||
handlePrevious,
|
||||
handleCancel,
|
||||
primaryBtnText,
|
||||
error,
|
||||
setError,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -11,14 +11,7 @@ interface CopyJobActionMenuProps {
|
||||
|
||||
const CopyJobActionMenu: React.FC<CopyJobActionMenuProps> = ({ job, handleClick }) => {
|
||||
const [updatingJobAction, setUpdatingJobAction] = React.useState<{ jobName: string; action: string } | null>(null);
|
||||
if (
|
||||
[
|
||||
CopyJobStatusType.Completed,
|
||||
CopyJobStatusType.Cancelled,
|
||||
CopyJobStatusType.Failed,
|
||||
CopyJobStatusType.Faulted,
|
||||
].includes(job.Status)
|
||||
) {
|
||||
if ([CopyJobStatusType.Completed, CopyJobStatusType.Cancelled].includes(job.Status)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -62,7 +55,7 @@ const CopyJobActionMenu: React.FC<CopyJobActionMenuProps> = ({ job, handleClick
|
||||
[CopyJobStatusType.InProgress, CopyJobStatusType.Running, CopyJobStatusType.Partitioning].includes(job.Status)
|
||||
) {
|
||||
const filteredItems = baseItems.filter((item) => item.key !== CopyJobActions.resume);
|
||||
if ((job.Mode ?? "").toLowerCase() === CopyJobMigrationType.Online) {
|
||||
if (job.Mode === CopyJobMigrationType.Online) {
|
||||
filteredItems.push({
|
||||
key: CopyJobActions.complete,
|
||||
text: ContainerCopyMessages.MonitorJobs.Actions.complete,
|
||||
@@ -74,7 +67,7 @@ const CopyJobActionMenu: React.FC<CopyJobActionMenuProps> = ({ job, handleClick
|
||||
return filteredItems;
|
||||
}
|
||||
|
||||
if ([CopyJobStatusType.Skipped].includes(job.Status)) {
|
||||
if ([CopyJobStatusType.Failed, CopyJobStatusType.Faulted, CopyJobStatusType.Skipped].includes(job.Status)) {
|
||||
return baseItems.filter((item) => item.key === CopyJobActions.resume);
|
||||
}
|
||||
|
||||
|
||||
@@ -73,8 +73,6 @@ export interface CopyJobFlowType {
|
||||
}
|
||||
|
||||
export interface CopyJobContextProviderType {
|
||||
contextError: string | null;
|
||||
setContextError: React.Dispatch<React.SetStateAction<string | null>>;
|
||||
flow: CopyJobFlowType;
|
||||
setFlow: React.Dispatch<React.SetStateAction<CopyJobFlowType>>;
|
||||
copyJobState: CopyJobContextState | null;
|
||||
|
||||
@@ -16,6 +16,7 @@ import InfoIcon from "../../../../images/info_color.svg";
|
||||
import LoadingIcon from "../../../../images/loading.svg";
|
||||
import WarningIcon from "../../../../images/warning.svg";
|
||||
import { ClientDefaults, KeyCodes } from "../../../Common/Constants";
|
||||
import { userContext } from "../../../UserContext";
|
||||
import { useNotificationConsole } from "../../../hooks/useNotificationConsole";
|
||||
import { ConsoleData, ConsoleDataType } from "./ConsoleData";
|
||||
|
||||
@@ -126,6 +127,7 @@ export class NotificationConsoleComponent extends React.Component<
|
||||
<span className="numWarningItems">{numWarningItems}</span>
|
||||
</span>
|
||||
</span>
|
||||
{userContext.features.pr && <PrPreview pr={userContext.features.pr} />}
|
||||
<span className="consoleSplitter" />
|
||||
<span className="headerStatus">
|
||||
<span className="headerStatusEllipsis" aria-live="assertive" aria-atomic="true">
|
||||
@@ -291,6 +293,21 @@ export class NotificationConsoleComponent extends React.Component<
|
||||
};
|
||||
}
|
||||
|
||||
const PrPreview = (props: { pr: string }) => {
|
||||
const url = new URL(props.pr);
|
||||
const [, ref] = url.hash.split("#");
|
||||
url.hash = "";
|
||||
|
||||
return (
|
||||
<>
|
||||
<span className="consoleSplitter" />
|
||||
<a target="_blank" rel="noreferrer" href={url.href} style={{ marginRight: "1em", fontWeight: "bold" }}>
|
||||
{ref}
|
||||
</a>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const NotificationConsole: React.FC = () => {
|
||||
const setIsExpanded = useNotificationConsole((state) => state.setIsExpanded);
|
||||
const isExpanded = useNotificationConsole((state) => state.isExpanded);
|
||||
|
||||
13
src/NotebookViewer/notebookViewer.html
Normal file
13
src/NotebookViewer/notebookViewer.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="height=device-height, width=device-width, initial-scale=1.0" />
|
||||
<title>Notebook Viewer</title>
|
||||
<link rel="shortcut icon" href="../../images/CosmosDB_rgb_ui_lighttheme.ico" type="image/x-icon" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="notebookComponentContainer" id="notebookContent"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -25,6 +25,7 @@ export type Features = {
|
||||
readonly notebookServerUrl?: string;
|
||||
readonly sandboxNotebookOutputs: boolean;
|
||||
readonly selfServeType?: string;
|
||||
readonly pr?: string;
|
||||
readonly showMinRUSurvey: boolean;
|
||||
readonly ttl90Days: boolean;
|
||||
readonly mongoProxyEndpoint?: string;
|
||||
@@ -95,6 +96,7 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
|
||||
notebookServerUrl: get("notebookserverurl"),
|
||||
sandboxNotebookOutputs: "true" === get("sandboxnotebookoutputs", "true"),
|
||||
selfServeType: get("selfservetype"),
|
||||
pr: get("pr"),
|
||||
showMinRUSurvey: "true" === get("showminrusurvey"),
|
||||
ttl90Days: "true" === get("ttl90days"),
|
||||
autoscaleDefault: "true" === get("autoscaledefault"),
|
||||
|
||||
@@ -113,6 +113,7 @@ module.exports = function (_env = {}, argv = {}) {
|
||||
hostedExplorer: "./src/HostedExplorer.tsx",
|
||||
terminal: "./src/Terminal/index.ts",
|
||||
cellOutputViewer: "./src/CellOutputViewer/CellOutputViewer.tsx",
|
||||
notebookViewer: "./src/NotebookViewer/NotebookViewer.tsx",
|
||||
galleryViewer: "./src/GalleryViewer/GalleryViewer.tsx",
|
||||
selfServe: "./src/SelfServe/SelfServe.tsx",
|
||||
connectToGitHub: "./src/GitHub/GitHubConnector.ts",
|
||||
@@ -150,6 +151,11 @@ module.exports = function (_env = {}, argv = {}) {
|
||||
template: "src/CellOutputViewer/cellOutputViewer.html",
|
||||
chunks: ["cellOutputViewer"],
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
filename: "notebookViewer.html",
|
||||
template: "src/NotebookViewer/notebookViewer.html",
|
||||
chunks: ["notebookViewer"],
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
filename: "gallery.html",
|
||||
template: "src/GalleryViewer/galleryViewer.html",
|
||||
|
||||
Reference in New Issue
Block a user