Container Copy Job implementation for SQL accounts (#2241)

* Initial dev for container copy

* remove padding from label

* Added Copy Job prerequisites screen

* Added hooks to evaluate reader role access

* added copyjob pre-requsite screen along with it's validations

* Added monitor copy job list screen

* added copy job list refresh and reset functionality

* remove arm token dependency

* fetch account details from account id instead of context

* Fix lint & typescript checks

* show copyjob screen from portal navigation

* adding copy job details screen

* remove duplicate code & show sql accounts only

* ui fixes for list job page

* pending icon

* copy job details screen ui

* reset .vscode/settings.json

* Fixed existing UTs

* disabling action buttons until it's in progress

* fixed formatting

* Adding loader on submit button and show job creation errors in the panel itself

* updating disabling action menu item logic

* added custom pager

* fix lint and ts errors

* updating file names and removing comments

* remove comments

* modularize the arom common code

* Adding content and removing tooltip

* updating job details screen

* updating online copy enabled screen

* Adding below changes
- Don't show permission screen for same account in offline mode
- Don't show identity permissions for same account in online mode
- Show error message if selected containers are identical
- Update abort signal messages

* added feedback code from explorer

* Add tooltips and long polling
- Added tooltips to permission sections
- Implemented long polling for PITR and online copy enabled sections
- Long polling automatically stops after 15 minutes
- After polling ends, a refresh button will be displayed

---------

Co-authored-by: nishthaAhujaa <nishtha17354@iiittd.ac.in>
This commit is contained in:
BChoudhury-ms
2025-11-05 22:54:00 +05:30
committed by GitHub
parent 3718f5a16a
commit 2417da152d
78 changed files with 4152 additions and 36 deletions

View File

@@ -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<DatabaseModel[]> => {
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;
}

View File

@@ -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<DatabaseAccount[]> {
export async function fetchDatabaseAccounts(
subscriptionId: string,
accessToken: string = "",
): Promise<DatabaseAccount[]> {
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<DatabaseAccount[]> {
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;

View File

@@ -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<DatabaseModel[]> => {
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;
}

View File

@@ -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;

View File

@@ -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<HTMLElement>; // Optional ref for focusing the last element.
}
export const useSidePanel: UseStore<SidePanelState> = 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: () => {

View File

@@ -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<Subscription[]> {
export async function fetchSubscriptions(accessToken: string = ""): Promise<Subscription[]> {
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<Subscript
return subscriptions.sort((a, b) => a.displayName.localeCompare(b.displayName));
}
export async function fetchSubscriptionsFromGraph(accessToken: string): Promise<Subscription[]> {
export async function fetchSubscriptionsFromGraph(accessToken: string = ""): Promise<Subscription[]> {
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;