-
-
- {/* Main Command Bar - Start */}
-
- {/* Collections Tree and Tabs - Begin */}
-
- {/* Collections Tree and Tabs - End */}
-
-
-
-
+ {userContext.features.enableContainerCopy && userContext.apiType === "SQL" ? (
+
+ ) : (
+
+ )}
+
{
}
@@ -113,6 +106,27 @@ const App: React.FunctionComponent = () => {
const mainElement = document.getElementById("Main");
ReactDOM.render(
, mainElement);
+function DivExplorer({ explorer }: { explorer: Explorer }): JSX.Element {
+ return (
+
+
+ {/* Main Command Bar - Start */}
+
+ {/* Collections Tree and Tabs - Begin */}
+
+ {/* Collections Tree and Tabs - End */}
+
+
+
+
+ );
+}
+
function LoadingExplorer(): JSX.Element {
return (
diff --git a/src/Platform/Hosted/extractFeatures.ts b/src/Platform/Hosted/extractFeatures.ts
index 685930b43..80a5494a4 100644
--- a/src/Platform/Hosted/extractFeatures.ts
+++ b/src/Platform/Hosted/extractFeatures.ts
@@ -39,6 +39,7 @@ export type Features = {
readonly copilotChatFixedMonacoEditorHeight: boolean;
readonly enablePriorityBasedExecution: boolean;
readonly disableConnectionStringLogin: boolean;
+ readonly enableContainerCopy: boolean;
readonly enableCloudShell: boolean;
// can be set via both flight and feature flag
@@ -111,6 +112,7 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
copilotChatFixedMonacoEditorHeight: "true" === get("copilotchatfixedmonacoeditorheight"),
enablePriorityBasedExecution: "true" === get("enableprioritybasedexecution"),
disableConnectionStringLogin: "true" === get("disableconnectionstringlogin"),
+ enableContainerCopy: "true" === get("enablecontainercopy"),
enableCloudShell: true,
};
}
diff --git a/src/UserContext.ts b/src/UserContext.ts
index 8a723ad91..ecc4f5807 100644
--- a/src/UserContext.ts
+++ b/src/UserContext.ts
@@ -176,7 +176,7 @@ function updateUserContext(newContext: Partial): void {
Object.assign(userContext, newContext);
}
-function apiType(account: DatabaseAccount | undefined): ApiType {
+export function apiType(account: DatabaseAccount | undefined): ApiType {
if (!account) {
return "SQL";
}
diff --git a/src/Utils/AuthorizationUtils.test.ts b/src/Utils/AuthorizationUtils.test.ts
index bc47c7a5f..650f2ed17 100644
--- a/src/Utils/AuthorizationUtils.test.ts
+++ b/src/Utils/AuthorizationUtils.test.ts
@@ -8,6 +8,7 @@ describe("AuthorizationUtils", () => {
const setAadDataPlane = (enabled: boolean) => {
updateUserContext({
features: {
+ enableContainerCopy: false,
enableAadDataPlane: enabled,
canExceedMaximumValue: false,
cosmosdb: false,
diff --git a/src/Utils/CopyJobAuthUtils.ts b/src/Utils/CopyJobAuthUtils.ts
new file mode 100644
index 000000000..19e8f5b18
--- /dev/null
+++ b/src/Utils/CopyJobAuthUtils.ts
@@ -0,0 +1,12 @@
+import { userContext } from "UserContext";
+
+export function getCopyJobAuthorizationHeader(token: string = ""): Headers {
+ if (!token && !userContext.authorizationToken) {
+ throw new Error("Authorization token is missing");
+ }
+ const headers = new Headers();
+ const authToken = token ? `Bearer ${token}` : userContext.authorizationToken ?? "";
+ headers.append("Authorization", authToken);
+ headers.append("Content-Type", "application/json");
+ return headers;
+}
diff --git a/src/Utils/arm/RbacUtils.ts b/src/Utils/arm/RbacUtils.ts
new file mode 100644
index 000000000..739f327d2
--- /dev/null
+++ b/src/Utils/arm/RbacUtils.ts
@@ -0,0 +1,115 @@
+import { configContext } from "ConfigContext";
+import { buildArmUrl } from "Utils/arm/armUtils";
+import { armRequest } from "Utils/arm/request";
+import { getCopyJobAuthorizationHeader } from "../CopyJobAuthUtils";
+
+export type FetchAccountDetailsParams = {
+ subscriptionId: string;
+ resourceGroupName: string;
+ accountName: string;
+};
+
+export type RoleAssignmentPropertiesType = {
+ roleDefinitionId: string;
+ principalId: string;
+ scope: string;
+};
+
+export type RoleAssignmentType = {
+ id: string;
+ name: string;
+ properties: RoleAssignmentPropertiesType;
+ type: string;
+};
+
+type RoleDefinitionDataActions = {
+ dataActions: string[];
+};
+
+export type RoleDefinitionType = {
+ assignableScopes: string[];
+ id: string;
+ name: string;
+ permissions: RoleDefinitionDataActions[];
+ resourceGroup: string;
+ roleName: string;
+ type: string;
+ typePropertiesType: string;
+};
+
+const apiVersion = "2025-04-15";
+
+const handleResponse = async (response: Response, context: string) => {
+ if (!response.ok) {
+ const body = await response.text().catch(() => "");
+ throw new Error(`Failed to fetch ${context}. Status: ${response.status}. ${body || ""}`);
+ }
+ return response.json();
+};
+
+export const fetchRoleAssignments = async (
+ subscriptionId: string,
+ resourceGroupName: string,
+ accountName: string,
+ principalId: string,
+): Promise => {
+ const uri = buildArmUrl(
+ `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/sqlRoleAssignments`,
+ apiVersion,
+ );
+
+ const response = await fetch(uri, { method: "GET", headers: getCopyJobAuthorizationHeader() });
+ const data = await handleResponse(response, "role assignments");
+
+ return (data.value || []).filter(
+ (assignment: RoleAssignmentType) => assignment?.properties?.principalId === principalId,
+ );
+};
+
+export const fetchRoleDefinitions = async (roleAssignments: RoleAssignmentType[]): Promise => {
+ const roleDefinitionIds = roleAssignments.map((assignment) => assignment.properties.roleDefinitionId);
+ const uniqueRoleDefinitionIds = Array.from(new Set(roleDefinitionIds));
+
+ const headers = getCopyJobAuthorizationHeader();
+ const roleDefinitionUris = uniqueRoleDefinitionIds.map((id) => buildArmUrl(id, apiVersion));
+
+ const promises = roleDefinitionUris.map((url) => fetch(url, { method: "GET", headers }));
+ const responses = await Promise.all(promises);
+
+ const roleDefinitions = await Promise.all(
+ responses.map((res, i) => handleResponse(res, `role definition ${uniqueRoleDefinitionIds[i]}`)),
+ );
+
+ return roleDefinitions;
+};
+
+export const assignRole = async (
+ subscriptionId: string,
+ resourceGroupName: string,
+ accountName: string,
+ principalId: string,
+): Promise => {
+ if (!principalId) {
+ return null;
+ }
+ const accountScope = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}`;
+ const roleDefinitionId = `${accountScope}/sqlRoleDefinitions/00000000-0000-0000-0000-000000000001`;
+ const roleAssignmentName = crypto.randomUUID();
+ const path = `${accountScope}/sqlRoleAssignments/${roleAssignmentName}`;
+
+ const body = {
+ properties: {
+ roleDefinitionId,
+ scope: `${accountScope}/`,
+ principalId,
+ },
+ };
+ const response: RoleAssignmentType = await armRequest({
+ host: configContext.ARM_ENDPOINT,
+ path,
+ method: "PUT",
+ apiVersion,
+ body,
+ });
+ return response;
+};
diff --git a/src/Utils/arm/armUtils.ts b/src/Utils/arm/armUtils.ts
new file mode 100644
index 000000000..3c2d0c356
--- /dev/null
+++ b/src/Utils/arm/armUtils.ts
@@ -0,0 +1,15 @@
+import { configContext } from "ConfigContext";
+
+const getArmBaseUrl = (): string => {
+ const base = configContext.ARM_ENDPOINT;
+ return base.endsWith("/") ? base.slice(0, -1) : base;
+};
+
+const buildArmUrl = (path: string, apiVersion: string): string => {
+ if (!path || !apiVersion) {
+ return "";
+ }
+ return `${getArmBaseUrl()}${path}?api-version=${apiVersion}`;
+};
+
+export { buildArmUrl, getArmBaseUrl };
diff --git a/src/Utils/arm/databaseAccountUtils.ts b/src/Utils/arm/databaseAccountUtils.ts
new file mode 100644
index 000000000..c7eb756b0
--- /dev/null
+++ b/src/Utils/arm/databaseAccountUtils.ts
@@ -0,0 +1,36 @@
+import { DatabaseAccount } from "Contracts/DataModels";
+import { userContext } from "UserContext";
+import { buildArmUrl } from "Utils/arm/armUtils";
+
+const apiVersion = "2025-04-15";
+export type FetchAccountDetailsParams = {
+ subscriptionId: string;
+ resourceGroupName: string;
+ accountName: string;
+};
+
+const buildUrl = (params: FetchAccountDetailsParams): string => {
+ const { subscriptionId, resourceGroupName, accountName } = params;
+
+ return buildArmUrl(
+ `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}`,
+ apiVersion,
+ );
+};
+
+export async function fetchDatabaseAccount(subscriptionId: string, resourceGroupName: string, accountName: string) {
+ if (!userContext.authorizationToken) {
+ return Promise.reject("Authorization token is missing");
+ }
+ const headers = new Headers();
+ headers.append("Authorization", userContext.authorizationToken);
+ headers.append("Content-Type", "application/json");
+ const uri = buildUrl({ subscriptionId, resourceGroupName, accountName });
+ const response = await fetch(uri, { method: "GET", headers: headers });
+
+ if (!response.ok) {
+ throw new Error(`Error fetching database account: ${response.statusText}`);
+ }
+ const account: DatabaseAccount = await response.json();
+ return account;
+}
diff --git a/src/Utils/arm/generatedClients/dataTransferService/dataTransferJobs.ts b/src/Utils/arm/generatedClients/dataTransferService/dataTransferJobs.ts
index 0c2b7c916..a6168aba1 100644
--- a/src/Utils/arm/generatedClients/dataTransferService/dataTransferJobs.ts
+++ b/src/Utils/arm/generatedClients/dataTransferService/dataTransferJobs.ts
@@ -3,13 +3,13 @@
Run "npm run generateARMClients" to regenerate
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
- Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2023-11-15-preview/dataTransferService.json
+ Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/DocumentDB/preview/2025-05-01-preview/dataTransferService.json
*/
import { configContext } from "../../../../ConfigContext";
import { armRequest } from "../../request";
import * as Types from "./types";
-const apiVersion = "2023-11-15-preview";
+const apiVersion = "2025-05-01-preview";
/* Creates a Data Transfer Job. */
export async function create(
@@ -67,12 +67,24 @@ export async function cancel(
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "POST", apiVersion });
}
+/* Completes a Data Transfer Online Job. */
+export async function complete(
+ subscriptionId: string,
+ resourceGroupName: string,
+ accountName: string,
+ jobName: string,
+): Promise {
+ const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/dataTransferJobs/${jobName}/complete`;
+ return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "POST", apiVersion });
+}
+
/* Get a list of Data Transfer jobs. */
export async function listByDatabaseAccount(
subscriptionId: string,
resourceGroupName: string,
accountName: string,
+ signal?: AbortSignal,
): Promise {
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/dataTransferJobs`;
- return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "GET", apiVersion });
+ return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "GET", apiVersion, signal });
}
diff --git a/src/Utils/arm/generatedClients/dataTransferService/types.ts b/src/Utils/arm/generatedClients/dataTransferService/types.ts
index 27c3db709..8807b6873 100644
--- a/src/Utils/arm/generatedClients/dataTransferService/types.ts
+++ b/src/Utils/arm/generatedClients/dataTransferService/types.ts
@@ -3,13 +3,13 @@
Run "npm run generateARMClients" to regenerate
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
- Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2023-11-15-preview/dataTransferService.json
+ Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/DocumentDB/preview/2025-05-01-preview/dataTransferService.json
*/
/* Base class for all DataTransfer source/sink */
export interface DataTransferDataSourceSink {
/* undocumented */
- component: "CosmosDBCassandra" | "CosmosDBMongo" | "CosmosDBSql" | "AzureBlobStorage";
+ component: "CosmosDBCassandra" | "CosmosDBMongo" | "CosmosDBMongoVCore" | "CosmosDBSql" | "AzureBlobStorage";
}
/* A base CosmosDB data source/sink */
@@ -34,6 +34,18 @@ export type CosmosMongoDataTransferDataSourceSink = BaseCosmosDataTransferDataSo
collectionName: string;
};
+/* A CosmosDB Mongo vCore API data source/sink */
+export type CosmosMongoVCoreDataTransferDataSourceSink = DataTransferDataSourceSink & {
+ /* undocumented */
+ databaseName: string;
+ /* undocumented */
+ collectionName: string;
+ /* undocumented */
+ hostName?: string;
+ /* undocumented */
+ connectionStringKeyVaultUri?: string;
+};
+
/* A CosmosDB No Sql API data source/sink */
export type CosmosSqlDataTransferDataSourceSink = BaseCosmosDataTransferDataSourceSink & {
/* undocumented */
diff --git a/src/Utils/arm/identityUtils.ts b/src/Utils/arm/identityUtils.ts
new file mode 100644
index 000000000..48700ea1f
--- /dev/null
+++ b/src/Utils/arm/identityUtils.ts
@@ -0,0 +1,57 @@
+import { DatabaseAccount } from "Contracts/DataModels";
+import { configContext } from "../../ConfigContext";
+import { fetchDatabaseAccount } from "./databaseAccountUtils";
+import { armRequest } from "./request";
+
+const apiVersion = "2025-04-15";
+
+const updateIdentity = async (
+ subscriptionId: string,
+ resourceGroupName: string,
+ accountName: string,
+ body: object,
+): Promise => {
+ const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}`;
+ const response: { status: string } = await armRequest({
+ host: configContext.ARM_ENDPOINT,
+ path,
+ method: "PATCH",
+ apiVersion,
+ body,
+ });
+ if (response.status === "Succeeded") {
+ const account = await fetchDatabaseAccount(subscriptionId, resourceGroupName, accountName);
+ return account;
+ }
+ return null;
+};
+
+const updateSystemIdentity = async (
+ subscriptionId: string,
+ resourceGroupName: string,
+ accountName: string,
+): Promise => {
+ const body = {
+ identity: {
+ type: "SystemAssigned",
+ },
+ };
+ const updatedAccount = await updateIdentity(subscriptionId, resourceGroupName, accountName, body);
+ return updatedAccount;
+};
+
+const updateDefaultIdentity = async (
+ subscriptionId: string,
+ resourceGroupName: string,
+ accountName: string,
+): Promise => {
+ const body = {
+ properties: {
+ defaultIdentity: "SystemAssignedIdentity",
+ },
+ };
+ const updatedAccount = await updateIdentity(subscriptionId, resourceGroupName, accountName, body);
+ return updatedAccount;
+};
+
+export { updateDefaultIdentity, updateSystemIdentity };
diff --git a/src/Utils/arm/request.ts b/src/Utils/arm/request.ts
index 5e3ebb004..3471fb67b 100644
--- a/src/Utils/arm/request.ts
+++ b/src/Utils/arm/request.ts
@@ -48,6 +48,7 @@ interface Options {
queryParams?: ARMQueryParams;
contentType?: string;
customHeaders?: Record;
+ signal?: AbortSignal;
}
export async function armRequestWithoutPolling({
@@ -59,6 +60,7 @@ export async function armRequestWithoutPolling({
queryParams,
contentType,
customHeaders,
+ signal,
}: Options): Promise<{ result: T; operationStatusUrl: string }> {
const url = new URL(path, host);
url.searchParams.append("api-version", configContext.armAPIVersion || apiVersion);
@@ -81,6 +83,7 @@ export async function armRequestWithoutPolling({
method,
headers,
body: requestBody ? JSON.stringify(requestBody) : undefined,
+ signal,
});
if (!response.ok) {
@@ -116,6 +119,7 @@ export async function armRequest({
queryParams,
contentType,
customHeaders,
+ signal,
}: Options): Promise {
const armRequestResult = await armRequestWithoutPolling({
host,
@@ -126,6 +130,7 @@ export async function armRequest({
queryParams,
contentType,
customHeaders,
+ signal,
});
const operationStatusUrl = armRequestResult.operationStatusUrl;
if (operationStatusUrl) {
diff --git a/src/hooks/useDataContainers.tsx b/src/hooks/useDataContainers.tsx
new file mode 100644
index 000000000..ecd6e55e1
--- /dev/null
+++ b/src/hooks/useDataContainers.tsx
@@ -0,0 +1,75 @@
+import { DatabaseModel } from "Contracts/DataModels";
+import useSWR from "swr";
+import { getCollectionEndpoint, getDatabaseEndpoint } from "../Common/DatabaseAccountUtility";
+import { configContext } from "../ConfigContext";
+import { ApiType } from "../UserContext";
+import { getCopyJobAuthorizationHeader } from "../Utils/CopyJobAuthUtils";
+
+const apiVersion = "2023-09-15";
+export interface FetchDataContainersListParams {
+ subscriptionId: string;
+ resourceGroupName: string;
+ databaseName: string;
+ accountName: string;
+ apiType?: ApiType;
+}
+
+const buildReadDataContainersListUrl = (params: FetchDataContainersListParams): string => {
+ const { subscriptionId, resourceGroupName, accountName, databaseName, apiType } = params;
+ const databaseEndpoint = getDatabaseEndpoint(apiType);
+ const collectionEndpoint = getCollectionEndpoint(apiType);
+
+ let armEndpoint = configContext.ARM_ENDPOINT;
+ if (armEndpoint.endsWith("/")) {
+ armEndpoint = armEndpoint.slice(0, -1);
+ }
+ return `${armEndpoint}/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/${databaseEndpoint}/${databaseName}/${collectionEndpoint}?api-version=${apiVersion}`;
+};
+
+const fetchDataContainersList = async (
+ subscriptionId: string,
+ resourceGroupName: string,
+ accountName: string,
+ databaseName: string,
+ apiType: ApiType,
+): Promise => {
+ const uri = buildReadDataContainersListUrl({
+ subscriptionId,
+ resourceGroupName,
+ accountName,
+ databaseName,
+ apiType,
+ });
+ const headers = getCopyJobAuthorizationHeader();
+
+ const response = await fetch(uri, {
+ method: "GET",
+ headers: headers,
+ });
+
+ if (!response.ok) {
+ throw new Error("Failed to fetch containers");
+ }
+
+ const data = await response.json();
+ return data.value;
+};
+
+export function useDataContainers(
+ subscriptionId: string,
+ resourceGroupName: string,
+ accountName: string,
+ databaseName: string,
+ apiType: ApiType,
+): DatabaseModel[] | undefined {
+ const { data } = useSWR(
+ () =>
+ subscriptionId && resourceGroupName && accountName && databaseName && apiType
+ ? ["fetchContainersLinkedToDatabases", subscriptionId, resourceGroupName, accountName, databaseName, apiType]
+ : undefined,
+ (_, subscriptionId, resourceGroupName, accountName, databaseName, apiType) =>
+ fetchDataContainersList(subscriptionId, resourceGroupName, accountName, databaseName, apiType),
+ );
+
+ return data;
+}
diff --git a/src/hooks/useDatabaseAccounts.tsx b/src/hooks/useDatabaseAccounts.tsx
index f517b2e30..18474b6fc 100644
--- a/src/hooks/useDatabaseAccounts.tsx
+++ b/src/hooks/useDatabaseAccounts.tsx
@@ -1,6 +1,7 @@
import { HttpHeaders } from "Common/Constants";
import { QueryRequestOptions, QueryResponse } from "Contracts/AzureResourceGraph";
import useSWR from "swr";
+import { userContext } from "UserContext";
import { configContext } from "../ConfigContext";
import { DatabaseAccount } from "../Contracts/DataModels";
/* eslint-disable @typescript-eslint/no-explicit-any */
@@ -10,9 +11,15 @@ interface AccountListResult {
value: DatabaseAccount[];
}
-export async function fetchDatabaseAccounts(subscriptionId: string, accessToken: string): Promise {
+export async function fetchDatabaseAccounts(
+ subscriptionId: string,
+ accessToken: string = "",
+): Promise {
+ if (!accessToken && !userContext.authorizationToken) {
+ return [];
+ }
const headers = new Headers();
- const bearer = `Bearer ${accessToken}`;
+ const bearer = accessToken ? `Bearer ${accessToken}` : userContext.authorizationToken;
headers.append("Authorization", bearer);
@@ -35,10 +42,13 @@ export async function fetchDatabaseAccounts(subscriptionId: string, accessToken:
export async function fetchDatabaseAccountsFromGraph(
subscriptionId: string,
- accessToken: string,
+ accessToken: string = "",
): Promise {
+ if (!accessToken && !userContext.authorizationToken) {
+ return [];
+ }
const headers = new Headers();
- const bearer = `Bearer ${accessToken}`;
+ const bearer = accessToken ? `Bearer ${accessToken}` : userContext.authorizationToken;
headers.append("Authorization", bearer);
headers.append(HttpHeaders.contentType, "application/json");
@@ -85,9 +95,9 @@ export async function fetchDatabaseAccountsFromGraph(
return databaseAccounts.sort((a, b) => a.name.localeCompare(b.name));
}
-export function useDatabaseAccounts(subscriptionId: string, armToken: string): DatabaseAccount[] | undefined {
+export function useDatabaseAccounts(subscriptionId: string, armToken: string = ""): DatabaseAccount[] | undefined {
const { data } = useSWR(
- () => (armToken && subscriptionId ? ["databaseAccounts", subscriptionId, armToken] : undefined),
+ () => (subscriptionId ? ["databaseAccounts", subscriptionId, armToken] : undefined),
(_, subscriptionId, armToken) => fetchDatabaseAccountsFromGraph(subscriptionId, armToken),
);
return data;
diff --git a/src/hooks/useDatabases.tsx b/src/hooks/useDatabases.tsx
new file mode 100644
index 000000000..c0d77b8e0
--- /dev/null
+++ b/src/hooks/useDatabases.tsx
@@ -0,0 +1,65 @@
+import { DatabaseModel } from "Contracts/DataModels";
+import useSWR from "swr";
+import { getDatabaseEndpoint } from "../Common/DatabaseAccountUtility";
+import { configContext } from "../ConfigContext";
+import { ApiType } from "../UserContext";
+import { getCopyJobAuthorizationHeader } from "../Utils/CopyJobAuthUtils";
+
+const apiVersion = "2023-09-15";
+export interface FetchDatabasesListParams {
+ subscriptionId: string;
+ resourceGroupName: string;
+ accountName: string;
+ apiType?: ApiType;
+}
+
+const buildReadDatabasesListUrl = (params: FetchDatabasesListParams): string => {
+ const { subscriptionId, resourceGroupName, accountName, apiType } = params;
+ const databaseEndpoint = getDatabaseEndpoint(apiType);
+
+ let armEndpoint = configContext.ARM_ENDPOINT;
+ if (armEndpoint.endsWith("/")) {
+ armEndpoint = armEndpoint.slice(0, -1);
+ }
+ return `${armEndpoint}/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/${databaseEndpoint}?api-version=${apiVersion}`;
+};
+
+const fetchDatabasesList = async (
+ subscriptionId: string,
+ resourceGroupName: string,
+ accountName: string,
+ apiType: ApiType,
+): Promise => {
+ const uri = buildReadDatabasesListUrl({ subscriptionId, resourceGroupName, accountName, apiType });
+ const headers = getCopyJobAuthorizationHeader();
+
+ const response = await fetch(uri, {
+ method: "GET",
+ headers: headers,
+ });
+
+ if (!response.ok) {
+ throw new Error("Failed to fetch databases");
+ }
+
+ const data = await response.json();
+ return data.value;
+};
+
+export function useDatabases(
+ subscriptionId: string,
+ resourceGroupName: string,
+ accountName: string,
+ apiType: ApiType,
+): DatabaseModel[] | undefined {
+ const { data } = useSWR(
+ () =>
+ subscriptionId && resourceGroupName && accountName && apiType
+ ? ["fetchDatabasesLinkedToResource", subscriptionId, resourceGroupName, accountName, apiType]
+ : undefined,
+ (_, subscriptionId, resourceGroupName, accountName, apiType) =>
+ fetchDatabasesList(subscriptionId, resourceGroupName, accountName, apiType),
+ );
+
+ return data;
+}
diff --git a/src/hooks/useKnockoutExplorer.ts b/src/hooks/useKnockoutExplorer.ts
index 46e655a87..3e5546f80 100644
--- a/src/hooks/useKnockoutExplorer.ts
+++ b/src/hooks/useKnockoutExplorer.ts
@@ -960,6 +960,10 @@ function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) {
Object.assign(userContext.features, extractFeatures(new URLSearchParams(inputs.features)));
}
+ if (configContext.platform === Platform.Portal && inputs.containerCopyEnabled && userContext.apiType === "SQL") {
+ Object.assign(userContext.features, { enableContainerCopy: inputs.containerCopyEnabled });
+ }
+
if (inputs.flights) {
if (inputs.flights.indexOf(Flights.AutoscaleTest) !== -1) {
userContext.features.autoscaleDefault;
diff --git a/src/hooks/useSidePanel.ts b/src/hooks/useSidePanel.ts
index 8f7eab69b..25b87f346 100644
--- a/src/hooks/useSidePanel.ts
+++ b/src/hooks/useSidePanel.ts
@@ -3,15 +3,19 @@ import create, { UseStore } from "zustand";
export interface SidePanelState {
isOpen: boolean;
panelWidth: string;
+ hasConsole: boolean;
panelContent?: JSX.Element;
headerText?: string;
openSidePanel: (headerText: string, panelContent: JSX.Element, panelWidth?: string, onClose?: () => void) => void;
closeSidePanel: () => void;
+ setPanelHasConsole: (hasConsole: boolean) => void;
getRef?: React.RefObject; // Optional ref for focusing the last element.
}
export const useSidePanel: UseStore = create((set) => ({
isOpen: false,
panelWidth: "440px",
+ hasConsole: true,
+ setPanelHasConsole: (hasConsole: boolean) => set((state) => ({ ...state, hasConsole })),
openSidePanel: (headerText, panelContent, panelWidth = "440px") =>
set((state) => ({ ...state, headerText, panelContent, panelWidth, isOpen: true })),
closeSidePanel: () => {
diff --git a/src/hooks/useSubscriptions.tsx b/src/hooks/useSubscriptions.tsx
index ca80a87f5..5977ac911 100644
--- a/src/hooks/useSubscriptions.tsx
+++ b/src/hooks/useSubscriptions.tsx
@@ -1,6 +1,7 @@
import { HttpHeaders } from "Common/Constants";
import { QueryRequestOptions, QueryResponse } from "Contracts/AzureResourceGraph";
import useSWR from "swr";
+import { userContext } from "UserContext";
import { configContext } from "../ConfigContext";
import { Subscription } from "../Contracts/DataModels";
/* eslint-disable @typescript-eslint/no-explicit-any */
@@ -10,9 +11,12 @@ interface SubscriptionListResult {
value: Subscription[];
}
-export async function fetchSubscriptions(accessToken: string): Promise {
+export async function fetchSubscriptions(accessToken: string = ""): Promise {
+ if (!accessToken && !userContext.authorizationToken) {
+ return [];
+ }
const headers = new Headers();
- const bearer = `Bearer ${accessToken}`;
+ const bearer = accessToken ? `Bearer ${accessToken}` : userContext.authorizationToken;
headers.append("Authorization", bearer);
@@ -35,9 +39,12 @@ export async function fetchSubscriptions(accessToken: string): Promise a.displayName.localeCompare(b.displayName));
}
-export async function fetchSubscriptionsFromGraph(accessToken: string): Promise {
+export async function fetchSubscriptionsFromGraph(accessToken: string = ""): Promise {
+ if (!accessToken && !userContext.authorizationToken) {
+ return [];
+ }
const headers = new Headers();
- const bearer = `Bearer ${accessToken}`;
+ const bearer = accessToken ? `Bearer ${accessToken}` : userContext.authorizationToken;
headers.append("Authorization", bearer);
headers.append(HttpHeaders.contentType, "application/json");
@@ -85,9 +92,9 @@ export async function fetchSubscriptionsFromGraph(accessToken: string): Promise<
return subscriptions.sort((a, b) => a.displayName.localeCompare(b.displayName));
}
-export function useSubscriptions(armToken: string): Subscription[] | undefined {
+export function useSubscriptions(armToken: string = ""): Subscription[] | undefined {
const { data } = useSWR(
- () => (armToken ? ["subscriptions", armToken] : undefined),
+ () => ["subscriptions", armToken],
(_, armToken) => fetchSubscriptionsFromGraph(armToken),
);
return data;