Fabric native: use SDK not ARM for update offers/collections. Enable Delete Container context menu item in resource tree (#2069)

* For all control plane operations, do not use ARM for Fabric. Enable "delete container" for fabric native.

* Fix unit test

* Fix tre note tests with proper fabric config. Add new fabric non-readonly test.
This commit is contained in:
Laurent Nguyen 2025-03-07 07:10:45 +01:00 committed by GitHub
parent 083bccfda9
commit 1529303107
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 232 additions and 35 deletions

View File

@ -1,4 +1,5 @@
import { ContainerRequest, ContainerResponse, DatabaseRequest, DatabaseResponse, RequestOptions } from "@azure/cosmos"; import { ContainerRequest, ContainerResponse, DatabaseRequest, DatabaseResponse, RequestOptions } from "@azure/cosmos";
import { isFabricNative } from "Platform/Fabric/FabricUtil";
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import { useDatabases } from "../../Explorer/useDatabases"; import { useDatabases } from "../../Explorer/useDatabases";
@ -24,7 +25,7 @@ export const createCollection = async (params: DataModels.CreateCollectionParams
); );
try { try {
let collection: DataModels.Collection; let collection: DataModels.Collection;
if (userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations) { if (!isFabricNative() && userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations) {
if (params.createNewDatabase) { if (params.createNewDatabase) {
const createDatabaseParams: DataModels.CreateDatabaseParams = { const createDatabaseParams: DataModels.CreateDatabaseParams = {
autoPilotMaxThroughput: params.autoPilotMaxThroughput, autoPilotMaxThroughput: params.autoPilotMaxThroughput,

View File

@ -1,3 +1,4 @@
import { isFabric } from "Platform/Fabric/FabricUtil";
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import { deleteCassandraTable } from "../../Utils/arm/generatedClients/cosmos/cassandraResources"; import { deleteCassandraTable } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
@ -12,7 +13,7 @@ import { handleError } from "../ErrorHandlingUtils";
export async function deleteCollection(databaseId: string, collectionId: string): Promise<void> { export async function deleteCollection(databaseId: string, collectionId: string): Promise<void> {
const clearMessage = logConsoleProgress(`Deleting container ${collectionId}`); const clearMessage = logConsoleProgress(`Deleting container ${collectionId}`);
try { try {
if (userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations) { if (userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations && !isFabric()) {
await deleteCollectionWithARM(databaseId, collectionId); await deleteCollectionWithARM(databaseId, collectionId);
} else { } else {
await client().database(databaseId).container(collectionId).delete(); await client().database(databaseId).container(collectionId).delete();

View File

@ -1,4 +1,4 @@
import { isFabric, isFabricMirroredKey } from "Platform/Fabric/FabricUtil"; import { isFabric, isFabricMirroredKey, isFabricNative } from "Platform/Fabric/FabricUtil";
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
import { Offer, ReadDatabaseOfferParams } from "../../Contracts/DataModels"; import { Offer, ReadDatabaseOfferParams } from "../../Contracts/DataModels";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
@ -11,8 +11,9 @@ import { handleError } from "../ErrorHandlingUtils";
import { readOfferWithSDK } from "./readOfferWithSDK"; import { readOfferWithSDK } from "./readOfferWithSDK";
export const readDatabaseOffer = async (params: ReadDatabaseOfferParams): Promise<Offer> => { export const readDatabaseOffer = async (params: ReadDatabaseOfferParams): Promise<Offer> => {
if (isFabricMirroredKey()) { if (isFabricMirroredKey() || isFabricNative()) {
// TODO This works, but is very slow, because it requests the token, so we skip for now // For Fabric Mirroring, it is slow, because it requests the token and we don't need it.
// For Fabric Native, it is not supported.
console.error("Skiping readDatabaseOffer for Fabric"); console.error("Skiping readDatabaseOffer for Fabric");
return undefined; return undefined;
} }

View File

@ -1,4 +1,5 @@
import { ContainerDefinition, RequestOptions } from "@azure/cosmos"; import { ContainerDefinition, RequestOptions } from "@azure/cosmos";
import { isFabric } from "Platform/Fabric/FabricUtil";
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
import { Collection } from "../../Contracts/DataModels"; import { Collection } from "../../Contracts/DataModels";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
@ -36,7 +37,8 @@ export async function updateCollection(
if ( if (
userContext.authType === AuthType.AAD && userContext.authType === AuthType.AAD &&
!userContext.features.enableSDKoperations && !userContext.features.enableSDKoperations &&
userContext.apiType !== "Tables" userContext.apiType !== "Tables" &&
!isFabric()
) { ) {
collection = await updateCollectionWithARM(databaseId, collectionId, newCollection); collection = await updateCollectionWithARM(databaseId, collectionId, newCollection);
} else { } else {

View File

@ -1,4 +1,5 @@
import { OfferDefinition, RequestOptions } from "@azure/cosmos"; import { OfferDefinition, RequestOptions } from "@azure/cosmos";
import { isFabric } from "Platform/Fabric/FabricUtil";
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
import { Offer, SDKOfferDefinition, ThroughputBucket, UpdateOfferParams } from "../../Contracts/DataModels"; import { Offer, SDKOfferDefinition, ThroughputBucket, UpdateOfferParams } from "../../Contracts/DataModels";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
@ -56,7 +57,7 @@ export const updateOffer = async (params: UpdateOfferParams): Promise<Offer> =>
const clearMessage = logConsoleProgress(`Updating offer for ${offerResourceText}`); const clearMessage = logConsoleProgress(`Updating offer for ${offerResourceText}`);
try { try {
if (userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations) { if (userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations && !isFabric()) {
if (params.collectionId) { if (params.collectionId) {
updatedOffer = await updateCollectionOfferWithARM(params); updatedOffer = await updateCollectionOfferWithARM(params);
} else if (userContext.apiType === "Tables") { } else if (userContext.apiType === "Tables") {

View File

@ -146,7 +146,7 @@ export const createCollectionContextMenuButton = (
}); });
} }
if (configContext.platform !== Platform.Fabric) { if (!isFabric() || (isFabric() && !userContext.fabricContext?.isReadOnly)) {
items.push({ items.push({
iconSrc: DeleteCollectionIcon, iconSrc: DeleteCollectionIcon,
onClick: (lastFocusedElement?: React.RefObject<HTMLElement>) => { onClick: (lastFocusedElement?: React.RefObject<HTMLElement>) => {

View File

@ -740,7 +740,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
] ]
`; `;
exports[`createDatabaseTreeNodes generates the correct tree structure for the SQL API, on Fabric 1`] = ` exports[`createDatabaseTreeNodes generates the correct tree structure for the SQL API, on Fabric non read-only 1`] = `
[ [
{ {
"children": [ "children": [
@ -753,6 +753,12 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
"label": "New SQL Query", "label": "New SQL Query",
"onClick": [Function], "onClick": [Function],
}, },
{
"iconSrc": {},
"label": "Delete Container",
"onClick": [Function],
"styleClass": "deleteCollectionMenuItem",
},
], ],
"iconSrc": <DocumentMultipleRegular "iconSrc": <DocumentMultipleRegular
fontSize={16} fontSize={16}
@ -774,6 +780,12 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
"label": "New SQL Query", "label": "New SQL Query",
"onClick": [Function], "onClick": [Function],
}, },
{
"iconSrc": {},
"label": "Delete Container",
"onClick": [Function],
"styleClass": "deleteCollectionMenuItem",
},
], ],
"iconSrc": <DocumentMultipleRegular "iconSrc": <DocumentMultipleRegular
fontSize={16} fontSize={16}
@ -822,6 +834,12 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
"label": "New SQL Query", "label": "New SQL Query",
"onClick": [Function], "onClick": [Function],
}, },
{
"iconSrc": {},
"label": "Delete Container",
"onClick": [Function],
"styleClass": "deleteCollectionMenuItem",
},
], ],
"iconSrc": <DocumentMultipleRegular "iconSrc": <DocumentMultipleRegular
fontSize={16} fontSize={16}
@ -870,6 +888,12 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
"label": "New SQL Query", "label": "New SQL Query",
"onClick": [Function], "onClick": [Function],
}, },
{
"iconSrc": {},
"label": "Delete Container",
"onClick": [Function],
"styleClass": "deleteCollectionMenuItem",
},
], ],
"iconSrc": <DocumentMultipleRegular "iconSrc": <DocumentMultipleRegular
fontSize={16} fontSize={16}
@ -915,6 +939,145 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
] ]
`; `;
exports[`createDatabaseTreeNodes generates the correct tree structure for the SQL API, on Fabric read-only 1`] = `
[
{
"children": [
{
"children": undefined,
"className": "collectionNode",
"contextMenu": [
{
"iconSrc": {},
"label": "New SQL Query",
"onClick": [Function],
},
],
"iconSrc": <DocumentMultipleRegular
fontSize={16}
/>,
"isExpanded": true,
"isSelected": [Function],
"label": "standardCollection",
"onClick": [Function],
"onCollapsed": [Function],
"onContextMenuOpen": [Function],
"onExpanded": [Function],
},
{
"children": undefined,
"className": "collectionNode",
"contextMenu": [
{
"iconSrc": {},
"label": "New SQL Query",
"onClick": [Function],
},
],
"iconSrc": <DocumentMultipleRegular
fontSize={16}
/>,
"isExpanded": true,
"isSelected": [Function],
"label": "conflictsCollection",
"onClick": [Function],
"onCollapsed": [Function],
"onContextMenuOpen": [Function],
"onExpanded": [Function],
},
],
"className": "databaseNode",
"contextMenu": undefined,
"iconSrc": <DatabaseRegular
fontSize={16}
/>,
"isExpanded": true,
"isSelected": [Function],
"label": "standardDb",
"onCollapsed": [Function],
"onContextMenuOpen": [Function],
"onExpanded": [Function],
},
{
"children": [
{
"children": undefined,
"className": "collectionNode",
"contextMenu": [
{
"iconSrc": {},
"label": "New SQL Query",
"onClick": [Function],
},
],
"iconSrc": <DocumentMultipleRegular
fontSize={16}
/>,
"isExpanded": true,
"isSelected": [Function],
"label": "sampleItemsCollection",
"onClick": [Function],
"onCollapsed": [Function],
"onContextMenuOpen": [Function],
"onExpanded": [Function],
},
],
"className": "databaseNode",
"contextMenu": undefined,
"iconSrc": <DatabaseRegular
fontSize={16}
/>,
"isExpanded": true,
"isSelected": [Function],
"label": "sharedDatabase",
"onCollapsed": [Function],
"onContextMenuOpen": [Function],
"onExpanded": [Function],
},
{
"children": [
{
"children": undefined,
"className": "collectionNode",
"contextMenu": [
{
"iconSrc": {},
"label": "New SQL Query",
"onClick": [Function],
},
],
"iconSrc": <DocumentMultipleRegular
fontSize={16}
/>,
"isExpanded": true,
"isSelected": [Function],
"label": "schemaCollection",
"onClick": [Function],
"onCollapsed": [Function],
"onContextMenuOpen": [Function],
"onExpanded": [Function],
},
{
"className": "loadMoreNode",
"label": "load more",
"onClick": [Function],
},
],
"className": "databaseNode",
"contextMenu": undefined,
"iconSrc": <DatabaseRegular
fontSize={16}
/>,
"isExpanded": true,
"isSelected": [Function],
"label": "giganticDatabase",
"onCollapsed": [Function],
"onContextMenuOpen": [Function],
"onExpanded": [Function],
},
]
`;
exports[`createDatabaseTreeNodes generates the correct tree structure for the SQL API, on Portal 1`] = ` exports[`createDatabaseTreeNodes generates the correct tree structure for the SQL API, on Portal 1`] = `
[ [
{ {
@ -972,7 +1135,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
}, },
], ],
"isSelected": [Function], "isSelected": [Function],
"label": "mockSproc3", "label": "mockSproc4",
"onClick": [Function], "onClick": [Function],
}, },
], ],
@ -990,7 +1153,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
}, },
], ],
"isSelected": [Function], "isSelected": [Function],
"label": "mockUdf3", "label": "mockUdf4",
"onClick": [Function], "onClick": [Function],
}, },
], ],
@ -1008,7 +1171,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
}, },
], ],
"isSelected": [Function], "isSelected": [Function],
"label": "mockTrigger3", "label": "mockTrigger4",
"onClick": [Function], "onClick": [Function],
}, },
], ],

