Compare commits

...

5 Commits

Author SHA1 Message Date
Asier Isayas
30e106f600 clean up tests 2025-12-05 10:00:17 -08:00
Asier Isayas
383da73c52 Remove Synapse from command bar and container create 2025-12-05 09:35:35 -08:00
Laurent Nguyen
ca858c08fb Enhance accessibility by including description in aria-label for button component in Fabric Home (#2272)
Co-authored-by: Laurent Nguyen <languye@microsoft.com>
2025-12-05 17:39:18 +01:00
BChoudhury-ms
fa18b85364 copy job process performance enhancement (#2273) 2025-12-05 11:49:25 +05:30
sunghyunkang1111
d060f22357 Added minimum RU when creating container (#2268)
* Added minimum RU when creating container

* fix data test id

* Update test snap
2025-12-04 10:31:19 -08:00
39 changed files with 177 additions and 766 deletions

View File

@@ -531,11 +531,6 @@ export class PriorityLevel {
public static readonly Default = "low";
}
export class ariaLabelForLearnMoreLink {
public static readonly AnalyticalStore = "Learn more about analytical store.";
public static readonly AzureSynapseLink = "Learn more about Azure Synapse Link.";
}
export class GlobalSecondaryIndexLabels {
public static readonly NewGlobalSecondaryIndex: string = "New Global Secondary Index";
}

View File

@@ -44,8 +44,8 @@ export const getDatabaseEndpoint = (apiType: ApiType): string => {
return "gremlinDatabases";
case "Tables":
return "tables";
default:
case "SQL":
default:
return "sqlDatabases";
}
};
@@ -58,8 +58,8 @@ export const getCollectionEndpoint = (apiType: ApiType): string => {
return "tables";
case "Gremlin":
return "graphs";
default:
case "SQL":
default:
return "containers";
}
};

View File

@@ -95,9 +95,6 @@ const createSqlContainer = async (params: DataModels.CreateCollectionParams): Pr
const resource: ARMTypes.SqlContainerResource = {
id: params.collectionId,
};
if (params.analyticalStorageTtl) {
resource.analyticalStorageTtl = params.analyticalStorageTtl;
}
if (params.indexingPolicy) {
resource.indexingPolicy = params.indexingPolicy;
}
@@ -138,9 +135,6 @@ const createMongoCollection = async (params: DataModels.CreateCollectionParams):
const resource: ARMTypes.MongoDBCollectionResource = {
id: params.collectionId,
};
if (params.analyticalStorageTtl) {
resource.analyticalStorageTtl = params.analyticalStorageTtl;
}
if (params.partitionKey) {
const partitionKeyPath: string = params.partitionKey.paths[0];
resource.shardKey = { [partitionKeyPath]: "Hash" };
@@ -179,9 +173,6 @@ const createCassandraTable = async (params: DataModels.CreateCollectionParams):
const resource: ARMTypes.CassandraTableResource = {
id: params.collectionId,
};
if (params.analyticalStorageTtl) {
resource.analyticalStorageTtl = params.analyticalStorageTtl;
}
const rpPayload: ARMTypes.CassandraTableCreateUpdateParameters = {
properties: {
@@ -282,7 +273,6 @@ const createCollectionWithSDK = async (params: DataModels.CreateCollectionParams
partitionKey: params.partitionKey || undefined,
indexingPolicy: params.indexingPolicy || undefined,
uniqueKeyPolicy: params.uniqueKeyPolicy || undefined,
analyticalStorageTtl: params.analyticalStorageTtl,
vectorEmbeddingPolicy: params.vectorEmbeddingPolicy,
fullTextPolicy: params.fullTextPolicy,
} as ContainerRequest; // TODO: remove cast when https://github.com/Azure/azure-cosmos-js/issues/423 is fixed

View File

@@ -23,9 +23,6 @@ export const createGlobalSecondaryIndex = async (params: CreateGlobalSecondaryIn
if (params.materializedViewDefinition) {
resource.materializedViewDefinition = params.materializedViewDefinition;
}
if (params.analyticalStorageTtl) {
resource.analyticalStorageTtl = params.analyticalStorageTtl;
}
if (params.indexingPolicy) {
resource.indexingPolicy = params.indexingPolicy;
}

View File

@@ -36,14 +36,6 @@ export interface DatabaseAccountSystemData {
export interface DatabaseAccountBackupPolicy {
type: string;
/* periodicModeProperties?: {
backupIntervalInMinutes: number;
backupRetentionIntervalInHours: number;
backupStorageRedundancy: string;
};
continuousModeProperties?: {
tier: string;
}; */
}
export interface DatabaseAccountExtendedProperties {
@@ -73,6 +65,7 @@ export interface DatabaseAccountExtendedProperties {
publicNetworkAccess?: string;
enablePriorityBasedExecution?: boolean;
vcoreMongoEndpoint?: string;
enableAllVersionsAndDeletesChangeFeed?: boolean;
}
export interface DatabaseAccountResponseLocation {
@@ -423,7 +416,6 @@ export interface CreateCollectionParamsBase {
databaseId: string;
databaseLevelThroughput: boolean;
offerThroughput?: number;
analyticalStorageTtl?: number;
autoPilotMaxThroughput?: number;
indexingPolicy?: IndexingPolicy;
partitionKey?: PartitionKey;

View File

@@ -23,6 +23,7 @@ import {
extractErrorMessage,
formatUTCDateTime,
getAccountDetailsFromResourceId,
isIntraAccountCopy,
} from "../CopyJobUtils";
import CreateCopyJobScreensProvider from "../CreateCopyJob/Screens/CreateCopyJobScreensProvider";
import { CopyJobActions, CopyJobStatusType } from "../Enums/CopyJobEnums";
@@ -75,7 +76,6 @@ export const getCopyJobs = async (): Promise<CopyJobType[]> => {
}
copyJobsAbortController = null;
/* added a lower bound to "0" and upper bound to "100" */
const calculateCompletionPercentage = (processed: number, total: number): number => {
if (
typeof processed !== "number" ||
@@ -139,11 +139,12 @@ export const submitCreateCopyJob = async (state: CopyJobContextState, onSuccess:
const { subscriptionId, resourceGroup, accountName } = getAccountDetailsFromResourceId(
userContext.databaseAccount?.id || "",
);
const isSameAccount = isIntraAccountCopy(source?.account?.id, target?.account?.id);
const body = {
properties: {
source: {
component: "CosmosDBSql",
remoteAccountName: source?.account?.name,
...(isSameAccount ? {} : { remoteAccountName: source?.account?.name }),
databaseName: source?.databaseId,
containerName: source?.containerId,
},

View File

@@ -55,13 +55,15 @@ export default {
"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.`,
commonConfiguration: {
title: "Common configuration",
description: "Basic permissions required for copy operations",
crossAccountConfiguration: {
title: "Cross-account container copy",
description: (sourceAccount: string, destinationAccount: string) =>
`Please follow the instruction below to grant requisite permissions to copy data from "${sourceAccount}" to "${destinationAccount}".`,
},
onlineConfiguration: {
title: "Online copy configuration",
description: "Additional permissions required for online copy operations",
title: "Online container copy",
description: (accountName: string) =>
`Please follow the instructions below to enable online copy on your "${accountName}" account.`,
},
},
toggleBtn: {
@@ -129,10 +131,17 @@ export default {
},
onlineCopyEnabled: {
title: "Online copy enabled",
description: (accountName: string) => `Enable Online copy on "${accountName}".`,
description: (accountName: string) =>
`Enable online container copy by clicking the button below on your "${accountName}" account.`,
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",
validateAllVersionsAndDeletesChangeFeedSpinnerLabel:
"Validating All versions and deletes change feed mode (preview)...",
enablingAllVersionsAndDeletesChangeFeedSpinnerLabel:
"Enabling All versions and deletes change feed mode (preview)...",
enablingOnlineCopySpinnerLabel: (accountName: string) =>
`Enabling online copy on your "${accountName}" account ...`,
},
MonitorJobs: {
Columns: {

View File

@@ -1,5 +1,5 @@
import { Link, Stack, Text, Toggle } from "@fluentui/react";
import React, { useCallback } from "react";
import React from "react";
import { logError } from "../../../../../Common/Logger";
import { assignRole } from "../../../../../Utils/arm/RbacUtils";
import ContainerCopyMessages from "../../../ContainerCopyMessages";
@@ -25,7 +25,7 @@ const AddReadPermissionToDefaultIdentity: React.FC<AddReadPermissionToDefaultIde
const { copyJobState, setCopyJobState, setContextError } = useCopyJobContext();
const [readPermissionAssigned, onToggle] = useToggle(false);
const handleAddReadPermission = useCallback(async () => {
const handleAddReadPermission = async () => {
const { source, target } = copyJobState;
const selectedSourceAccount = source?.account;
try {
@@ -56,7 +56,7 @@ const AddReadPermissionToDefaultIdentity: React.FC<AddReadPermissionToDefaultIde
} finally {
setLoading(false);
}
}, [copyJobState, setCopyJobState, setContextError]);
};
return (
<Stack className="defaultManagedIdentityContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}>

View File

@@ -20,6 +20,7 @@ const validatorFn: AccountValidatorFn = (prev: DatabaseAccount, next: DatabaseAc
const OnlineCopyEnabled: React.FC = () => {
const [loading, setLoading] = React.useState(false);
const [loaderMessage, setLoaderMessage] = React.useState("");
const [showRefreshButton, setShowRefreshButton] = React.useState(false);
const intervalRef = React.useRef<NodeJS.Timeout | null>(null);
const timeoutRef = React.useRef<NodeJS.Timeout | null>(null);
@@ -75,6 +76,21 @@ const OnlineCopyEnabled: React.FC = () => {
setShowRefreshButton(false);
try {
setLoaderMessage(ContainerCopyMessages.onlineCopyEnabled.validateAllVersionsAndDeletesChangeFeedSpinnerLabel);
const sourAccountBeforeUpdate = await fetchDatabaseAccount(
sourceSubscriptionId,
sourceResourceGroup,
sourceAccountName,
);
if (!sourAccountBeforeUpdate?.properties.enableAllVersionsAndDeletesChangeFeed) {
setLoaderMessage(ContainerCopyMessages.onlineCopyEnabled.enablingAllVersionsAndDeletesChangeFeedSpinnerLabel);
await updateDatabaseAccount(sourceSubscriptionId, sourceResourceGroup, sourceAccountName, {
properties: {
enableAllVersionsAndDeletesChangeFeed: true,
},
});
}
setLoaderMessage(ContainerCopyMessages.onlineCopyEnabled.enablingOnlineCopySpinnerLabel(sourceAccountName));
await updateDatabaseAccount(sourceSubscriptionId, sourceResourceGroup, sourceAccountName, {
properties: {
enableAllVersionsAndDeletesChangeFeed: true,
@@ -120,7 +136,7 @@ const OnlineCopyEnabled: React.FC = () => {
return (
<Stack className="onlineCopyContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}>
<LoadingOverlay isLoading={loading} label={ContainerCopyMessages.popoverOverlaySpinnerLabel} />
<LoadingOverlay isLoading={loading} label={loaderMessage} />
<Stack.Item className="info-message">
{ContainerCopyMessages.onlineCopyEnabled.description(source?.account?.name || "")}&ensp;
<Link href={ContainerCopyMessages.onlineCopyEnabled.href} target="_blank" rel="noopener noreferrer">

View File

@@ -44,10 +44,9 @@ const useManagedIdentity = (
const errorMessage = error.message || "Error enabling system-assigned managed identity. Please try again later.";
logError(errorMessage, "CopyJob/useManagedIdentity.handleAddSystemIdentity");
setContextError(errorMessage);
} finally {
setLoading(false);
}
}, [copyJobState, updateIdentityFn, setCopyJobState]);
}, [copyJobState?.target?.account?.id, updateIdentityFn, setCopyJobState]);
return { loading, handleAddSystemIdentity };
};

View File

@@ -186,15 +186,20 @@ const usePermissionSections = (state: CopyJobContextState): PermissionGroupConfi
const groupsToValidate = useMemo(() => {
const isSameAccount = isIntraAccountCopy(sourceAccount.accountId, targetAccount.accountId);
const commonSections = isSameAccount ? [] : [...PERMISSION_SECTIONS_CONFIG];
const crossAccountSections = isSameAccount ? [] : [...PERMISSION_SECTIONS_CONFIG];
const groups: PermissionGroupConfig[] = [];
const sourceAccountName = state.source?.account?.name || "";
const targetAccountName = state.target?.account?.name || "";
if (commonSections.length > 0) {
if (crossAccountSections.length > 0) {
groups.push({
id: "commonConfigs",
title: ContainerCopyMessages.assignPermissions.commonConfiguration.title,
description: ContainerCopyMessages.assignPermissions.commonConfiguration.description,
sections: commonSections,
id: "crossAccountConfigs",
title: ContainerCopyMessages.assignPermissions.crossAccountConfiguration.title,
description: ContainerCopyMessages.assignPermissions.crossAccountConfiguration.description(
sourceAccountName,
targetAccountName,
),
sections: crossAccountSections,
});
}
@@ -202,7 +207,7 @@ const usePermissionSections = (state: CopyJobContextState): PermissionGroupConfi
groups.push({
id: "onlineConfigs",
title: ContainerCopyMessages.assignPermissions.onlineConfiguration.title,
description: ContainerCopyMessages.assignPermissions.onlineConfiguration.description,
description: ContainerCopyMessages.assignPermissions.onlineConfiguration.description(sourceAccountName),
sections: [...PERMISSION_SECTIONS_FOR_ONLINE_JOBS],
});
}

View File

@@ -11,25 +11,19 @@ export function useDropdownOptions(
subscriptionOptions: DropdownOptionType[];
accountOptions: DropdownOptionType[];
} {
const subscriptionOptions = React.useMemo(
() =>
subscriptions?.map((sub) => ({
key: sub.subscriptionId,
text: sub.displayName,
data: sub,
})) || [],
[subscriptions],
);
const subscriptionOptions =
subscriptions?.map((sub) => ({
key: sub.subscriptionId,
text: sub.displayName,
data: sub,
})) || [];
const accountOptions = React.useMemo(
() =>
accounts?.map((account) => ({
key: account.id,
text: account.name,
data: account,
})) || [],
[accounts],
);
const accountOptions =
accounts?.map((account) => ({
key: account.id,
text: account.name,
data: account,
})) || [];
return { subscriptionOptions, accountOptions };
}
@@ -38,45 +32,42 @@ 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) => {
if (type === "subscription") {
return {
...prevState,
source: {
...prevState.source,
subscription: data || null,
account: null,
},
};
}
if (type === "account") {
return {
...prevState,
source: {
...prevState.source,
account: data || null,
},
};
}
return prevState;
});
setValidationCache(new Map<string, boolean>());
},
[setCopyJobState, setValidationCache],
);
const handleSelectSourceAccount = (
type: "subscription" | "account",
data: (Subscription & DatabaseAccount) | undefined,
) => {
setCopyJobState((prevState: CopyJobContextState) => {
if (type === "subscription") {
return {
...prevState,
source: {
...prevState.source,
subscription: data || null,
account: null,
},
};
}
if (type === "account") {
return {
...prevState,
source: {
...prevState.source,
account: data || null,
},
};
}
return prevState;
});
setValidationCache(new Map<string, boolean>());
};
const handleMigrationTypeChange = React.useCallback(
(_ev?: React.FormEvent<HTMLElement>, checked?: boolean) => {
setCopyJobState((prevState: CopyJobContextState) => ({
...prevState,
migrationType: checked ? CopyJobMigrationType.Offline : CopyJobMigrationType.Online,
}));
setValidationCache(new Map<string, boolean>());
},
[setCopyJobState, setValidationCache],
);
const handleMigrationTypeChange = React.useCallback((_ev?: React.FormEvent<HTMLElement>, checked?: boolean) => {
setCopyJobState((prevState: CopyJobContextState) => ({
...prevState,
migrationType: checked ? CopyJobMigrationType.Offline : CopyJobMigrationType.Online,
}));
setValidationCache(new Map<string, boolean>());
}, []);
return { handleSelectSourceAccount, handleMigrationTypeChange };
}

View File

@@ -7,7 +7,7 @@ import ContainerCopyMessages from "../../../ContainerCopyMessages";
import { useCopyJobContext } from "../../../Context/CopyJobContext";
import { DatabaseContainerSection } from "./components/DatabaseContainerSection";
import { dropDownChangeHandler } from "./Events/DropDownChangeHandler";
import { useMemoizedSourceAndTargetData } from "./memoizedData";
import { useSourceAndTargetData } from "./memoizedData";
type SelectSourceAndTargetContainers = {
showAddCollectionPanel?: () => void;
@@ -16,31 +16,35 @@ type SelectSourceAndTargetContainers = {
const SelectSourceAndTargetContainers = ({ showAddCollectionPanel }: SelectSourceAndTargetContainers) => {
const { copyJobState, setCopyJobState } = useCopyJobContext();
const { source, target, sourceDbParams, sourceContainerParams, targetDbParams, targetContainerParams } =
useMemoizedSourceAndTargetData(copyJobState);
useSourceAndTargetData(copyJobState);
const sourceDatabases = useDatabases(...sourceDbParams) || [];
const sourceContainers = useDataContainers(...sourceContainerParams) || [];
const targetDatabases = useDatabases(...targetDbParams) || [];
const targetContainers = useDataContainers(...targetContainerParams) || [];
if (!source) {
return null;
}
const sourceDatabases = useDatabases(...sourceDbParams);
const sourceContainers = useDataContainers(...sourceContainerParams);
const targetDatabases = useDatabases(...targetDbParams);
const targetContainers = useDataContainers(...targetContainerParams);
const sourceDatabaseOptions = React.useMemo(
() => sourceDatabases.map((db: DatabaseModel) => ({ key: db.name, text: db.name, data: db })),
() => sourceDatabases?.map((db: DatabaseModel) => ({ key: db.name, text: db.name, data: db })) || [],
[sourceDatabases],
);
const sourceContainerOptions = React.useMemo(
() => sourceContainers.map((c: DatabaseModel) => ({ key: c.name, text: c.name, data: c })),
() => sourceContainers?.map((c: DatabaseModel) => ({ key: c.name, text: c.name, data: c })) || [],
[sourceContainers],
);
const targetDatabaseOptions = React.useMemo(
() => targetDatabases.map((db: DatabaseModel) => ({ key: db.name, text: db.name, data: db })),
() => targetDatabases?.map((db: DatabaseModel) => ({ key: db.name, text: db.name, data: db })) || [],
[targetDatabases],
);
const targetContainerOptions = React.useMemo(
() => targetContainers.map((c: DatabaseModel) => ({ key: c.name, text: c.name, data: c })),
() => targetContainers?.map((c: DatabaseModel) => ({ key: c.name, text: c.name, data: c })) || [],
[targetContainers],
);
const onDropdownChange = React.useCallback(dropDownChangeHandler(setCopyJobState), [setCopyJobState]);
const onDropdownChange = dropDownChangeHandler(setCopyJobState);
return (
<Stack className="selectSourceAndTargetContainers" tokens={{ childrenGap: 25 }}>

View File

@@ -1,8 +1,7 @@
import React from "react";
import { getAccountDetailsFromResourceId } from "../../../CopyJobUtils";
import { CopyJobContextState, DatabaseParams, DataContainerParams } from "../../../Types/CopyJobTypes";
export function useMemoizedSourceAndTargetData(copyJobState: CopyJobContextState) {
export function useSourceAndTargetData(copyJobState: CopyJobContextState) {
const { source, target } = copyJobState ?? {};
const selectedSourceAccount = source?.account;
const selectedTargetAccount = target?.account;
@@ -17,27 +16,22 @@ export function useMemoizedSourceAndTargetData(copyJobState: CopyJobContextState
accountName: targetAccountName,
} = getAccountDetailsFromResourceId(selectedTargetAccount?.id);
const sourceDbParams = React.useMemo(
() => [sourceSubscriptionId, sourceResourceGroup, sourceAccountName, "SQL"] as DatabaseParams,
[sourceSubscriptionId, sourceResourceGroup, sourceAccountName],
);
const sourceContainerParams = React.useMemo(
() =>
[sourceSubscriptionId, sourceResourceGroup, sourceAccountName, source?.databaseId, "SQL"] as DataContainerParams,
[sourceSubscriptionId, sourceResourceGroup, sourceAccountName, source?.databaseId],
);
const targetDbParams = React.useMemo(
() => [targetSubscriptionId, targetResourceGroup, targetAccountName, "SQL"] as DatabaseParams,
[targetSubscriptionId, targetResourceGroup, targetAccountName],
);
const targetContainerParams = React.useMemo(
() =>
[targetSubscriptionId, targetResourceGroup, targetAccountName, target?.databaseId, "SQL"] as DataContainerParams,
[targetSubscriptionId, targetResourceGroup, targetAccountName, target?.databaseId],
);
const sourceDbParams = [sourceSubscriptionId, sourceResourceGroup, sourceAccountName, "SQL"] as DatabaseParams;
const sourceContainerParams = [
sourceSubscriptionId,
sourceResourceGroup,
sourceAccountName,
source?.databaseId,
"SQL",
] as DataContainerParams;
const targetDbParams = [targetSubscriptionId, targetResourceGroup, targetAccountName, "SQL"] as DatabaseParams;
const targetContainerParams = [
targetSubscriptionId,
targetResourceGroup,
targetAccountName,
target?.databaseId,
"SQL",
] as DataContainerParams;
return { source, target, sourceDbParams, sourceContainerParams, targetDbParams, targetContainerParams };
}

View File

@@ -1,4 +1,5 @@
import { FontIcon, getTheme, mergeStyles, mergeStyleSets, Spinner, SpinnerSize, Stack, Text } from "@fluentui/react";
import PropTypes from "prop-types";
import React from "react";
import ContainerCopyMessages from "../../ContainerCopyMessages";
import { CopyJobStatusType } from "../../Enums/CopyJobEnums";
@@ -34,7 +35,11 @@ const iconMap: Partial<Record<CopyJobStatusType, string>> = {
[CopyJobStatusType.Completed]: "CompletedSolid",
};
const CopyJobStatusWithIcon: React.FC<{ status: CopyJobStatusType }> = ({ status }) => {
export interface CopyJobStatusWithIconProps {
status: CopyJobStatusType;
}
const CopyJobStatusWithIcon: React.FC<CopyJobStatusWithIconProps> = React.memo(({ status }) => {
const statusText = ContainerCopyMessages.MonitorJobs.Status[status] || "Unknown";
const isSpinnerStatus = [
@@ -57,6 +62,11 @@ const CopyJobStatusWithIcon: React.FC<{ status: CopyJobStatusType }> = ({ status
<Text>{statusText}</Text>
</Stack>
);
});
CopyJobStatusWithIcon.displayName = "CopyJobStatusWithIcon";
CopyJobStatusWithIcon.propTypes = {
status: PropTypes.oneOf(Object.values(CopyJobStatusType)).isRequired,
};
export default CopyJobStatusWithIcon;

View File

@@ -25,4 +25,4 @@ const CopyJobsNotFound: React.FC<CopyJobsNotFoundProps> = ({ explorer }) => {
);
};
export default CopyJobsNotFound;
export default React.memo(CopyJobsNotFound);

View File

@@ -1,9 +1,11 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable react/prop-types */
import {
ConstrainMode,
DetailsListLayoutMode,
DetailsRow,
IColumn,
IDetailsRowProps,
ScrollablePane,
ScrollbarVisibility,
ShimmeredDetailsList,
@@ -58,22 +60,19 @@ const CopyJobsList: React.FC<CopyJobsListProps> = ({ jobs, handleActionClick, pa
setStartIndex(0);
};
const columns: IColumn[] = React.useMemo(
() => getColumns(handleSort, handleActionClick, sortedColumnKey, isSortedDescending),
[handleSort, handleActionClick, sortedColumnKey, isSortedDescending],
);
const columns: IColumn[] = getColumns(handleSort, handleActionClick, sortedColumnKey, isSortedDescending);
const _handleRowClick = React.useCallback((job: CopyJobType) => {
const _handleRowClick = (job: CopyJobType) => {
openCopyJobDetailsPanel(job);
}, []);
};
const _onRenderRow = React.useCallback((props: any) => {
const _onRenderRow = (props: IDetailsRowProps) => {
return (
<div onClick={_handleRowClick.bind(null, props.item)}>
<DetailsRow {...props} styles={{ root: { cursor: "pointer" } }} />
</div>
);
}, []);
};
return (
<div style={styles.container}>

View File

@@ -4,13 +4,14 @@ import ShimmerTree, { IndentLevel } from "Common/ShimmerTree/ShimmerTree";
import Explorer from "Explorer/Explorer";
import React, { forwardRef, useEffect, useImperativeHandle } from "react";
import { getCopyJobs, updateCopyJobStatus } from "../Actions/CopyJobActions";
import { convertToCamelCase } from "../CopyJobUtils";
import { convertToCamelCase, isEqual } from "../CopyJobUtils";
import { CopyJobStatusType } from "../Enums/CopyJobEnums";
import CopyJobsNotFound from "../MonitorCopyJobs/Components/CopyJobs.NotFound";
import { CopyJobType, JobActionUpdatorType } from "../Types/CopyJobTypes";
import CopyJobsList from "./Components/CopyJobsList";
const FETCH_INTERVAL_MS = 30 * 1000;
const SHIMMER_INDENT_LEVELS: IndentLevel[] = Array(7).fill({ level: 0, width: "100%" });
interface MonitorCopyJobsProps {
explorer: Explorer;
@@ -27,8 +28,6 @@ const MonitorCopyJobs = forwardRef<MonitorCopyJobsRef, MonitorCopyJobsProps>(({
const isUpdatingRef = React.useRef(false);
const isFirstFetchRef = React.useRef(true);
const indentLevels = React.useMemo<IndentLevel[]>(() => Array(7).fill({ level: 0, width: "100%" }), []);
const fetchJobs = React.useCallback(async () => {
if (isUpdatingRef.current) {
return;
@@ -41,8 +40,7 @@ const MonitorCopyJobs = forwardRef<MonitorCopyJobsRef, MonitorCopyJobsProps>(({
const response = await getCopyJobs();
setJobs((prevJobs) => {
const isSame = JSON.stringify(prevJobs) === JSON.stringify(response);
return isSame ? prevJobs : response;
return isEqual(prevJobs, response) ? prevJobs : response;
});
} catch (error) {
setError(error.message || "Failed to load copy jobs. Please try again later.");
@@ -111,7 +109,9 @@ const MonitorCopyJobs = forwardRef<MonitorCopyJobsRef, MonitorCopyJobsProps>(({
return (
<Stack className="monitorCopyJobs flexContainer">
{loading && <ShimmerTree indentLevels={indentLevels} style={{ width: "100%", padding: "1rem 2.5rem" }} />}
{loading && (
<ShimmerTree indentLevels={SHIMMER_INDENT_LEVELS} style={{ width: "100%", padding: "1rem 2.5rem" }} />
)}
{error && (
<MessageBar messageBarType={MessageBarType.error} isMultiline={false} onDismiss={() => setError(null)}>
{error}

View File

@@ -286,6 +286,7 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
</Stack>
<TextField
id="autoscaleRUValueField"
data-test="autoscaleRUInput"
type="number"
styles={{
fieldGroup: { width: 100, height: 27, flexShrink: 0 },

View File

@@ -2144,6 +2144,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
</Stack>
<StyledTextFieldBase
ariaLabel="Container max RU/s"
data-test="autoscaleRUInput"
errorMessage=""
id="autoscaleRUValueField"
key=".0:$.$.1"
@@ -2170,6 +2171,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
>
<TextFieldBase
ariaLabel="Container max RU/s"
data-test="autoscaleRUInput"
deferredValidationTime={200}
errorMessage=""
id="autoscaleRUValueField"
@@ -2470,6 +2472,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
aria-invalid={false}
aria-label="Container max RU/s"
className="ms-TextField-field field-124"
data-test="autoscaleRUInput"
id="autoscaleRUValueField"
max="9007199254740991"
min={1000}

View File

@@ -18,7 +18,6 @@ import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
import { acquireMsalTokenForAccount } from "Utils/AuthorizationUtils";
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointUtils";
import { featureRegistered } from "Utils/FeatureRegistrationUtils";
import { update } from "Utils/arm/generatedClients/cosmos/databaseAccounts";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import * as ko from "knockout";
import React from "react";
@@ -48,7 +47,7 @@ import { stringToBlob } from "../Utils/BlobUtils";
import { isCapabilityEnabled } from "../Utils/CapabilityUtils";
import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils";
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils";
import { logConsoleError, logConsoleInfo } from "../Utils/NotificationConsoleUtils";
import { useSidePanel } from "../hooks/useSidePanel";
import { ReactTabKind, useTabs } from "../hooks/useTabs";
import "./ComponentRegisterer";
@@ -218,56 +217,6 @@ export default class Explorer {
this.refreshNotebookList();
}
public openEnableSynapseLinkDialog(): void {
const addSynapseLinkDialogProps: DialogProps = {
linkProps: {
linkText: "Learn more",
linkUrl: "https://aka.ms/cosmosdb-synapselink",
},
isModal: true,
title: `Enable Azure Synapse Link on your Cosmos DB account`,
subText: `Enable Azure Synapse Link to perform near real time analytical analytics on this account, without impacting the performance of your transactional workloads.
Azure Synapse Link brings together Cosmos Db Analytical Store and Synapse Analytics`,
primaryButtonText: "Enable Azure Synapse Link",
secondaryButtonText: "Cancel",
onPrimaryButtonClick: async () => {
const startTime = TelemetryProcessor.traceStart(Action.EnableAzureSynapseLink);
const clearInProgressMessage = logConsoleProgress(
"Enabling Azure Synapse Link for this account. This may take a few minutes before you can enable analytical store for this account.",
);
useNotebook.getState().setIsSynapseLinkUpdating(true);
useDialog.getState().closeDialog();
try {
await update(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name, {
properties: {
enableAnalyticalStorage: true,
},
});
clearInProgressMessage();
logConsoleInfo("Enabled Azure Synapse Link for this account");
TelemetryProcessor.traceSuccess(Action.EnableAzureSynapseLink, {}, startTime);
userContext.databaseAccount.properties.enableAnalyticalStorage = true;
} catch (error) {
clearInProgressMessage();
logConsoleError(`Enabling Azure Synapse Link for this account failed. ${getErrorMessage(error)}`);
TelemetryProcessor.traceFailure(Action.EnableAzureSynapseLink, {}, startTime);
} finally {
useNotebook.getState().setIsSynapseLinkUpdating(false);
}
},
onSecondaryButtonClick: () => {
useDialog.getState().closeDialog();
TelemetryProcessor.traceCancel(Action.EnableAzureSynapseLink);
},
};
useDialog.getState().openDialog(addSynapseLinkDialogProps);
TelemetryProcessor.traceStart(Action.EnableAzureSynapseLink);
}
public async openLoginForEntraIDPopUp(): Promise<void> {
if (userContext.databaseAccount.properties?.documentEndpoint) {
try {

View File

@@ -14,66 +14,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
afterEach(() => useSelectedNode.getState().setSelectedNode(undefined));
describe("Enable Azure Synapse Link Button", () => {
const enableAzureSynapseLinkBtnLabel = "Enable Azure Synapse Link";
const selectedNodeState = useSelectedNode.getState();
beforeAll(() => {
mockExplorer = {} as Explorer;
updateUserContext({
databaseAccount: {
properties: {
capabilities: [{ name: "EnableMongo" }],
},
} as DatabaseAccount,
});
});
it("Button should be visible", () => {
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const enableAzureSynapseLinkBtn = buttons.find(
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel,
);
expect(enableAzureSynapseLinkBtn).toBeDefined();
});
// TODO: Now that Tables API supports dataplane RBAC, calling createStaticCommandBarButtons will enable the
// Entra ID Login button, which causes this test to fail due to "Invalid hook call.". This seems to be
// unsupported in jest and needs to be tested with react-hooks-testing-library.
//
// it("Button should not be visible for Tables API", () => {
// updateUserContext({
// databaseAccount: {
// properties: {
// capabilities: [{ name: "EnableTable" }],
// },
// } as DatabaseAccount,
// });
//
// const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
// const enableAzureSynapseLinkBtn = buttons.find(
// (button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel,
// );
// expect(enableAzureSynapseLinkBtn).toBeUndefined();
//});
it("Button should not be visible for Cassandra API", () => {
updateUserContext({
databaseAccount: {
properties: {
capabilities: [{ name: "EnableCassandra" }],
},
} as DatabaseAccount,
});
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const enableAzureSynapseLinkBtn = buttons.find(
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel,
);
expect(enableAzureSynapseLinkBtn).toBeUndefined();
});
});
describe("Open Cassandra shell button", () => {
const openCassandraShellBtnLabel = "Open Cassandra shell";
const selectedNodeState = useSelectedNode.getState();

View File

@@ -12,10 +12,8 @@ import HostedTerminalIcon from "../../../../images/Hosted-Terminal.svg";
import OpenQueryFromDiskIcon from "../../../../images/OpenQueryFromDisk.svg";
import OpenInTabIcon from "../../../../images/open-in-tab.svg";
import SettingsIcon from "../../../../images/settings_15x15.svg";
import SynapseIcon from "../../../../images/synapse-link.svg";
import VSCodeIcon from "../../../../images/vscode.svg";
import { AuthType } from "../../../AuthType";
import * as Constants from "../../../Common/Constants";
import { Platform, configContext } from "../../../ConfigContext";
import * as ViewModels from "../../../Contracts/ViewModels";
import { userContext } from "../../../UserContext";
@@ -55,11 +53,6 @@ export function createStaticCommandBarButtons(
userContext.apiType !== "Tables" &&
userContext.apiType !== "Cassandra"
) {
const addSynapseLink = createOpenSynapseLinkDialogButton(container);
if (addSynapseLink) {
addDivider();
buttons.push(addSynapseLink);
}
if (userContext.apiType !== "Gremlin") {
const addVsCode = createOpenVsCodeDialogButton(container);
buttons.push(addVsCode);
@@ -237,33 +230,6 @@ function areScriptsSupported(): boolean {
);
}
function createOpenSynapseLinkDialogButton(container: Explorer): CommandButtonComponentProps {
if (configContext.platform === Platform.Emulator) {
return undefined;
}
if (userContext?.databaseAccount?.properties?.enableAnalyticalStorage) {
return undefined;
}
const capabilities = userContext?.databaseAccount?.properties?.capabilities || [];
if (capabilities.some((capability) => capability.name === Constants.CapabilityNames.EnableStorageAnalytics)) {
return undefined;
}
const label = "Enable Azure Synapse Link";
return {
iconSrc: SynapseIcon,
iconAlt: label,
onCommandClick: () => container.openEnableSynapseLinkDialog(),
commandButtonLabel: label,
hasPopup: false,
disabled:
useSelectedNode.getState().isQueryCopilotCollectionSelected() || useNotebook.getState().isSynapseLinkUpdating,
ariaLabel: label,
};
}
function createOpenVsCodeDialogButton(container: Explorer): CommandButtonComponentProps {
const label = "Visual Studio Code";
return {

View File

@@ -25,7 +25,6 @@ interface NotebookState {
isNotebooksEnabledForAccount: boolean;
notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo;
sparkClusterConnectionInfo: DataModels.SparkClusterConnectionInfo;
isSynapseLinkUpdating: boolean;
memoryUsageInfo: DataModels.MemoryUsageInfo;
isShellEnabled: boolean;
notebookBasePath: string;
@@ -44,7 +43,6 @@ interface NotebookState {
setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => void;
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => void;
setSparkClusterConnectionInfo: (sparkClusterConnectionInfo: DataModels.SparkClusterConnectionInfo) => void;
setIsSynapseLinkUpdating: (isSynapseLinkUpdating: boolean) => void;
setMemoryUsageInfo: (memoryUsageInfo: DataModels.MemoryUsageInfo) => void;
setIsShellEnabled: (isShellEnabled: boolean) => void;
setNotebookBasePath: (notebookBasePath: string) => void;
@@ -79,7 +77,6 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
password: undefined,
endpoints: [],
},
isSynapseLinkUpdating: false,
memoryUsageInfo: undefined,
isShellEnabled: false,
notebookBasePath: Constants.Notebook.defaultBasePath,
@@ -106,7 +103,6 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
set({ notebookServerInfo }),
setSparkClusterConnectionInfo: (sparkClusterConnectionInfo: DataModels.SparkClusterConnectionInfo) =>
set({ sparkClusterConnectionInfo }),
setIsSynapseLinkUpdating: (isSynapseLinkUpdating: boolean) => set({ isSynapseLinkUpdating }),
setMemoryUsageInfo: (memoryUsageInfo: DataModels.MemoryUsageInfo) => set({ memoryUsageInfo }),
setIsShellEnabled: (isShellEnabled: boolean) => set({ isShellEnabled }),
setNotebookBasePath: (notebookBasePath: string) => set({ notebookBasePath }),

View File

@@ -25,7 +25,6 @@ import { FullTextPoliciesComponent } from "Explorer/Controls/FullTextSeach/FullT
import { VectorEmbeddingPoliciesComponent } from "Explorer/Controls/VectorSearch/VectorEmbeddingPoliciesComponent";
import {
AllPropertiesIndexed,
AnalyticalStoreHeader,
ContainerVectorPolicyTooltipContent,
FullTextPolicyDefault,
getPartitionKey,
@@ -33,11 +32,9 @@ import {
getPartitionKeyPlaceHolder,
getPartitionKeyTooltipText,
isFreeTierAccount,
isSynapseLinkEnabled,
parseUniqueKeys,
scrollToSection,
SharedDatabaseDefault,
shouldShowAnalyticalStoreOptions,
UniqueKeysHeader,
} from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
import { useSidePanel } from "hooks/useSidePanel";
@@ -86,7 +83,6 @@ export interface AddCollectionPanelState {
enableDedicatedThroughput: boolean;
createMongoWildCardIndex: boolean;
useHashV1: boolean;
enableAnalyticalStore: boolean;
uniqueKeys: string[];
errorMessage: string;
showErrorDetails: boolean;
@@ -128,7 +124,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
createMongoWildCardIndex:
isCapabilityEnabled("EnableMongo") && !isCapabilityEnabled("EnableMongo16MBDocumentSupport"),
useHashV1: false,
enableAnalyticalStore: false,
uniqueKeys: [],
errorMessage: "",
showErrorDetails: false,
@@ -781,70 +776,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
<Separator className="panelSeparator" style={{ marginTop: -15, marginBottom: -4 }} />
)}
{shouldShowAnalyticalStoreOptions() && (
<Stack className="panelGroupSpacing" style={{ marginTop: -4 }}>
<Text className="panelTextBold" variant="small">
{AnalyticalStoreHeader()}
</Text>
<Stack horizontal verticalAlign="center">
<div role="radiogroup">
<input
className="panelRadioBtn"
checked={this.state.enableAnalyticalStore}
disabled={!isSynapseLinkEnabled()}
aria-label="Enable analytical store"
aria-checked={this.state.enableAnalyticalStore}
name="analyticalStore"
type="radio"
role="radio"
id="enableAnalyticalStoreBtn"
tabIndex={0}
onChange={this.onEnableAnalyticalStoreRadioBtnChange.bind(this)}
/>
<span className="panelRadioBtnLabel">On</span>
<input
className="panelRadioBtn"
checked={!this.state.enableAnalyticalStore}
disabled={!isSynapseLinkEnabled()}
aria-label="Disable analytical store"
aria-checked={!this.state.enableAnalyticalStore}
name="analyticalStore"
type="radio"
role="radio"
id="disableAnalyticalStoreBtn"
tabIndex={0}
onChange={this.onDisableAnalyticalStoreRadioBtnChange.bind(this)}
/>
<span className="panelRadioBtnLabel">Off</span>
</div>
</Stack>
{!isSynapseLinkEnabled() && (
<Stack className="panelGroupSpacing">
<Text variant="small">
Azure Synapse Link is required for creating an analytical store{" "}
{getCollectionName().toLocaleLowerCase()}. Enable Synapse Link for this Cosmos DB account. <br />
<Link
href="https://aka.ms/cosmosdb-synapselink"
target="_blank"
aria-label={Constants.ariaLabelForLearnMoreLink.AzureSynapseLink}
className="capacitycalculator-link"
>
Learn more
</Link>
</Text>
<DefaultButton
text="Enable"
onClick={() => this.props.explorer.openEnableSynapseLinkDialog()}
style={{ height: 27, width: 80 }}
styles={{ label: { fontSize: 12 } }}
/>
</Stack>
)}
</Stack>
)}
{this.shouldShowVectorSearchParameters() && (
<Stack>
<CollapsibleSectionComponent
@@ -1053,22 +984,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
}
}
private onEnableAnalyticalStoreRadioBtnChange(event: React.ChangeEvent<HTMLInputElement>): void {
if (event.target.checked && !this.state.enableAnalyticalStore) {
this.setState({
enableAnalyticalStore: true,
});
}
}
private onDisableAnalyticalStoreRadioBtnChange(event: React.ChangeEvent<HTMLInputElement>): void {
if (event.target.checked && this.state.enableAnalyticalStore) {
this.setState({
enableAnalyticalStore: false,
});
}
}
private onTurnOnIndexing(event: React.ChangeEvent<HTMLInputElement>): void {
if (event.target.checked && !this.state.enableIndexing) {
this.setState({
@@ -1243,25 +1158,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
return true;
}
private getAnalyticalStorageTtl(): number {
if (!isSynapseLinkEnabled()) {
return undefined;
}
if (!shouldShowAnalyticalStoreOptions()) {
return undefined;
}
if (this.state.enableAnalyticalStore) {
// TODO: always default to 90 days once the backend hotfix is deployed
return userContext.features.ttl90Days
? Constants.AnalyticalStorageTtl.Days90
: Constants.AnalyticalStorageTtl.Infinite;
}
return Constants.AnalyticalStorageTtl.Disabled;
}
private getSampleDBName(): string {
const existingSampleDBs = useDatabases
.getState()
@@ -1385,7 +1281,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
databaseLevelThroughput,
offerThroughput,
autoPilotMaxThroughput,
analyticalStorageTtl: this.getAnalyticalStorageTtl(),
indexingPolicy,
partitionKey,
uniqueKeyPolicy,

View File

@@ -1,9 +1,6 @@
import { DirectionalHint, Icon, Link, Stack, Text, TooltipHost } from "@fluentui/react";
import * as Constants from "Common/Constants";
import { configContext, Platform } from "ConfigContext";
import * as DataModels from "Contracts/DataModels";
import { getFullTextLanguageOptions } from "Explorer/Controls/FullTextSeach/FullTextPoliciesComponent";
import { isFabricNative } from "Platform/Fabric/FabricUtil";
import React from "react";
import { userContext } from "UserContext";
@@ -84,70 +81,6 @@ export function UniqueKeysHeader(): JSX.Element {
);
}
export function shouldShowAnalyticalStoreOptions(): boolean {
if (isFabricNative() || configContext.platform === Platform.Emulator) {
return false;
}
switch (userContext.apiType) {
case "SQL":
case "Mongo":
return true;
default:
return false;
}
}
export function AnalyticalStoreHeader(): JSX.Element {
const tooltipContent =
"Enable analytical store capability to perform near real-time analytics on your operational data, without impacting the performance of transactional workloads.";
return (
<Stack horizontal style={{ marginBottom: -2 }}>
<Text className="panelTextBold" variant="small">
Analytical Store
</Text>
<TooltipHost directionalHint={DirectionalHint.bottomLeftEdge} content={tooltipContent}>
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} ariaLabel={tooltipContent} />
</TooltipHost>
</Stack>
);
}
export function AnalyticalStorageContent(): JSX.Element {
return (
<Text variant="small">
Enable analytical store capability to perform near real-time analytics on your operational data, without impacting
the performance of transactional workloads.{" "}
<Link
aria-label={Constants.ariaLabelForLearnMoreLink.AnalyticalStore}
target="_blank"
href="https://aka.ms/analytical-store-overview"
>
Learn more
</Link>
</Text>
);
}
export function isSynapseLinkEnabled(): boolean {
if (!userContext.databaseAccount) {
return false;
}
const { properties } = userContext.databaseAccount;
if (!properties) {
return false;
}
if (properties.enableAnalyticalStorage) {
return true;
}
return properties.capabilities?.some(
(capability) => capability.name === Constants.CapabilityNames.EnableStorageAnalytics,
);
}
export function scrollToSection(id: string): void {
document.getElementById(id)?.scrollIntoView();
}

View File

@@ -367,129 +367,6 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
}
}
/>
<Stack
className="panelGroupSpacing"
style={
{
"marginTop": -4,
}
}
>
<Text
className="panelTextBold"
variant="small"
>
<Stack
horizontal={true}
style={
{
"marginBottom": -2,
}
}
>
<Text
className="panelTextBold"
variant="small"
>
Analytical Store
</Text>
<StyledTooltipHostBase
content="Enable analytical store capability to perform near real-time analytics on your operational data, without impacting the performance of transactional workloads."
directionalHint={4}
>
<Icon
ariaLabel="Enable analytical store capability to perform near real-time analytics on your operational data, without impacting the performance of transactional workloads."
className="panelInfoIcon"
iconName="Info"
tabIndex={0}
/>
</StyledTooltipHostBase>
</Stack>
</Text>
<Stack
horizontal={true}
verticalAlign="center"
>
<div
role="radiogroup"
>
<input
aria-checked={false}
aria-label="Enable analytical store"
checked={false}
className="panelRadioBtn"
disabled={true}
id="enableAnalyticalStoreBtn"
name="analyticalStore"
onChange={[Function]}
role="radio"
tabIndex={0}
type="radio"
/>
<span
className="panelRadioBtnLabel"
>
On
</span>
<input
aria-checked={true}
aria-label="Disable analytical store"
checked={true}
className="panelRadioBtn"
disabled={true}
id="disableAnalyticalStoreBtn"
name="analyticalStore"
onChange={[Function]}
role="radio"
tabIndex={0}
type="radio"
/>
<span
className="panelRadioBtnLabel"
>
Off
</span>
</div>
</Stack>
<Stack
className="panelGroupSpacing"
>
<Text
variant="small"
>
Azure Synapse Link is required for creating an analytical store
container
. Enable Synapse Link for this Cosmos DB account.
<br />
<StyledLinkBase
aria-label="Learn more about Azure Synapse Link."
className="capacitycalculator-link"
href="https://aka.ms/cosmosdb-synapselink"
target="_blank"
>
Learn more
</StyledLinkBase>
</Text>
<CustomizedDefaultButton
onClick={[Function]}
style={
{
"height": 27,
"width": 80,
}
}
styles={
{
"label": {
"fontSize": 12,
},
}
}
text="Enable"
/>
</Stack>
</Stack>
<Stack>
<CollapsibleSectionComponent
isExpandedByDefault={false}

View File

@@ -21,16 +21,13 @@ import {
AllPropertiesIndexed,
FullTextPolicyDefault,
getPartitionKey,
isSynapseLinkEnabled,
scrollToSection,
shouldShowAnalyticalStoreOptions,
} from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
import {
chooseSourceContainerStyle,
chooseSourceContainerStyles,
} from "Explorer/Panes/AddGlobalSecondaryIndexPanel/AddGlobalSecondaryIndexPanelStyles";
import { AdvancedComponent } from "Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/AdvancedComponent";
import { AnalyticalStoreComponent } from "Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/AnalyticalStoreComponent";
import { FullTextSearchComponent } from "Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/FullTextSearchComponent";
import { PartitionKeyComponent } from "Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/PartitionKeyComponent";
import { ThroughputComponent } from "Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/ThroughputComponent";
@@ -64,7 +61,6 @@ export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanel
const [useHashV1, setUseHashV1] = useState<boolean>();
const [enableDedicatedThroughput, setEnabledDedicatedThroughput] = useState<boolean>();
const [isThroughputCapExceeded, setIsThroughputCapExceeded] = useState<boolean>();
const [enableAnalyticalStore, setEnableAnalyticalStore] = useState<boolean>();
const [vectorEmbeddingPolicy, setVectorEmbeddingPolicy] = useState<VectorEmbedding[]>([]);
const [vectorIndexingPolicy, setVectorIndexingPolicy] = useState<VectorIndex[]>([]);
const [vectorPolicyValidated, setVectorPolicyValidated] = useState<boolean>(true);
@@ -142,25 +138,6 @@ export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanel
return isVectorSearchEnabled() && (isServerlessAccount() || showCollectionThroughputInput());
};
const getAnalyticalStorageTtl = (): number => {
if (!isSynapseLinkEnabled()) {
return undefined;
}
if (!shouldShowAnalyticalStoreOptions()) {
return undefined;
}
if (enableAnalyticalStore) {
// TODO: always default to 90 days once the backend hotfix is deployed
return userContext.features.ttl90Days
? Constants.AnalyticalStorageTtl.Days90
: Constants.AnalyticalStorageTtl.Infinite;
}
return Constants.AnalyticalStorageTtl.Disabled;
};
const validateInputs = (): boolean => {
if (!selectedSourceContainer) {
setErrorMessage("Please select a source container");
@@ -257,7 +234,6 @@ export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanel
...(!databaseLevelThroughput && {
autoPilotMaxThroughput: globalSecondaryIndexThroughput,
}),
analyticalStorageTtl: getAnalyticalStorageTtl(),
indexingPolicy: indexingPolicy,
partitionKey: partitionKeyPaths,
vectorEmbeddingPolicy: vectorEmbeddingPolicyFinal,
@@ -369,9 +345,7 @@ export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanel
isCostAknowledgedOnChange,
}}
/>
{shouldShowAnalyticalStoreOptions() && (
<AnalyticalStoreComponent {...{ explorer, enableAnalyticalStore, setEnableAnalyticalStore }} />
)}
{showVectorSearchParameters() && (
<VectorSearchComponent
{...{

View File

@@ -1,99 +0,0 @@
import { DefaultButton, Link, Stack, Text } from "@fluentui/react";
import * as Constants from "Common/Constants";
import Explorer from "Explorer/Explorer";
import {
AnalyticalStorageContent,
isSynapseLinkEnabled,
} from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
import React from "react";
import { getCollectionName } from "Utils/APITypeUtils";
export interface AnalyticalStoreComponentProps {
explorer: Explorer;
enableAnalyticalStore: boolean;
setEnableAnalyticalStore: React.Dispatch<React.SetStateAction<boolean>>;
}
export const AnalyticalStoreComponent = (props: AnalyticalStoreComponentProps): JSX.Element => {
const { explorer, enableAnalyticalStore, setEnableAnalyticalStore } = props;
const onEnableAnalyticalStoreRadioButtonChange = (checked: boolean): void => {
if (checked && !enableAnalyticalStore) {
setEnableAnalyticalStore(true);
}
};
const onDisableAnalyticalStoreRadioButtonnChange = (checked: boolean): void => {
if (checked && enableAnalyticalStore) {
setEnableAnalyticalStore(false);
}
};
return (
<Stack className="panelGroupSpacing">
<Text className="panelTextBold" variant="small">
{AnalyticalStorageContent()}
</Text>
<Stack horizontal verticalAlign="center">
<div role="radiogroup">
<input
className="panelRadioBtn"
checked={enableAnalyticalStore}
disabled={!isSynapseLinkEnabled()}
aria-label="Enable analytical store"
aria-checked={enableAnalyticalStore}
name="analyticalStore"
type="radio"
role="radio"
id="enableAnalyticalStoreBtn"
tabIndex={0}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
onEnableAnalyticalStoreRadioButtonChange(event.target.checked);
}}
/>
<span className="panelRadioBtnLabel">On</span>
<input
className="panelRadioBtn"
checked={!enableAnalyticalStore}
disabled={!isSynapseLinkEnabled()}
aria-label="Disable analytical store"
aria-checked={!enableAnalyticalStore}
name="analyticalStore"
type="radio"
role="radio"
id="disableAnalyticalStoreBtn"
tabIndex={0}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
onDisableAnalyticalStoreRadioButtonnChange(event.target.checked);
}}
/>
<span className="panelRadioBtnLabel">Off</span>
</div>
</Stack>
{!isSynapseLinkEnabled() && (
<Stack className="panelGroupSpacing">
<Text variant="small">
Azure Synapse Link is required for creating an analytical store {getCollectionName().toLocaleLowerCase()}.
Enable Synapse Link for this Cosmos DB account.{" "}
<Link
href="https://aka.ms/cosmosdb-synapselink"
target="_blank"
aria-label={Constants.ariaLabelForLearnMoreLink.AzureSynapseLink}
className="capacitycalculator-link"
>
Learn more
</Link>
</Text>
<DefaultButton
text="Enable"
onClick={() => explorer.openEnableSynapseLinkDialog()}
style={{ height: 27, width: 80 }}
styles={{ label: { fontSize: 12 } }}
/>
</Stack>
)}
</Stack>
);
};

View File

@@ -142,36 +142,6 @@ exports[`AddGlobalSecondaryIndexPanel render default panel 1`] = `
setIsThroughputCapExceeded={[Function]}
showCollectionThroughputInput={[Function]}
/>
<AnalyticalStoreComponent
explorer={
Explorer {
"_isInitializingNotebooks": false,
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function],
"phoenixClient": PhoenixClient {
"armResourceId": undefined,
"retryOptions": {
"maxTimeout": 5000,
"minTimeout": 5000,
"retries": 3,
},
},
"provideFeedbackEmail": [Function],
"queriesClient": QueriesClient {
"container": [Circular],
},
"refreshNotebookList": [Function],
"resourceTree": ResourceTreeAdapter {
"container": [Circular],
"copyNotebook": [Function],
"parameters": [Function],
},
}
}
setEnableAnalyticalStore={[Function]}
/>
<FullTextSearchComponent
fullTextPolicy={
{

View File

@@ -142,7 +142,7 @@ const FabricHomeScreenButton: React.FC<FabricHomeScreenButtonProps & { className
return (
<div role="button" className={`${styles.buttonContainer} ${className}`} onClick={onClick} tabIndex={0}>
<div className={styles.buttonUpperPart}>{icon}</div>
<div aria-label={title} className={styles.buttonLowerPart}>
<div aria-label={`${title} ${description}`} className={styles.buttonLowerPart}>
<div>{title}</div>
<div>{description}</div>
</div>

View File

@@ -60,7 +60,6 @@ export enum Action {
LibraryManage,
ClusterLibraryManage,
ModifyOptionForThroughputWithSharedDatabase,
EnableAzureSynapseLink,
CreateNewNotebook,
OpenSampleNotebook,
ExecuteCell,

View File

@@ -2,7 +2,7 @@ import { DatabaseAccount } from "Contracts/DataModels";
import { userContext } from "UserContext";
import { buildArmUrl } from "Utils/arm/armUtils";
const apiVersion = "2025-04-15";
const apiVersion = "2025-05-01-preview";
export type FetchAccountDetailsParams = {
subscriptionId: string;
resourceGroupName: string;

View File

@@ -1,6 +1,6 @@
import { expect, test } from "@playwright/test";
import { DataExplorer, TestAccount, generateUniqueName } from "../fx";
import { DataExplorer, TEST_AUTOSCALE_THROUGHPUT_RU, TestAccount, generateUniqueName } from "../fx";
test("Cassandra keyspace and table CRUD", async ({ page }) => {
const keyspaceId = generateUniqueName("db");
@@ -14,6 +14,7 @@ test("Cassandra keyspace and table CRUD", async ({ page }) => {
async (panel, okButton) => {
await panel.getByPlaceholder("Type a new keyspace id").fill(keyspaceId);
await panel.getByPlaceholder("Enter table Id").fill(tableId);
await panel.getByTestId("autoscaleRUInput").fill(TEST_AUTOSCALE_THROUGHPUT_RU.toString());
await okButton.click();
},
{ closeTimeout: 5 * 60 * 1000 },

View File

@@ -54,6 +54,7 @@ export const defaultAccounts: Record<TestAccount, string> = {
export const resourceGroupName = process.env.DE_TEST_RESOURCE_GROUP ?? "de-e2e-tests";
export const subscriptionId = process.env.DE_TEST_SUBSCRIPTION_ID ?? "69e02f2d-f059-4409-9eac-97e8a276ae2c";
export const TEST_AUTOSCALE_THROUGHPUT_RU = 1000;
function tryGetStandardName(accountType: TestAccount) {
if (process.env.DE_TEST_ACCOUNT_PREFIX) {

View File

@@ -1,6 +1,6 @@
import { expect, test } from "@playwright/test";
import { DataExplorer, TestAccount, generateUniqueName } from "../fx";
import { DataExplorer, TEST_AUTOSCALE_THROUGHPUT_RU, TestAccount, generateUniqueName } from "../fx";
test("Gremlin graph CRUD", async ({ page }) => {
const databaseId = generateUniqueName("db");
@@ -16,6 +16,7 @@ test("Gremlin graph CRUD", async ({ page }) => {
await panel.getByPlaceholder("Type a new database id").fill(databaseId);
await panel.getByRole("textbox", { name: "Graph id, Example Graph1" }).fill(graphId);
await panel.getByRole("textbox", { name: "Partition key" }).fill("/pk");
await panel.getByTestId("autoscaleRUInput").fill(TEST_AUTOSCALE_THROUGHPUT_RU.toString());
await okButton.click();
},
{ closeTimeout: 5 * 60 * 1000 },

View File

@@ -1,6 +1,6 @@
import { expect, test } from "@playwright/test";
import { DataExplorer, TestAccount, generateUniqueName } from "../fx";
import { DataExplorer, TEST_AUTOSCALE_THROUGHPUT_RU, TestAccount, generateUniqueName } from "../fx";
(
[
@@ -21,6 +21,7 @@ import { DataExplorer, TestAccount, generateUniqueName } from "../fx";
await panel.getByPlaceholder("Type a new database id").fill(databaseId);
await panel.getByRole("textbox", { name: "Collection id, Example Collection1" }).fill(collectionId);
await panel.getByRole("textbox", { name: "Shard key" }).fill("pk");
await panel.getByTestId("autoscaleRUInput").fill(TEST_AUTOSCALE_THROUGHPUT_RU.toString());
await okButton.click();
},
{ closeTimeout: 5 * 60 * 1000 },

View File

@@ -1,6 +1,6 @@
import { expect, test } from "@playwright/test";
import { DataExplorer, TestAccount, generateUniqueName } from "../fx";
import { DataExplorer, TEST_AUTOSCALE_THROUGHPUT_RU, TestAccount, generateUniqueName } from "../fx";
test("SQL database and container CRUD", async ({ page }) => {
const databaseId = generateUniqueName("db");
@@ -15,6 +15,7 @@ test("SQL database and container CRUD", async ({ page }) => {
await panel.getByPlaceholder("Type a new database id").fill(databaseId);
await panel.getByRole("textbox", { name: "Container id, Example Container1" }).fill(containerId);
await panel.getByRole("textbox", { name: "Partition key" }).fill("/pk");
await panel.getByTestId("autoscaleRUInput").fill(TEST_AUTOSCALE_THROUGHPUT_RU.toString());
await okButton.click();
},
{ closeTimeout: 5 * 60 * 1000 },

View File

@@ -1,6 +1,6 @@
import { expect, test } from "@playwright/test";
import { DataExplorer, TestAccount, generateUniqueName } from "../fx";
import { DataExplorer, TEST_AUTOSCALE_THROUGHPUT_RU, TestAccount, generateUniqueName } from "../fx";
test("Tables CRUD", async ({ page }) => {
const tableId = generateUniqueName("table"); // A unique table name IS needed because the database is shared when using Table Storage.
@@ -12,7 +12,7 @@ test("Tables CRUD", async ({ page }) => {
"New Table",
async (panel, okButton) => {
await panel.getByRole("textbox", { name: "Table id, Example Table1" }).fill(tableId);
await panel.getByLabel("Table Max RU/s").fill("1000");
await panel.getByTestId("autoscaleRUInput").fill(TEST_AUTOSCALE_THROUGHPUT_RU.toString());
await okButton.click();
},
{ closeTimeout: 5 * 60 * 1000 },