View File

@ -17,7 +17,7 @@ import {
} from "Explorer/Tree/treeNodeUtil"; } from "Explorer/Tree/treeNodeUtil";
import { useDatabases } from "Explorer/useDatabases"; import { useDatabases } from "Explorer/useDatabases";
import { useSelectedNode } from "Explorer/useSelectedNode"; import { useSelectedNode } from "Explorer/useSelectedNode";
import { FabricContext, updateUserContext } from "UserContext"; import { FabricContext, updateUserContext, UserContext } from "UserContext";
import PromiseSource from "Utils/PromiseSource"; import PromiseSource from "Utils/PromiseSource";
import { useSidePanel } from "hooks/useSidePanel"; import { useSidePanel } from "hooks/useSidePanel";
import { useTabs } from "hooks/useTabs"; import { useTabs } from "hooks/useTabs";
@ -361,9 +361,30 @@ describe("createDatabaseTreeNodes", () => {
}); });
}); });
it.each<[string, Platform, boolean, Partial<DataModels.DatabaseAccountExtendedProperties>]>([ it.each<[string, Platform, boolean, Partial<DataModels.DatabaseAccountExtendedProperties>, Partial<UserContext>]>([
["the SQL API, on Fabric", Platform.Fabric, false, { capabilities: [], enableMultipleWriteLocations: true }], [
["the SQL API, on Portal", Platform.Portal, false, { capabilities: [], enableMultipleWriteLocations: true }], "the SQL API, on Fabric read-only",
Platform.Fabric,
false,
{ capabilities: [], enableMultipleWriteLocations: true },
{ fabricContext: { isReadOnly: true } as FabricContext<CosmosDbArtifactType> },
],
[
"the SQL API, on Fabric non read-only",
Platform.Fabric,
false,
{ capabilities: [], enableMultipleWriteLocations: true },
{ fabricContext: { isReadOnly: false } as FabricContext<CosmosDbArtifactType> },
],
[
"the SQL API, on Portal",
Platform.Portal,
false,
{ capabilities: [], enableMultipleWriteLocations: true },
{
fabricContext: undefined,
},
],
[ [
"the Cassandra API, serverless, on Hosted", "the Cassandra API, serverless, on Hosted",
Platform.Hosted, Platform.Hosted,
@ -374,6 +395,7 @@ describe("createDatabaseTreeNodes", () => {
{ name: CapabilityNames.EnableServerless, description: "" }, { name: CapabilityNames.EnableServerless, description: "" },
], ],
}, },
{ fabricContext: undefined },
], ],
[ [
"the Mongo API, with Notebooks and Phoenix features, on Emulator", "the Mongo API, with Notebooks and Phoenix features, on Emulator",
@ -382,26 +404,31 @@ describe("createDatabaseTreeNodes", () => {
{ {
capabilities: [{ name: CapabilityNames.EnableMongo, description: "" }], capabilities: [{ name: CapabilityNames.EnableMongo, description: "" }],
}, },
{ fabricContext: undefined },
], ],
])("generates the correct tree structure for %s", (_, platform, isNotebookEnabled, dbAccountProperties) => { ])(
useNotebook.setState({ isPhoenixFeatures: isNotebookEnabled }); "generates the correct tree structure for %s",
updateConfigContext({ platform }); (_, platform, isNotebookEnabled, dbAccountProperties, userContext) => {
updateUserContext({ useNotebook.setState({ isPhoenixFeatures: isNotebookEnabled });
databaseAccount: { updateConfigContext({ platform });
properties: { updateUserContext({
enableMultipleWriteLocations: true, ...userContext,
...dbAccountProperties, databaseAccount: {
}, properties: {
} as unknown as DataModels.DatabaseAccount, enableMultipleWriteLocations: true,
}); ...dbAccountProperties,
const nodes = createDatabaseTreeNodes( },
explorer, } as unknown as DataModels.DatabaseAccount,
isNotebookEnabled, });
useDatabases.getState().databases, const nodes = createDatabaseTreeNodes(
refreshActiveTab, explorer,
); isNotebookEnabled,
expect(nodes).toMatchSnapshot(); useDatabases.getState().databases,
}); refreshActiveTab,
);
expect(nodes).toMatchSnapshot();
},
);
// The above tests focused on the tree structure. The below tests focus on some core behaviors of the nodes. // The above tests focused on the tree structure. The below tests focus on some core behaviors of the nodes.
// They are not exhaustive, because exhaustive tests here require a lot of mocking and can become very brittle. // They are not exhaustive, because exhaustive tests here require a lot of mocking and can become very brittle.
@ -559,6 +586,7 @@ describe("createDatabaseTreeNodes", () => {
updateUserContext({ updateUserContext({
fabricContext: { fabricContext: {
artifactType: CosmosDbArtifactType.MIRRORED_KEY, artifactType: CosmosDbArtifactType.MIRRORED_KEY,
isReadOnly: true,
} as FabricContext<CosmosDbArtifactType>, } as FabricContext<CosmosDbArtifactType>,
}); });
}, },