mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-01-06 03:00:23 +00:00
Compare commits
12 Commits
2950560
...
revert-173
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00c05fce8a | ||
|
|
b480c635ca | ||
|
|
16eb096fdb | ||
|
|
e9571c0f2d | ||
|
|
c9abcc1728 | ||
|
|
a36f3f7922 | ||
|
|
5a64fc2582 | ||
|
|
12366bb645 | ||
|
|
9cebe5f9ba | ||
|
|
5d80ecb462 | ||
|
|
f87611a39d | ||
|
|
a914fd020c |
@@ -25,29 +25,30 @@ a:focus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.resourceTreeAndTabs {
|
.resourceTreeAndTabs {
|
||||||
border-radius: @FabricBoxBorderRadius;
|
border-radius: 0px;
|
||||||
box-shadow: @FabricBoxBorderShadow;
|
box-shadow: @FabricBoxBorderShadow;
|
||||||
margin: @FabricBoxMargin;
|
margin: @FabricBoxMargin;
|
||||||
margin-top: 4px;
|
margin-top: 0px;
|
||||||
|
margin-bottom: 0px;
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabsManagerContainer {
|
.tabsManagerContainer {
|
||||||
background-color: #fafafa
|
background-color: #ffffff
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-tabs-margin {
|
.nav-tabs-margin {
|
||||||
padding-top: 8px;
|
padding-top: 8px;
|
||||||
background-color: #fafafa
|
background-color: #ffffff
|
||||||
}
|
}
|
||||||
|
|
||||||
.commandBarContainer {
|
.commandBarContainer {
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
border-bottom: none;
|
border-radius: @FabricBoxBorderRadius @FabricBoxBorderRadius 0px 0px;
|
||||||
border-radius: @FabricBoxBorderRadius;
|
|
||||||
box-shadow: @FabricBoxBorderShadow;
|
box-shadow: @FabricBoxBorderShadow;
|
||||||
margin: @FabricBoxMargin;
|
margin: @FabricBoxMargin;
|
||||||
margin-top: 0px;
|
margin-top: 0px;
|
||||||
|
margin-bottom: 0px;
|
||||||
padding-top: 2px;
|
padding-top: 2px;
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
@@ -162,9 +163,10 @@ a:focus {
|
|||||||
|
|
||||||
|
|
||||||
.dataExplorerErrorConsoleContainer {
|
.dataExplorerErrorConsoleContainer {
|
||||||
border-radius: @FabricBoxBorderRadius;
|
border-radius: 0px 0px @FabricBoxBorderRadius @FabricBoxBorderRadius;
|
||||||
box-shadow: @FabricBoxBorderShadow;
|
box-shadow: @FabricBoxBorderShadow;
|
||||||
margin: @FabricBoxMargin;
|
margin: @FabricBoxMargin;
|
||||||
|
margin-top: 0px;
|
||||||
width: auto;
|
width: auto;
|
||||||
align-self: auto;
|
align-self: auto;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -435,6 +435,22 @@ export class JunoEndpoints {
|
|||||||
public static readonly Stage = "https://tools-staging.cosmos.azure.com";
|
public static readonly Stage = "https://tools-staging.cosmos.azure.com";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class MongoProxyEndpoints {
|
||||||
|
public static readonly Development: string = "https://localhost:7238";
|
||||||
|
public static readonly Mpac: string = "https://cdb-ms-mpac-mp.cosmos.azure.com";
|
||||||
|
public static readonly Prod: string = "https://cdb-ms-prod-mp.cosmos.azure.com";
|
||||||
|
public static readonly Fairfax: string = "https://cdb-ff-prod-mp.cosmos.azure.us";
|
||||||
|
public static readonly Mooncake: string = "https://cdb-mc-prod-mp.cosmos.azure.cn";
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CassandraProxyEndpoints {
|
||||||
|
public static readonly Development: string = "https://localhost:7240";
|
||||||
|
public static readonly Mpac: string = "https://cdb-ms-mpac-cp.cosmos.azure.com";
|
||||||
|
public static readonly Prod: string = "https://cdb-ms-prod-cp.cosmos.azure.com";
|
||||||
|
public static readonly Fairfax: string = "https://cdb-ff-prod-cp.cosmos.azure.us";
|
||||||
|
public static readonly Mooncake: string = "https://cdb-mc-prod-cp.cosmos.azure.cn";
|
||||||
|
}
|
||||||
|
|
||||||
export class PriorityLevel {
|
export class PriorityLevel {
|
||||||
public static readonly High = "high";
|
public static readonly High = "high";
|
||||||
public static readonly Low = "low";
|
public static readonly Low = "low";
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import { Constants as CosmosSDKConstants } from "@azure/cosmos";
|
import { Constants as CosmosSDKConstants } from "@azure/cosmos";
|
||||||
import { MongoProxyEndpoints, allowedMongoProxyEndpoints_ToBeDeprecated, validateEndpoint } from "Utils/EndpointUtils";
|
import {
|
||||||
|
allowedMongoProxyEndpoints,
|
||||||
|
allowedMongoProxyEndpoints_ToBeDeprecated,
|
||||||
|
validateEndpoint,
|
||||||
|
} from "Utils/EndpointUtils";
|
||||||
import queryString from "querystring";
|
import queryString from "querystring";
|
||||||
import { AuthType } from "../AuthType";
|
import { AuthType } from "../AuthType";
|
||||||
import { configContext } from "../ConfigContext";
|
import { configContext } from "../ConfigContext";
|
||||||
@@ -10,7 +14,7 @@ import DocumentId from "../Explorer/Tree/DocumentId";
|
|||||||
import { hasFlag } from "../Platform/Hosted/extractFeatures";
|
import { hasFlag } from "../Platform/Hosted/extractFeatures";
|
||||||
import { userContext } from "../UserContext";
|
import { userContext } from "../UserContext";
|
||||||
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
||||||
import { ApiType, ContentType, HttpHeaders, HttpStatusCodes } from "./Constants";
|
import { ApiType, ContentType, HttpHeaders, HttpStatusCodes, MongoProxyEndpoints } from "./Constants";
|
||||||
import { MinimalQueryIterator } from "./IteratorUtilities";
|
import { MinimalQueryIterator } from "./IteratorUtilities";
|
||||||
import { sendMessage } from "./MessageHandler";
|
import { sendMessage } from "./MessageHandler";
|
||||||
|
|
||||||
@@ -461,7 +465,7 @@ export function updateDocument_ToBeDeprecated(
|
|||||||
|
|
||||||
export function deleteDocument(databaseId: string, collection: Collection, documentId: DocumentId): Promise<void> {
|
export function deleteDocument(databaseId: string, collection: Collection, documentId: DocumentId): Promise<void> {
|
||||||
if (!useMongoProxyEndpoint("deleteDocument")) {
|
if (!useMongoProxyEndpoint("deleteDocument")) {
|
||||||
deleteDocument_ToBeDeprecated(databaseId, collection, documentId);
|
return deleteDocument_ToBeDeprecated(databaseId, collection, documentId);
|
||||||
}
|
}
|
||||||
const { databaseAccount } = userContext;
|
const { databaseAccount } = userContext;
|
||||||
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||||
@@ -550,7 +554,7 @@ export function createMongoCollectionWithProxy(
|
|||||||
params: DataModels.CreateCollectionParams,
|
params: DataModels.CreateCollectionParams,
|
||||||
): Promise<DataModels.Collection> {
|
): Promise<DataModels.Collection> {
|
||||||
if (!useMongoProxyEndpoint("createCollectionWithProxy")) {
|
if (!useMongoProxyEndpoint("createCollectionWithProxy")) {
|
||||||
createMongoCollectionWithProxy_ToBeDeprecated(params);
|
return createMongoCollectionWithProxy_ToBeDeprecated(params);
|
||||||
}
|
}
|
||||||
const { databaseAccount } = userContext;
|
const { databaseAccount } = userContext;
|
||||||
const shardKey: string = params.partitionKey?.paths[0];
|
const shardKey: string = params.partitionKey?.paths[0];
|
||||||
@@ -644,7 +648,10 @@ export function getFeatureEndpointOrDefault(feature: string): string {
|
|||||||
} else {
|
} else {
|
||||||
endpoint =
|
endpoint =
|
||||||
hasFlag(userContext.features.mongoProxyAPIs, feature) &&
|
hasFlag(userContext.features.mongoProxyAPIs, feature) &&
|
||||||
validateEndpoint(userContext.features.mongoProxyEndpoint, allowedMongoProxyEndpoints_ToBeDeprecated)
|
validateEndpoint(userContext.features.mongoProxyEndpoint, [
|
||||||
|
...allowedMongoProxyEndpoints,
|
||||||
|
...allowedMongoProxyEndpoints_ToBeDeprecated,
|
||||||
|
])
|
||||||
? userContext.features.mongoProxyEndpoint
|
? userContext.features.mongoProxyEndpoint
|
||||||
: configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_ENDPOINT;
|
: configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_ENDPOINT;
|
||||||
}
|
}
|
||||||
@@ -683,8 +690,14 @@ export function getARMCreateCollectionEndpoint(params: DataModels.MongoParameter
|
|||||||
}
|
}
|
||||||
|
|
||||||
function useMongoProxyEndpoint(api: string): boolean {
|
function useMongoProxyEndpoint(api: string): boolean {
|
||||||
|
let canAccessMongoProxy: boolean = userContext.databaseAccount.properties.publicNetworkAccess === "Enabled";
|
||||||
|
if (userContext.databaseAccount.properties.ipRules?.length > 0) {
|
||||||
|
canAccessMongoProxy = canAccessMongoProxy && configContext.MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
canAccessMongoProxy &&
|
||||||
configContext.NEW_MONGO_APIS?.includes(api) &&
|
configContext.NEW_MONGO_APIS?.includes(api) &&
|
||||||
[MongoProxyEndpoints.Development, MongoProxyEndpoints.MPAC].includes(configContext.MONGO_PROXY_ENDPOINT)
|
[MongoProxyEndpoints.Development, MongoProxyEndpoints.Mpac].includes(configContext.MONGO_PROXY_ENDPOINT)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
188
src/Common/dataAccess/dataTransfers.ts
Normal file
188
src/Common/dataAccess/dataTransfers.ts
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
import { ApiType, userContext } from "UserContext";
|
||||||
|
import * as NotificationConsoleUtils from "Utils/NotificationConsoleUtils";
|
||||||
|
import {
|
||||||
|
cancel,
|
||||||
|
create,
|
||||||
|
get,
|
||||||
|
listByDatabaseAccount,
|
||||||
|
} from "Utils/arm/generatedClients/dataTransferService/dataTransferJobs";
|
||||||
|
import {
|
||||||
|
CosmosCassandraDataTransferDataSourceSink,
|
||||||
|
CosmosMongoDataTransferDataSourceSink,
|
||||||
|
CosmosSqlDataTransferDataSourceSink,
|
||||||
|
CreateJobRequest,
|
||||||
|
DataTransferJobFeedResults,
|
||||||
|
DataTransferJobGetResults,
|
||||||
|
} from "Utils/arm/generatedClients/dataTransferService/types";
|
||||||
|
import { addToPolling, removeFromPolling, updateDataTransferJob, useDataTransferJobs } from "hooks/useDataTransferJobs";
|
||||||
|
import promiseRetry, { AbortError, FailedAttemptError } from "p-retry";
|
||||||
|
|
||||||
|
export interface DataTransferParams {
|
||||||
|
jobName: string;
|
||||||
|
apiType: ApiType;
|
||||||
|
subscriptionId: string;
|
||||||
|
resourceGroupName: string;
|
||||||
|
accountName: string;
|
||||||
|
sourceDatabaseName: string;
|
||||||
|
sourceCollectionName: string;
|
||||||
|
targetDatabaseName: string;
|
||||||
|
targetCollectionName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getDataTransferJobs = async (
|
||||||
|
subscriptionId: string,
|
||||||
|
resourceGroup: string,
|
||||||
|
accountName: string,
|
||||||
|
): Promise<DataTransferJobGetResults[]> => {
|
||||||
|
let dataTransferJobs: DataTransferJobGetResults[] = [];
|
||||||
|
let dataTransferFeeds: DataTransferJobFeedResults = await listByDatabaseAccount(
|
||||||
|
subscriptionId,
|
||||||
|
resourceGroup,
|
||||||
|
accountName,
|
||||||
|
);
|
||||||
|
dataTransferJobs = [...dataTransferJobs, ...(dataTransferFeeds?.value || [])];
|
||||||
|
while (dataTransferFeeds?.nextLink) {
|
||||||
|
const nextResponse = await window.fetch(dataTransferFeeds.nextLink, {
|
||||||
|
headers: {
|
||||||
|
Authorization: userContext.authorizationToken,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (nextResponse.ok) {
|
||||||
|
dataTransferFeeds = await nextResponse.json();
|
||||||
|
dataTransferJobs = [...dataTransferJobs, ...(dataTransferFeeds?.value || [])];
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dataTransferJobs;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const initiateDataTransfer = async (params: DataTransferParams): Promise<DataTransferJobGetResults> => {
|
||||||
|
const {
|
||||||
|
jobName,
|
||||||
|
apiType,
|
||||||
|
subscriptionId,
|
||||||
|
resourceGroupName,
|
||||||
|
accountName,
|
||||||
|
sourceDatabaseName,
|
||||||
|
sourceCollectionName,
|
||||||
|
targetDatabaseName,
|
||||||
|
targetCollectionName,
|
||||||
|
} = params;
|
||||||
|
const sourcePayload = createPayload(apiType, sourceDatabaseName, sourceCollectionName);
|
||||||
|
const targetPayload = createPayload(apiType, targetDatabaseName, targetCollectionName);
|
||||||
|
const body: CreateJobRequest = {
|
||||||
|
properties: {
|
||||||
|
source: sourcePayload,
|
||||||
|
destination: targetPayload,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return create(subscriptionId, resourceGroupName, accountName, jobName, body);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const pollDataTransferJob = async (
|
||||||
|
jobName: string,
|
||||||
|
subscriptionId: string,
|
||||||
|
resourceGroupName: string,
|
||||||
|
accountName: string,
|
||||||
|
): Promise<unknown> => {
|
||||||
|
const currentPollingJobs = useDataTransferJobs.getState().pollingDataTransferJobs;
|
||||||
|
if (currentPollingJobs.has(jobName)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let clearMessage = NotificationConsoleUtils.logConsoleProgress(`Data transfer job ${jobName} in progress`);
|
||||||
|
return await promiseRetry(
|
||||||
|
() => pollDataTransferJobOperation(jobName, subscriptionId, resourceGroupName, accountName, clearMessage),
|
||||||
|
{
|
||||||
|
retries: 500,
|
||||||
|
maxTimeout: 5000,
|
||||||
|
onFailedAttempt: (error: FailedAttemptError) => {
|
||||||
|
clearMessage();
|
||||||
|
clearMessage = NotificationConsoleUtils.logConsoleProgress(error.message);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const pollDataTransferJobOperation = async (
|
||||||
|
jobName: string,
|
||||||
|
subscriptionId: string,
|
||||||
|
resourceGroupName: string,
|
||||||
|
accountName: string,
|
||||||
|
clearMessage?: () => void,
|
||||||
|
): Promise<DataTransferJobGetResults> => {
|
||||||
|
if (!userContext.authorizationToken) {
|
||||||
|
throw new Error("No authority token provided");
|
||||||
|
}
|
||||||
|
|
||||||
|
addToPolling(jobName);
|
||||||
|
|
||||||
|
const body: DataTransferJobGetResults = await get(subscriptionId, resourceGroupName, accountName, jobName);
|
||||||
|
const status = body?.properties?.status;
|
||||||
|
|
||||||
|
updateDataTransferJob(body);
|
||||||
|
|
||||||
|
if (status === "Cancelled" || status === "Failed" || status === "Faulted") {
|
||||||
|
removeFromPolling(jobName);
|
||||||
|
const errorMessage = body?.properties?.error
|
||||||
|
? JSON.stringify(body?.properties?.error)
|
||||||
|
: "Operation could not be completed";
|
||||||
|
const error = new Error(errorMessage);
|
||||||
|
clearMessage && clearMessage();
|
||||||
|
NotificationConsoleUtils.logConsoleError(`Data transfer job ${jobName} Failed`);
|
||||||
|
throw new AbortError(error);
|
||||||
|
}
|
||||||
|
if (status === "Completed") {
|
||||||
|
removeFromPolling(jobName);
|
||||||
|
clearMessage && clearMessage();
|
||||||
|
NotificationConsoleUtils.logConsoleInfo(`Data transfer job ${jobName} completed`);
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
const processedCount = body.properties.processedCount;
|
||||||
|
const totalCount = body.properties.totalCount;
|
||||||
|
const retryMessage = `Data transfer job ${jobName} in progress, total count: ${totalCount}, processed count: ${processedCount}`;
|
||||||
|
throw new Error(retryMessage);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const cancelDataTransferJob = async (
|
||||||
|
subscriptionId: string,
|
||||||
|
resourceGroupName: string,
|
||||||
|
accountName: string,
|
||||||
|
jobName: string,
|
||||||
|
): Promise<void> => {
|
||||||
|
const cancelResult: DataTransferJobGetResults = await cancel(subscriptionId, resourceGroupName, accountName, jobName);
|
||||||
|
updateDataTransferJob(cancelResult);
|
||||||
|
removeFromPolling(cancelResult?.properties?.jobName);
|
||||||
|
};
|
||||||
|
|
||||||
|
const createPayload = (
|
||||||
|
apiType: ApiType,
|
||||||
|
databaseName: string,
|
||||||
|
containerName: string,
|
||||||
|
):
|
||||||
|
| CosmosSqlDataTransferDataSourceSink
|
||||||
|
| CosmosMongoDataTransferDataSourceSink
|
||||||
|
| CosmosCassandraDataTransferDataSourceSink => {
|
||||||
|
switch (apiType) {
|
||||||
|
case "SQL":
|
||||||
|
return {
|
||||||
|
component: "CosmosDBSql",
|
||||||
|
databaseName: databaseName,
|
||||||
|
containerName: containerName,
|
||||||
|
} as CosmosSqlDataTransferDataSourceSink;
|
||||||
|
case "Mongo":
|
||||||
|
return {
|
||||||
|
component: "CosmosDBMongo",
|
||||||
|
databaseName: databaseName,
|
||||||
|
collectionName: containerName,
|
||||||
|
} as CosmosMongoDataTransferDataSourceSink;
|
||||||
|
case "Cassandra":
|
||||||
|
return {
|
||||||
|
component: "CosmosDBCassandra",
|
||||||
|
keyspaceName: databaseName,
|
||||||
|
tableName: containerName,
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported API type for data transfer: ${apiType}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import { JunoEndpoints } from "Common/Constants";
|
import { CassandraProxyEndpoints, JunoEndpoints, MongoProxyEndpoints } from "Common/Constants";
|
||||||
import {
|
import {
|
||||||
allowedAadEndpoints,
|
allowedAadEndpoints,
|
||||||
allowedArcadiaEndpoints,
|
allowedArcadiaEndpoints,
|
||||||
|
allowedCassandraProxyEndpoints,
|
||||||
allowedEmulatorEndpoints,
|
allowedEmulatorEndpoints,
|
||||||
allowedGraphEndpoints,
|
allowedGraphEndpoints,
|
||||||
allowedHostedExplorerEndpoints,
|
allowedHostedExplorerEndpoints,
|
||||||
@@ -40,7 +41,9 @@ export interface ConfigContext {
|
|||||||
BACKEND_ENDPOINT?: string;
|
BACKEND_ENDPOINT?: string;
|
||||||
MONGO_BACKEND_ENDPOINT?: string;
|
MONGO_BACKEND_ENDPOINT?: string;
|
||||||
MONGO_PROXY_ENDPOINT?: string;
|
MONGO_PROXY_ENDPOINT?: string;
|
||||||
|
MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED?: boolean;
|
||||||
NEW_MONGO_APIS?: string[];
|
NEW_MONGO_APIS?: string[];
|
||||||
|
CASSANDRA_PROXY_ENDPOINT?: string;
|
||||||
PROXY_PATH?: string;
|
PROXY_PATH?: string;
|
||||||
JUNO_ENDPOINT: string;
|
JUNO_ENDPOINT: string;
|
||||||
GITHUB_CLIENT_ID: string;
|
GITHUB_CLIENT_ID: string;
|
||||||
@@ -85,7 +88,7 @@ let configContext: Readonly<ConfigContext> = {
|
|||||||
GITHUB_TEST_ENV_CLIENT_ID: "b63fc8cbf87fd3c6e2eb", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1777772
|
GITHUB_TEST_ENV_CLIENT_ID: "b63fc8cbf87fd3c6e2eb", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1777772
|
||||||
JUNO_ENDPOINT: JunoEndpoints.Prod,
|
JUNO_ENDPOINT: JunoEndpoints.Prod,
|
||||||
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
||||||
MONGO_PROXY_ENDPOINT: "https://cdb-ms-prod-mp.cosmos.azure.com",
|
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||||
NEW_MONGO_APIS: [
|
NEW_MONGO_APIS: [
|
||||||
// "resourcelist",
|
// "resourcelist",
|
||||||
// "createDocument",
|
// "createDocument",
|
||||||
@@ -94,6 +97,8 @@ let configContext: Readonly<ConfigContext> = {
|
|||||||
// "deleteDocument",
|
// "deleteDocument",
|
||||||
// "createCollectionWithProxy",
|
// "createCollectionWithProxy",
|
||||||
],
|
],
|
||||||
|
MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED: false,
|
||||||
|
CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Prod,
|
||||||
isTerminalEnabled: false,
|
isTerminalEnabled: false,
|
||||||
isPhoenixEnabled: false,
|
isPhoenixEnabled: false,
|
||||||
};
|
};
|
||||||
@@ -147,6 +152,10 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
|
|||||||
delete newContext.MONGO_BACKEND_ENDPOINT;
|
delete newContext.MONGO_BACKEND_ENDPOINT;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!validateEndpoint(newContext.CASSANDRA_PROXY_ENDPOINT, allowedCassandraProxyEndpoints)) {
|
||||||
|
delete newContext.CASSANDRA_PROXY_ENDPOINT;
|
||||||
|
}
|
||||||
|
|
||||||
if (!validateEndpoint(newContext.JUNO_ENDPOINT, allowedJunoOrigins)) {
|
if (!validateEndpoint(newContext.JUNO_ENDPOINT, allowedJunoOrigins)) {
|
||||||
delete newContext.JUNO_ENDPOINT;
|
delete newContext.JUNO_ENDPOINT;
|
||||||
}
|
}
|
||||||
@@ -164,10 +173,7 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
|
|||||||
|
|
||||||
// Injected for local development. These will be removed in the production bundle by webpack
|
// Injected for local development. These will be removed in the production bundle by webpack
|
||||||
if (process.env.NODE_ENV === "development") {
|
if (process.env.NODE_ENV === "development") {
|
||||||
const port: string = process.env.PORT || "1234";
|
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
BACKEND_ENDPOINT: "https://localhost:" + port,
|
|
||||||
MONGO_BACKEND_ENDPOINT: "https://localhost:" + port,
|
|
||||||
PROXY_PATH: "/proxy",
|
PROXY_PATH: "/proxy",
|
||||||
EMULATOR_ENDPOINT: "https://localhost:8081",
|
EMULATOR_ENDPOINT: "https://localhost:8081",
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -387,6 +387,7 @@ export interface DataExplorerInputsFrame {
|
|||||||
serverId?: string;
|
serverId?: string;
|
||||||
extensionEndpoint?: string;
|
extensionEndpoint?: string;
|
||||||
mongoProxyEndpoint?: string;
|
mongoProxyEndpoint?: string;
|
||||||
|
cassandraProxyEndpoint?: string;
|
||||||
subscriptionType?: SubscriptionType;
|
subscriptionType?: SubscriptionType;
|
||||||
quotaId?: string;
|
quotaId?: string;
|
||||||
isTryCosmosDBSubscription?: boolean;
|
isTryCosmosDBSubscription?: boolean;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { IPivotItemProps, IPivotProps, Pivot, PivotItem } from "@fluentui/react";
|
import { IPivotItemProps, IPivotProps, Pivot, PivotItem } from "@fluentui/react";
|
||||||
import { useDatabases } from "Explorer/useDatabases";
|
import { useDatabases } from "Explorer/useDatabases";
|
||||||
|
import { isRunningOnPublicCloud } from "Utils/CloudUtils";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import DiscardIcon from "../../../../images/discard.svg";
|
import DiscardIcon from "../../../../images/discard.svg";
|
||||||
import SaveIcon from "../../../../images/save-cosmos.svg";
|
import SaveIcon from "../../../../images/save-cosmos.svg";
|
||||||
@@ -18,6 +19,10 @@ import { userContext } from "../../../UserContext";
|
|||||||
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
|
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
|
||||||
import { MongoDBCollectionResource, MongoIndex } from "../../../Utils/arm/generatedClients/cosmos/types";
|
import { MongoDBCollectionResource, MongoIndex } from "../../../Utils/arm/generatedClients/cosmos/types";
|
||||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||||
|
import {
|
||||||
|
PartitionKeyComponent,
|
||||||
|
PartitionKeyComponentProps,
|
||||||
|
} from "../../Controls/Settings/SettingsSubComponents/PartitionKeyComponent";
|
||||||
import { useCommandBar } from "../../Menus/CommandBar/CommandBarComponentAdapter";
|
import { useCommandBar } from "../../Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
import { SettingsTabV2 } from "../../Tabs/SettingsTabV2";
|
import { SettingsTabV2 } from "../../Tabs/SettingsTabV2";
|
||||||
import "./SettingsComponent.less";
|
import "./SettingsComponent.less";
|
||||||
@@ -128,6 +133,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
private changeFeedPolicyVisible: boolean;
|
private changeFeedPolicyVisible: boolean;
|
||||||
private isFixedContainer: boolean;
|
private isFixedContainer: boolean;
|
||||||
private shouldShowIndexingPolicyEditor: boolean;
|
private shouldShowIndexingPolicyEditor: boolean;
|
||||||
|
private shouldShowPartitionKeyEditor: boolean;
|
||||||
private totalThroughputUsed: number;
|
private totalThroughputUsed: number;
|
||||||
public mongoDBCollectionResource: MongoDBCollectionResource;
|
public mongoDBCollectionResource: MongoDBCollectionResource;
|
||||||
|
|
||||||
@@ -140,6 +146,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
this.offer = this.collection?.offer();
|
this.offer = this.collection?.offer();
|
||||||
this.isAnalyticalStorageEnabled = !!this.collection?.analyticalStorageTtl();
|
this.isAnalyticalStorageEnabled = !!this.collection?.analyticalStorageTtl();
|
||||||
this.shouldShowIndexingPolicyEditor = userContext.apiType !== "Cassandra" && userContext.apiType !== "Mongo";
|
this.shouldShowIndexingPolicyEditor = userContext.apiType !== "Cassandra" && userContext.apiType !== "Mongo";
|
||||||
|
this.shouldShowPartitionKeyEditor = userContext.apiType === "SQL" && isRunningOnPublicCloud();
|
||||||
|
|
||||||
this.changeFeedPolicyVisible = userContext.features.enableChangeFeedPolicy;
|
this.changeFeedPolicyVisible = userContext.features.enableChangeFeedPolicy;
|
||||||
|
|
||||||
@@ -1056,6 +1063,12 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
onConflictResolutionDirtyChange: this.onConflictResolutionDirtyChange,
|
onConflictResolutionDirtyChange: this.onConflictResolutionDirtyChange,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const partitionKeyComponentProps: PartitionKeyComponentProps = {
|
||||||
|
database: useDatabases.getState().findDatabaseWithId(this.collection.databaseId),
|
||||||
|
collection: this.collection,
|
||||||
|
explorer: this.props.settingsTab.getContainer(),
|
||||||
|
};
|
||||||
|
|
||||||
const tabs: SettingsV2TabInfo[] = [];
|
const tabs: SettingsV2TabInfo[] = [];
|
||||||
if (!hasDatabaseSharedThroughput(this.collection) && this.offer) {
|
if (!hasDatabaseSharedThroughput(this.collection) && this.offer) {
|
||||||
tabs.push({
|
tabs.push({
|
||||||
@@ -1091,6 +1104,13 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.shouldShowPartitionKeyEditor) {
|
||||||
|
tabs.push({
|
||||||
|
tab: SettingsV2TabTypes.PartitionKeyTab,
|
||||||
|
content: <PartitionKeyComponent {...partitionKeyComponentProps} />,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const pivotProps: IPivotProps = {
|
const pivotProps: IPivotProps = {
|
||||||
onLinkClick: this.onPivotChange,
|
onLinkClick: this.onPivotChange,
|
||||||
selectedKey: SettingsV2TabTypes[this.state.selectedTab],
|
selectedKey: SettingsV2TabTypes[this.state.selectedTab],
|
||||||
|
|||||||
@@ -0,0 +1,216 @@
|
|||||||
|
import {
|
||||||
|
DefaultButton,
|
||||||
|
FontWeights,
|
||||||
|
Link,
|
||||||
|
MessageBar,
|
||||||
|
MessageBarType,
|
||||||
|
PrimaryButton,
|
||||||
|
ProgressIndicator,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
} from "@fluentui/react";
|
||||||
|
import * as React from "react";
|
||||||
|
import * as ViewModels from "../../../../Contracts/ViewModels";
|
||||||
|
|
||||||
|
import { handleError } from "Common/ErrorHandlingUtils";
|
||||||
|
import { cancelDataTransferJob, pollDataTransferJob } from "Common/dataAccess/dataTransfers";
|
||||||
|
import Explorer from "Explorer/Explorer";
|
||||||
|
import { ChangePartitionKeyPane } from "Explorer/Panes/ChangePartitionKeyPane/ChangePartitionKeyPane";
|
||||||
|
import {
|
||||||
|
CosmosSqlDataTransferDataSourceSink,
|
||||||
|
DataTransferJobGetResults,
|
||||||
|
} from "Utils/arm/generatedClients/dataTransferService/types";
|
||||||
|
import { refreshDataTransferJobs, useDataTransferJobs } from "hooks/useDataTransferJobs";
|
||||||
|
import { useSidePanel } from "hooks/useSidePanel";
|
||||||
|
import { userContext } from "../../../../UserContext";
|
||||||
|
|
||||||
|
export interface PartitionKeyComponentProps {
|
||||||
|
database: ViewModels.Database;
|
||||||
|
collection: ViewModels.Collection;
|
||||||
|
explorer: Explorer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({ database, collection, explorer }) => {
|
||||||
|
const { dataTransferJobs } = useDataTransferJobs();
|
||||||
|
const [portalDataTransferJob, setPortalDataTransferJob] = React.useState<DataTransferJobGetResults>(null);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const loadDataTransferJobs = refreshDataTransferOperations;
|
||||||
|
loadDataTransferJobs();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const currentJob = findPortalDataTransferJob();
|
||||||
|
setPortalDataTransferJob(currentJob);
|
||||||
|
startPollingforUpdate(currentJob);
|
||||||
|
}, [dataTransferJobs]);
|
||||||
|
|
||||||
|
const isHierarchicalPartitionedContainer = (): boolean => collection.partitionKey?.kind === "MultiHash";
|
||||||
|
|
||||||
|
const getPartitionKeyValue = (): string => {
|
||||||
|
return (collection.partitionKeyProperties || []).map((property) => "/" + property).join(", ");
|
||||||
|
};
|
||||||
|
|
||||||
|
const partitionKeyName = "Partition key";
|
||||||
|
const partitionKeyValue = getPartitionKeyValue();
|
||||||
|
|
||||||
|
const textHeadingStyle = {
|
||||||
|
root: { fontWeight: FontWeights.semibold, fontSize: 16 },
|
||||||
|
};
|
||||||
|
|
||||||
|
const textSubHeadingStyle = {
|
||||||
|
root: { fontWeight: FontWeights.semibold },
|
||||||
|
};
|
||||||
|
|
||||||
|
const startPollingforUpdate = (currentJob: DataTransferJobGetResults) => {
|
||||||
|
if (isCurrentJobInProgress(currentJob)) {
|
||||||
|
const jobName = currentJob?.properties?.jobName;
|
||||||
|
try {
|
||||||
|
pollDataTransferJob(
|
||||||
|
jobName,
|
||||||
|
userContext.subscriptionId,
|
||||||
|
userContext.resourceGroup,
|
||||||
|
userContext.databaseAccount.name,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error, "ChangePartitionKey", `Failed to complete data transfer job ${jobName}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelRunningDataTransferJob = async (currentJob: DataTransferJobGetResults) => {
|
||||||
|
await cancelDataTransferJob(
|
||||||
|
userContext.subscriptionId,
|
||||||
|
userContext.resourceGroup,
|
||||||
|
userContext.databaseAccount.name,
|
||||||
|
currentJob?.properties?.jobName,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isCurrentJobInProgress = (currentJob: DataTransferJobGetResults) => {
|
||||||
|
const jobStatus = currentJob?.properties?.status;
|
||||||
|
return (
|
||||||
|
jobStatus &&
|
||||||
|
jobStatus !== "Completed" &&
|
||||||
|
jobStatus !== "Cancelled" &&
|
||||||
|
jobStatus !== "Failed" &&
|
||||||
|
jobStatus !== "Faulted"
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const refreshDataTransferOperations = async () => {
|
||||||
|
await refreshDataTransferJobs(
|
||||||
|
userContext.subscriptionId,
|
||||||
|
userContext.resourceGroup,
|
||||||
|
userContext.databaseAccount.name,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const findPortalDataTransferJob = (): DataTransferJobGetResults => {
|
||||||
|
return dataTransferJobs.find((feed: DataTransferJobGetResults) => {
|
||||||
|
const sourceSink: CosmosSqlDataTransferDataSourceSink = feed?.properties
|
||||||
|
?.source as CosmosSqlDataTransferDataSourceSink;
|
||||||
|
return sourceSink.databaseName === collection.databaseId && sourceSink.containerName === collection.id();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getProgressDescription = (): string => {
|
||||||
|
const processedCount = portalDataTransferJob?.properties?.processedCount;
|
||||||
|
const totalCount = portalDataTransferJob?.properties?.totalCount;
|
||||||
|
const processedCountString = totalCount > 0 ? `(${processedCount} of ${totalCount} documents processed)` : "";
|
||||||
|
return `${portalDataTransferJob?.properties?.status} ${processedCountString}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const startPartitionkeyChangeWorkflow = () => {
|
||||||
|
useSidePanel
|
||||||
|
.getState()
|
||||||
|
.openSidePanel(
|
||||||
|
"Change partition key",
|
||||||
|
<ChangePartitionKeyPane
|
||||||
|
sourceDatabase={database}
|
||||||
|
sourceCollection={collection}
|
||||||
|
explorer={explorer}
|
||||||
|
onClose={refreshDataTransferOperations}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPercentageComplete = () => {
|
||||||
|
const processedCount = portalDataTransferJob?.properties?.processedCount;
|
||||||
|
const totalCount = portalDataTransferJob?.properties?.totalCount;
|
||||||
|
const jobStatus = portalDataTransferJob?.properties?.status;
|
||||||
|
const isCancelled = jobStatus === "Cancelled";
|
||||||
|
const isCompleted = jobStatus === "Completed";
|
||||||
|
if (totalCount <= 0 && !isCompleted) {
|
||||||
|
return isCancelled ? 0 : null;
|
||||||
|
}
|
||||||
|
return isCompleted ? 1 : processedCount / totalCount;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack tokens={{ childrenGap: 20 }} styles={{ root: { maxWidth: 600 } }}>
|
||||||
|
<Stack tokens={{ childrenGap: 10 }}>
|
||||||
|
<Text styles={textHeadingStyle}>Change {partitionKeyName.toLowerCase()}</Text>
|
||||||
|
<Stack horizontal tokens={{ childrenGap: 20 }}>
|
||||||
|
<Stack tokens={{ childrenGap: 5 }}>
|
||||||
|
<Text styles={textSubHeadingStyle}>Current {partitionKeyName.toLowerCase()}</Text>
|
||||||
|
<Text styles={textSubHeadingStyle}>Partitioning</Text>
|
||||||
|
</Stack>
|
||||||
|
<Stack tokens={{ childrenGap: 5 }}>
|
||||||
|
<Text>{partitionKeyValue}</Text>
|
||||||
|
<Text>{isHierarchicalPartitionedContainer() ? "Hierarchical" : "Non-hierarchical"}</Text>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
<MessageBar messageBarType={MessageBarType.warning}>
|
||||||
|
To safeguard the integrity of the data being copied to the new container, ensure that no updates are made to the
|
||||||
|
source container for the entire duration of the partition key change process.
|
||||||
|
<Link
|
||||||
|
href="https://learn.microsoft.com/azure/cosmos-db/container-copy#how-does-container-copy-work"
|
||||||
|
target="_blank"
|
||||||
|
underline
|
||||||
|
>
|
||||||
|
Learn more
|
||||||
|
</Link>
|
||||||
|
</MessageBar>
|
||||||
|
<Text>
|
||||||
|
To change the partition key, a new destination container must be created or an existing destination container
|
||||||
|
selected. Data will then be copied to the destination container.
|
||||||
|
</Text>
|
||||||
|
<PrimaryButton
|
||||||
|
styles={{ root: { width: "fit-content" } }}
|
||||||
|
text="Change"
|
||||||
|
onClick={startPartitionkeyChangeWorkflow}
|
||||||
|
disabled={isCurrentJobInProgress(portalDataTransferJob)}
|
||||||
|
/>
|
||||||
|
{portalDataTransferJob && (
|
||||||
|
<Stack>
|
||||||
|
<Text styles={textHeadingStyle}>{partitionKeyName} change job</Text>
|
||||||
|
<Stack
|
||||||
|
horizontal
|
||||||
|
tokens={{ childrenGap: 20 }}
|
||||||
|
styles={{
|
||||||
|
root: {
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ProgressIndicator
|
||||||
|
label={portalDataTransferJob?.properties?.jobName}
|
||||||
|
description={getProgressDescription()}
|
||||||
|
percentComplete={getPercentageComplete()}
|
||||||
|
styles={{
|
||||||
|
root: {
|
||||||
|
width: "85%",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
></ProgressIndicator>
|
||||||
|
{isCurrentJobInProgress(portalDataTransferJob) && (
|
||||||
|
<DefaultButton text="Cancel" onClick={() => cancelRunningDataTransferJob(portalDataTransferJob)} />
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -306,7 +306,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
|||||||
};
|
};
|
||||||
|
|
||||||
const costElement = (): JSX.Element => {
|
const costElement = (): JSX.Element => {
|
||||||
const prices: PriceBreakdown = getRuPriceBreakdown(throughput, serverId, numberOfRegions, isMultimaster, true);
|
const prices: PriceBreakdown = getRuPriceBreakdown(throughput, serverId, numberOfRegions, isMultimaster, false);
|
||||||
return (
|
return (
|
||||||
<Stack {...checkBoxAndInputStackProps} style={{ marginTop: 15 }}>
|
<Stack {...checkBoxAndInputStackProps} style={{ marginTop: 15 }}>
|
||||||
{newThroughput && newThroughputCostElement()}
|
{newThroughput && newThroughputCostElement()}
|
||||||
|
|||||||
@@ -917,7 +917,7 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
|
|||||||
>
|
>
|
||||||
$
|
$
|
||||||
|
|
||||||
0.012
|
0.0080
|
||||||
/hr
|
/hr
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
@@ -929,7 +929,7 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
|
|||||||
>
|
>
|
||||||
$
|
$
|
||||||
|
|
||||||
0.29
|
0.19
|
||||||
/day
|
/day
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
@@ -941,7 +941,7 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
|
|||||||
>
|
>
|
||||||
$
|
$
|
||||||
|
|
||||||
8.76
|
5.84
|
||||||
/mo
|
/mo
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -1354,7 +1354,7 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = `
|
|||||||
>
|
>
|
||||||
$
|
$
|
||||||
|
|
||||||
0.012
|
0.0080
|
||||||
/hr
|
/hr
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
@@ -1366,7 +1366,7 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = `
|
|||||||
>
|
>
|
||||||
$
|
$
|
||||||
|
|
||||||
0.29
|
0.19
|
||||||
/day
|
/day
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
@@ -1378,7 +1378,7 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = `
|
|||||||
>
|
>
|
||||||
$
|
$
|
||||||
|
|
||||||
8.76
|
5.84
|
||||||
/mo
|
/mo
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ export enum SettingsV2TabTypes {
|
|||||||
ConflictResolutionTab,
|
ConflictResolutionTab,
|
||||||
SubSettingsTab,
|
SubSettingsTab,
|
||||||
IndexingPolicyTab,
|
IndexingPolicyTab,
|
||||||
|
PartitionKeyTab,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IsComponentDirtyResult {
|
export interface IsComponentDirtyResult {
|
||||||
@@ -146,6 +147,8 @@ export const getTabTitle = (tab: SettingsV2TabTypes): string => {
|
|||||||
return "Settings";
|
return "Settings";
|
||||||
case SettingsV2TabTypes.IndexingPolicyTab:
|
case SettingsV2TabTypes.IndexingPolicyTab:
|
||||||
return "Indexing Policy";
|
return "Indexing Policy";
|
||||||
|
case SettingsV2TabTypes.PartitionKeyTab:
|
||||||
|
return "Partition Keys";
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unknown tab ${tab}`);
|
throw new Error(`Unknown tab ${tab}`);
|
||||||
}
|
}
|
||||||
@@ -199,3 +202,49 @@ export const getMongoIndexTypeText = (index: MongoIndexTypes): string => {
|
|||||||
export const isIndexTransforming = (indexTransformationProgress: number): boolean =>
|
export const isIndexTransforming = (indexTransformationProgress: number): boolean =>
|
||||||
// index transformation progress can be 0
|
// index transformation progress can be 0
|
||||||
indexTransformationProgress !== undefined && indexTransformationProgress !== 100;
|
indexTransformationProgress !== undefined && indexTransformationProgress !== 100;
|
||||||
|
|
||||||
|
export const getPartitionKeyName = (apiType: string, isLowerCase?: boolean): string => {
|
||||||
|
const partitionKeyName = apiType === "Mongo" ? "Shard key" : "Partition key";
|
||||||
|
return isLowerCase ? partitionKeyName.toLocaleLowerCase() : partitionKeyName;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getPartitionKeyTooltipText = (apiType: string): string => {
|
||||||
|
if (apiType === "Mongo") {
|
||||||
|
return "The shard key (field) is used to split your data across many replica sets (shards) to achieve unlimited scalability. It’s critical to choose a field that will evenly distribute your data.";
|
||||||
|
}
|
||||||
|
let tooltipText = `The ${getPartitionKeyName(
|
||||||
|
apiType,
|
||||||
|
true,
|
||||||
|
)} is used to automatically distribute data across partitions for scalability. Choose a property in your JSON document that has a wide range of values and evenly distributes request volume.`;
|
||||||
|
if (apiType === "SQL") {
|
||||||
|
tooltipText += " For small read-heavy workloads or write-heavy workloads of any size, id is often a good choice.";
|
||||||
|
}
|
||||||
|
return tooltipText;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getPartitionKeySubtext = (partitionKeyDefault: boolean, apiType: string): string => {
|
||||||
|
if (partitionKeyDefault && (apiType === "SQL" || apiType === "Mongo")) {
|
||||||
|
const subtext = "For small workloads, the item ID is a suitable choice for the partition key.";
|
||||||
|
return subtext;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getPartitionKeyPlaceHolder = (apiType: string, index?: number): string => {
|
||||||
|
switch (apiType) {
|
||||||
|
case "Mongo":
|
||||||
|
return "e.g., categoryId";
|
||||||
|
case "Gremlin":
|
||||||
|
return "e.g., /address";
|
||||||
|
case "SQL":
|
||||||
|
return `${
|
||||||
|
index === undefined
|
||||||
|
? "Required - first partition key e.g., /TenantId"
|
||||||
|
: index === 0
|
||||||
|
? "second partition key e.g., /UserId"
|
||||||
|
: "third partition key e.g., /SessionId"
|
||||||
|
}`;
|
||||||
|
default:
|
||||||
|
return "e.g., /address/zipCode";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -204,6 +204,98 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
shouldDiscardIndexingPolicy={false}
|
shouldDiscardIndexingPolicy={false}
|
||||||
/>
|
/>
|
||||||
</PivotItem>
|
</PivotItem>
|
||||||
|
<PivotItem
|
||||||
|
headerText="Partition Keys"
|
||||||
|
itemKey="PartitionKeyTab"
|
||||||
|
key="PartitionKeyTab"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"marginTop": 20,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<PartitionKeyComponent
|
||||||
|
collection={
|
||||||
|
Object {
|
||||||
|
"analyticalStorageTtl": [Function],
|
||||||
|
"changeFeedPolicy": [Function],
|
||||||
|
"conflictResolutionPolicy": [Function],
|
||||||
|
"container": Explorer {
|
||||||
|
"_isInitializingNotebooks": false,
|
||||||
|
"_resetNotebookWorkspace": [Function],
|
||||||
|
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||||
|
"isTabsContentExpanded": [Function],
|
||||||
|
"onRefreshDatabasesKeyPress": [Function],
|
||||||
|
"onRefreshResourcesClick": [Function],
|
||||||
|
"phoenixClient": PhoenixClient {
|
||||||
|
"armResourceId": undefined,
|
||||||
|
"retryOptions": Object {
|
||||||
|
"maxTimeout": 5000,
|
||||||
|
"minTimeout": 5000,
|
||||||
|
"retries": 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"provideFeedbackEmail": [Function],
|
||||||
|
"queriesClient": QueriesClient {
|
||||||
|
"container": [Circular],
|
||||||
|
},
|
||||||
|
"refreshNotebookList": [Function],
|
||||||
|
"resourceTree": ResourceTreeAdapter {
|
||||||
|
"container": [Circular],
|
||||||
|
"copyNotebook": [Function],
|
||||||
|
"parameters": [Function],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"databaseId": "test",
|
||||||
|
"defaultTtl": [Function],
|
||||||
|
"geospatialConfig": [Function],
|
||||||
|
"getDatabase": [Function],
|
||||||
|
"id": [Function],
|
||||||
|
"indexingPolicy": [Function],
|
||||||
|
"offer": [Function],
|
||||||
|
"partitionKey": Object {
|
||||||
|
"kind": "hash",
|
||||||
|
"paths": Array [],
|
||||||
|
"version": 2,
|
||||||
|
},
|
||||||
|
"partitionKeyProperties": Array [
|
||||||
|
"partitionKey",
|
||||||
|
],
|
||||||
|
"readSettings": [Function],
|
||||||
|
"uniqueKeyPolicy": Object {},
|
||||||
|
"usageSizeInKB": [Function],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
explorer={
|
||||||
|
Explorer {
|
||||||
|
"_isInitializingNotebooks": false,
|
||||||
|
"_resetNotebookWorkspace": [Function],
|
||||||
|
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||||
|
"isTabsContentExpanded": [Function],
|
||||||
|
"onRefreshDatabasesKeyPress": [Function],
|
||||||
|
"onRefreshResourcesClick": [Function],
|
||||||
|
"phoenixClient": PhoenixClient {
|
||||||
|
"armResourceId": undefined,
|
||||||
|
"retryOptions": Object {
|
||||||
|
"maxTimeout": 5000,
|
||||||
|
"minTimeout": 5000,
|
||||||
|
"retries": 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"provideFeedbackEmail": [Function],
|
||||||
|
"queriesClient": QueriesClient {
|
||||||
|
"container": [Circular],
|
||||||
|
},
|
||||||
|
"refreshNotebookList": [Function],
|
||||||
|
"resourceTree": ResourceTreeAdapter {
|
||||||
|
"container": [Circular],
|
||||||
|
"copyNotebook": [Function],
|
||||||
|
"parameters": [Function],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</PivotItem>
|
||||||
</StyledPivot>
|
</StyledPivot>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -196,18 +196,22 @@ export function createContextCommandBarButtons(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function createControlCommandBarButtons(container: Explorer): CommandButtonComponentProps[] {
|
export function createControlCommandBarButtons(container: Explorer): CommandButtonComponentProps[] {
|
||||||
const buttons: CommandButtonComponentProps[] = [
|
const buttons: CommandButtonComponentProps[] =
|
||||||
{
|
configContext.platform === Platform.Fabric && userContext.fabricContext?.isReadOnly
|
||||||
iconSrc: SettingsIcon,
|
? []
|
||||||
iconAlt: "Settings",
|
: [
|
||||||
onCommandClick: () => useSidePanel.getState().openSidePanel("Settings", <SettingsPane explorer={container} />),
|
{
|
||||||
commandButtonLabel: undefined,
|
iconSrc: SettingsIcon,
|
||||||
ariaLabel: "Settings",
|
iconAlt: "Settings",
|
||||||
tooltipText: "Settings",
|
onCommandClick: () =>
|
||||||
hasPopup: true,
|
useSidePanel.getState().openSidePanel("Settings", <SettingsPane explorer={container} />),
|
||||||
disabled: false,
|
commandButtonLabel: undefined,
|
||||||
},
|
ariaLabel: "Settings",
|
||||||
];
|
tooltipText: "Settings",
|
||||||
|
hasPopup: true,
|
||||||
|
disabled: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const showOpenFullScreen =
|
const showOpenFullScreen =
|
||||||
configContext.platform === Platform.Portal && !isRunningOnNationalCloud() && userContext.apiType !== "Gremlin";
|
configContext.platform === Platform.Portal && !isRunningOnNationalCloud() && userContext.apiType !== "Gremlin";
|
||||||
|
|||||||
@@ -0,0 +1,396 @@
|
|||||||
|
import {
|
||||||
|
DefaultButton,
|
||||||
|
DirectionalHint,
|
||||||
|
Dropdown,
|
||||||
|
IDropdownOption,
|
||||||
|
Icon,
|
||||||
|
IconButton,
|
||||||
|
Link,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
TooltipHost,
|
||||||
|
} from "@fluentui/react";
|
||||||
|
import * as Constants from "Common/Constants";
|
||||||
|
import { handleError } from "Common/ErrorHandlingUtils";
|
||||||
|
import { createCollection } from "Common/dataAccess/createCollection";
|
||||||
|
import { DataTransferParams, initiateDataTransfer } from "Common/dataAccess/dataTransfers";
|
||||||
|
import * as DataModels from "Contracts/DataModels";
|
||||||
|
import * as ViewModels from "Contracts/ViewModels";
|
||||||
|
import {
|
||||||
|
getPartitionKeyName,
|
||||||
|
getPartitionKeyPlaceHolder,
|
||||||
|
getPartitionKeySubtext,
|
||||||
|
getPartitionKeyTooltipText,
|
||||||
|
} from "Explorer/Controls/Settings/SettingsUtils";
|
||||||
|
import Explorer from "Explorer/Explorer";
|
||||||
|
import { RightPaneForm } from "Explorer/Panes/RightPaneForm/RightPaneForm";
|
||||||
|
import { useDatabases } from "Explorer/useDatabases";
|
||||||
|
import { userContext } from "UserContext";
|
||||||
|
import { getCollectionName } from "Utils/APITypeUtils";
|
||||||
|
import { useSidePanel } from "hooks/useSidePanel";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
export interface ChangePartitionKeyPaneProps {
|
||||||
|
sourceDatabase: ViewModels.Database;
|
||||||
|
sourceCollection: ViewModels.Collection;
|
||||||
|
explorer: Explorer;
|
||||||
|
onClose: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
|
||||||
|
sourceDatabase,
|
||||||
|
sourceCollection,
|
||||||
|
explorer,
|
||||||
|
onClose,
|
||||||
|
}) => {
|
||||||
|
const [targetCollectionId, setTargetCollectionId] = React.useState<string>();
|
||||||
|
const [createNewContainer, setCreateNewContainer] = React.useState<boolean>(true);
|
||||||
|
const [formError, setFormError] = React.useState<string>();
|
||||||
|
const [isExecuting, setIsExecuting] = React.useState<boolean>(false);
|
||||||
|
const [subPartitionKeys, setSubPartitionKeys] = React.useState<string[]>([]);
|
||||||
|
const [partitionKey, setPartitionKey] = React.useState<string>();
|
||||||
|
|
||||||
|
const getCollectionOptions = (): IDropdownOption[] => {
|
||||||
|
return sourceDatabase
|
||||||
|
.collections()
|
||||||
|
.filter((collection) => collection.id !== sourceCollection.id)
|
||||||
|
.map((collection) => ({
|
||||||
|
key: collection.id(),
|
||||||
|
text: collection.id(),
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const submit = async () => {
|
||||||
|
if (!validateInputs()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsExecuting(true);
|
||||||
|
try {
|
||||||
|
createNewContainer && (await createContainer());
|
||||||
|
await createDataTransferJob();
|
||||||
|
await onClose();
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error, "ChangePartitionKey", "Failed to start data transfer job");
|
||||||
|
}
|
||||||
|
setIsExecuting(false);
|
||||||
|
useSidePanel.getState().closeSidePanel();
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateInputs = (): boolean => {
|
||||||
|
if (!createNewContainer && !targetCollectionId) {
|
||||||
|
setFormError("Choose an existing container");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createDataTransferJob = async () => {
|
||||||
|
const jobName = `Portal_${targetCollectionId}_${Math.floor(Date.now() / 1000)}`;
|
||||||
|
const dataTransferParams: DataTransferParams = {
|
||||||
|
jobName,
|
||||||
|
apiType: userContext.apiType,
|
||||||
|
subscriptionId: userContext.subscriptionId,
|
||||||
|
resourceGroupName: userContext.resourceGroup,
|
||||||
|
accountName: userContext.databaseAccount.name,
|
||||||
|
sourceDatabaseName: sourceDatabase.id(),
|
||||||
|
sourceCollectionName: sourceCollection.id(),
|
||||||
|
targetDatabaseName: sourceDatabase.id(),
|
||||||
|
targetCollectionName: targetCollectionId,
|
||||||
|
};
|
||||||
|
await initiateDataTransfer(dataTransferParams);
|
||||||
|
};
|
||||||
|
|
||||||
|
const createContainer = async () => {
|
||||||
|
const partitionKeyString = partitionKey.trim();
|
||||||
|
const partitionKeyData: DataModels.PartitionKey = partitionKeyString
|
||||||
|
? {
|
||||||
|
paths: [partitionKeyString, ...(subPartitionKeys.length > 0 ? subPartitionKeys : [])],
|
||||||
|
kind: subPartitionKeys.length > 0 ? "MultiHash" : "Hash",
|
||||||
|
version: 2,
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const createCollectionParams: DataModels.CreateCollectionParams = {
|
||||||
|
createNewDatabase: false,
|
||||||
|
collectionId: targetCollectionId,
|
||||||
|
databaseId: sourceDatabase.id(),
|
||||||
|
databaseLevelThroughput: isSelectedDatabaseSharedThroughput(),
|
||||||
|
offerThroughput: sourceCollection.offer()?.manualThroughput,
|
||||||
|
autoPilotMaxThroughput: sourceCollection.offer()?.autoscaleMaxThroughput,
|
||||||
|
partitionKey: partitionKeyData,
|
||||||
|
};
|
||||||
|
await createCollection(createCollectionParams);
|
||||||
|
await explorer.refreshAllDatabases();
|
||||||
|
};
|
||||||
|
|
||||||
|
const isSelectedDatabaseSharedThroughput = (): boolean => {
|
||||||
|
const selectedDatabase = useDatabases
|
||||||
|
.getState()
|
||||||
|
.databases?.find((database) => database.id() === sourceDatabase.id());
|
||||||
|
return !!selectedDatabase?.offer();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RightPaneForm formError={formError} isExecuting={isExecuting} onSubmit={submit} submitButtonText="OK">
|
||||||
|
<Stack tokens={{ childrenGap: 10 }} className="panelMainContent">
|
||||||
|
<Text variant="small">
|
||||||
|
When changing a container’s partition key, you will need to create a destination container with the correct
|
||||||
|
partition key. You may also select an existing destination container.
|
||||||
|
<Link
|
||||||
|
href="https://learn.microsoft.com/en-us/azure/cosmos-db/container-copy#container-copy-within-an-azure-cosmos-db-account"
|
||||||
|
target="_blank"
|
||||||
|
underline
|
||||||
|
>
|
||||||
|
Learn more
|
||||||
|
</Link>
|
||||||
|
</Text>
|
||||||
|
<Stack>
|
||||||
|
<Stack horizontal>
|
||||||
|
<span className="mandatoryStar">* </span>
|
||||||
|
<Text className="panelTextBold" variant="small">
|
||||||
|
Database id
|
||||||
|
</Text>
|
||||||
|
<TooltipHost
|
||||||
|
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||||
|
content={`A database is analogous to a namespace. It is the unit of management for a set of ${getCollectionName(
|
||||||
|
true,
|
||||||
|
).toLocaleLowerCase()}.`}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
iconName="Info"
|
||||||
|
className="panelInfoIcon"
|
||||||
|
tabIndex={0}
|
||||||
|
aria-label={`A database is analogous to a namespace. It is the unit of management for a set of ${getCollectionName(
|
||||||
|
true,
|
||||||
|
).toLocaleLowerCase()}.`}
|
||||||
|
/>
|
||||||
|
</TooltipHost>
|
||||||
|
</Stack>
|
||||||
|
<Dropdown
|
||||||
|
styles={{ title: { height: 27, lineHeight: 27 }, dropdownItem: { fontSize: 12 } }}
|
||||||
|
style={{ width: 300, fontSize: 12 }}
|
||||||
|
options={[]}
|
||||||
|
placeholder={sourceDatabase.id()}
|
||||||
|
responsiveMode={999}
|
||||||
|
disabled={true}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
<Stack className="panelGroupSpacing" horizontal verticalAlign="center">
|
||||||
|
<div role="radiogroup">
|
||||||
|
<input
|
||||||
|
className="panelRadioBtn"
|
||||||
|
checked={createNewContainer}
|
||||||
|
aria-label="Create new container"
|
||||||
|
aria-checked={createNewContainer}
|
||||||
|
name="containerType"
|
||||||
|
type="radio"
|
||||||
|
role="radio"
|
||||||
|
id="containerCreateNew"
|
||||||
|
tabIndex={0}
|
||||||
|
onChange={() => setCreateNewContainer(true)}
|
||||||
|
/>
|
||||||
|
<span className="panelRadioBtnLabel">New container</span>
|
||||||
|
|
||||||
|
<input
|
||||||
|
className="panelRadioBtn"
|
||||||
|
checked={!createNewContainer}
|
||||||
|
aria-label="Use existing container"
|
||||||
|
aria-checked={!createNewContainer}
|
||||||
|
name="containerType"
|
||||||
|
type="radio"
|
||||||
|
role="radio"
|
||||||
|
tabIndex={0}
|
||||||
|
onChange={() => setCreateNewContainer(false)}
|
||||||
|
/>
|
||||||
|
<span className="panelRadioBtnLabel">Existing container</span>
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
{createNewContainer ? (
|
||||||
|
<Stack>
|
||||||
|
<Stack className="panelGroupSpacing">
|
||||||
|
<Stack horizontal>
|
||||||
|
<span className="mandatoryStar">* </span>
|
||||||
|
<Text className="panelTextBold" variant="small">
|
||||||
|
{`${getCollectionName()} id`}
|
||||||
|
</Text>
|
||||||
|
<TooltipHost
|
||||||
|
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||||
|
content={`Unique identifier for the ${getCollectionName().toLocaleLowerCase()} and used for id-based routing through REST and all SDKs.`}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
role="button"
|
||||||
|
iconName="Info"
|
||||||
|
className="panelInfoIcon"
|
||||||
|
tabIndex={0}
|
||||||
|
ariaLabel={`Unique identifier for the ${getCollectionName().toLocaleLowerCase()} and used for id-based routing through REST and all SDKs.`}
|
||||||
|
/>
|
||||||
|
</TooltipHost>
|
||||||
|
</Stack>
|
||||||
|
<input
|
||||||
|
name="collectionId"
|
||||||
|
id="collectionId"
|
||||||
|
type="text"
|
||||||
|
aria-required
|
||||||
|
required
|
||||||
|
autoComplete="off"
|
||||||
|
pattern="[^/?#\\]*[^/?# \\]"
|
||||||
|
title="May not end with space nor contain characters '\' '/' '#' '?'"
|
||||||
|
placeholder={`e.g., ${getCollectionName()}1`}
|
||||||
|
size={40}
|
||||||
|
className="panelTextField"
|
||||||
|
aria-label={`${getCollectionName()} id, Example ${getCollectionName()}1`}
|
||||||
|
value={targetCollectionId}
|
||||||
|
onChange={(event: React.ChangeEvent<HTMLInputElement>) => setTargetCollectionId(event.target.value)}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
<Stack tokens={{ childrenGap: 10 }}>
|
||||||
|
<Stack horizontal>
|
||||||
|
<span className="mandatoryStar">* </span>
|
||||||
|
<Text className="panelTextBold" variant="small">
|
||||||
|
{getPartitionKeyName(userContext.apiType)}
|
||||||
|
</Text>
|
||||||
|
<TooltipHost
|
||||||
|
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||||
|
content={getPartitionKeyTooltipText(userContext.apiType)}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
iconName="Info"
|
||||||
|
className="panelInfoIcon"
|
||||||
|
tabIndex={0}
|
||||||
|
aria-label={getPartitionKeyTooltipText(userContext.apiType)}
|
||||||
|
/>
|
||||||
|
</TooltipHost>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Text variant="small" aria-label="pkDescription">
|
||||||
|
{getPartitionKeySubtext(userContext.features.partitionKeyDefault, userContext.apiType)}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="addCollection-partitionKeyValue"
|
||||||
|
aria-required
|
||||||
|
required
|
||||||
|
size={40}
|
||||||
|
className="panelTextField"
|
||||||
|
placeholder={getPartitionKeyPlaceHolder(userContext.apiType)}
|
||||||
|
aria-label={getPartitionKeyName(userContext.apiType)}
|
||||||
|
pattern={".*"}
|
||||||
|
title={""}
|
||||||
|
value={partitionKey}
|
||||||
|
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
if (!partitionKey && !event.target.value.startsWith("/")) {
|
||||||
|
setPartitionKey("/" + event.target.value);
|
||||||
|
} else {
|
||||||
|
setPartitionKey(event.target.value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{subPartitionKeys.map((subPartitionKey: string, index: number) => {
|
||||||
|
return (
|
||||||
|
<Stack style={{ marginBottom: 8 }} key={`uniqueKey${index}`} horizontal>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: "20px",
|
||||||
|
border: "solid",
|
||||||
|
borderWidth: "0px 0px 1px 1px",
|
||||||
|
marginRight: "5px",
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="addCollection-partitionKeyValue"
|
||||||
|
key={`addCollection-partitionKeyValue_${index}`}
|
||||||
|
aria-required
|
||||||
|
required
|
||||||
|
size={40}
|
||||||
|
tabIndex={index > 0 ? 1 : 0}
|
||||||
|
className="panelTextField"
|
||||||
|
autoComplete="off"
|
||||||
|
placeholder={getPartitionKeyPlaceHolder(userContext.apiType, index)}
|
||||||
|
aria-label={getPartitionKeyName(userContext.apiType)}
|
||||||
|
pattern={".*"}
|
||||||
|
title={""}
|
||||||
|
value={subPartitionKey}
|
||||||
|
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const keys = [...subPartitionKeys];
|
||||||
|
if (!keys[index] && !event.target.value.startsWith("/")) {
|
||||||
|
keys[index] = "/" + event.target.value.trim();
|
||||||
|
setSubPartitionKeys(keys);
|
||||||
|
} else {
|
||||||
|
keys[index] = event.target.value.trim();
|
||||||
|
setSubPartitionKeys(keys);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
iconProps={{ iconName: "Delete" }}
|
||||||
|
style={{ height: 27 }}
|
||||||
|
onClick={() => {
|
||||||
|
const keys = subPartitionKeys.filter((uniqueKey, j) => index !== j);
|
||||||
|
setSubPartitionKeys(keys);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<Stack className="panelGroupSpacing">
|
||||||
|
<DefaultButton
|
||||||
|
styles={{ root: { padding: 0, width: 200, height: 30 }, label: { fontSize: 12 } }}
|
||||||
|
disabled={subPartitionKeys.length >= Constants.BackendDefaults.maxNumMultiHashPartition}
|
||||||
|
onClick={() => setSubPartitionKeys([...subPartitionKeys, ""])}
|
||||||
|
>
|
||||||
|
Add hierarchical partition key
|
||||||
|
</DefaultButton>
|
||||||
|
{subPartitionKeys.length > 0 && (
|
||||||
|
<Text variant="small">
|
||||||
|
<Icon iconName="InfoSolid" className="removeIcon" tabIndex={0} /> This feature allows you to
|
||||||
|
partition your data with up to three levels of keys for better data distribution. Requires .NET V3,
|
||||||
|
Java V4 SDK, or preview JavaScript V3 SDK.{" "}
|
||||||
|
<Link href="https://aka.ms/cosmos-hierarchical-partitioning" target="_blank">
|
||||||
|
Learn more
|
||||||
|
</Link>
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
) : (
|
||||||
|
<Stack>
|
||||||
|
<Stack horizontal>
|
||||||
|
<span className="mandatoryStar">* </span>
|
||||||
|
<Text className="panelTextBold" variant="small">
|
||||||
|
{`${getCollectionName()}`}
|
||||||
|
</Text>
|
||||||
|
<TooltipHost
|
||||||
|
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||||
|
content={`Unique identifier for the ${getCollectionName().toLocaleLowerCase()} and used for id-based routing through REST and all SDKs.`}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
role="button"
|
||||||
|
iconName="Info"
|
||||||
|
className="panelInfoIcon"
|
||||||
|
tabIndex={0}
|
||||||
|
ariaLabel={`Unique identifier for the ${getCollectionName().toLocaleLowerCase()} and used for id-based routing through REST and all SDKs.`}
|
||||||
|
/>
|
||||||
|
</TooltipHost>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Dropdown
|
||||||
|
styles={{ title: { height: 27, lineHeight: 27 }, dropdownItem: { fontSize: 12 } }}
|
||||||
|
style={{ width: 300, fontSize: 12 }}
|
||||||
|
placeholder="Choose an existing container"
|
||||||
|
options={getCollectionOptions()}
|
||||||
|
onChange={(event: React.FormEvent<HTMLDivElement>, collection: IDropdownOption) => {
|
||||||
|
setTargetCollectionId(collection.key as string);
|
||||||
|
setFormError("");
|
||||||
|
}}
|
||||||
|
defaultSelectedKey={targetCollectionId}
|
||||||
|
responsiveMode={999}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</RightPaneForm>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -24,9 +24,7 @@ export const QuickstartCarousel: React.FC<QuickstartCarouselProps> = ({
|
|||||||
>
|
>
|
||||||
<Stack>
|
<Stack>
|
||||||
<Stack horizontal horizontalAlign="space-between" style={{ padding: 16 }}>
|
<Stack horizontal horizontalAlign="space-between" style={{ padding: 16 }}>
|
||||||
<Text role="heading" aria-level={1} variant="xLarge">
|
<Text variant="xLarge">{getHeaderText(page)}</Text>
|
||||||
{getHeaderText(page)}
|
|
||||||
</Text>
|
|
||||||
<IconButton iconProps={{ iconName: "Cancel" }} onClick={() => setPage(4)} ariaLabel="Close" />
|
<IconButton iconProps={{ iconName: "Cancel" }} onClick={() => setPage(4)} ariaLabel="Close" />
|
||||||
</Stack>
|
</Stack>
|
||||||
{getContent(page)}
|
{getContent(page)}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { ItemDefinition, PartitionKey, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
import { ItemDefinition, PartitionKey, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
||||||
|
import { Platform, configContext } from "ConfigContext";
|
||||||
import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
||||||
import { QueryConstants } from "Shared/Constants";
|
import { QueryConstants } from "Shared/Constants";
|
||||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||||
@@ -881,7 +882,7 @@ export default class DocumentsTab extends TabsBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected getTabsButtons(): CommandButtonComponentProps[] {
|
protected getTabsButtons(): CommandButtonComponentProps[] {
|
||||||
if (!userContext.hasWriteAccess) {
|
if (configContext.platform === Platform.Fabric && userContext.fabricContext?.isReadOnly) {
|
||||||
// All the following buttons require write access
|
// All the following buttons require write access
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { MessageBar, MessageBarButton, MessageBarType } from "@fluentui/react";
|
import { Link, MessageBar, MessageBarButton, MessageBarType } from "@fluentui/react";
|
||||||
|
import { CassandraProxyEndpoints, MongoProxyEndpoints } from "Common/Constants";
|
||||||
import { sendMessage } from "Common/MessageHandler";
|
import { sendMessage } from "Common/MessageHandler";
|
||||||
|
import { configContext, updateConfigContext } from "ConfigContext";
|
||||||
|
import { IpRule } from "Contracts/DataModels";
|
||||||
import { MessageTypes } from "Contracts/ExplorerContracts";
|
import { MessageTypes } from "Contracts/ExplorerContracts";
|
||||||
import { CollectionTabKind } from "Contracts/ViewModels";
|
import { CollectionTabKind } from "Contracts/ViewModels";
|
||||||
import Explorer from "Explorer/Explorer";
|
import Explorer from "Explorer/Explorer";
|
||||||
@@ -12,6 +15,7 @@ import { VcoreMongoConnectTab } from "Explorer/Tabs/VCoreMongoConnectTab";
|
|||||||
import { VcoreMongoQuickstartTab } from "Explorer/Tabs/VCoreMongoQuickstartTab";
|
import { VcoreMongoQuickstartTab } from "Explorer/Tabs/VCoreMongoQuickstartTab";
|
||||||
import { hasRUThresholdBeenConfigured } from "Shared/StorageUtility";
|
import { hasRUThresholdBeenConfigured } from "Shared/StorageUtility";
|
||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
|
import { CassandraProxyOutboundIPs, MongoProxyOutboundIPs, PortalBackendIPs } from "Utils/EndpointUtils";
|
||||||
import { useTeachingBubble } from "hooks/useTeachingBubble";
|
import { useTeachingBubble } from "hooks/useTeachingBubble";
|
||||||
import ko from "knockout";
|
import ko from "knockout";
|
||||||
import React, { MutableRefObject, useEffect, useRef, useState } from "react";
|
import React, { MutableRefObject, useEffect, useRef, useState } from "react";
|
||||||
@@ -33,6 +37,10 @@ export const Tabs = ({ explorer }: TabsProps): JSX.Element => {
|
|||||||
const [showRUThresholdMessageBar, setShowRUThresholdMessageBar] = useState<boolean>(
|
const [showRUThresholdMessageBar, setShowRUThresholdMessageBar] = useState<boolean>(
|
||||||
userContext.apiType === "SQL" && !hasRUThresholdBeenConfigured(),
|
userContext.apiType === "SQL" && !hasRUThresholdBeenConfigured(),
|
||||||
);
|
);
|
||||||
|
const [
|
||||||
|
showMongoAndCassandraProxiesNetworkSettingsWarningState,
|
||||||
|
setShowMongoAndCassandraProxiesNetworkSettingsWarningState,
|
||||||
|
] = useState<boolean>(showMongoAndCassandraProxiesNetworkSettingsWarning());
|
||||||
return (
|
return (
|
||||||
<div className="tabsManagerContainer">
|
<div className="tabsManagerContainer">
|
||||||
{networkSettingsWarning && (
|
{networkSettingsWarning && (
|
||||||
@@ -69,9 +77,25 @@ export const Tabs = ({ explorer }: TabsProps): JSX.Element => {
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{
|
{`To prevent queries from using excessive RUs, Data Explorer has a 5,000 RU default limit. To modify or remove
|
||||||
"To prevent queries from using excessive RUs, Data Explorer has a 5,000 RU default limit. To modify or remove the limit, go to the Settings cog on the right and find 'RU Threshold'."
|
the limit, go to the Settings cog on the right and find "RU Threshold".`}
|
||||||
}
|
<Link
|
||||||
|
href="https://review.learn.microsoft.com/en-us/azure/cosmos-db/data-explorer?branch=main#configure-request-unit-threshold"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
Learn More
|
||||||
|
</Link>
|
||||||
|
</MessageBar>
|
||||||
|
)}
|
||||||
|
{showMongoAndCassandraProxiesNetworkSettingsWarningState && (
|
||||||
|
<MessageBar
|
||||||
|
messageBarType={MessageBarType.warning}
|
||||||
|
onDismiss={() => {
|
||||||
|
setShowMongoAndCassandraProxiesNetworkSettingsWarningState(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{`We are moving our middleware to new infrastructure. To avoid future issues with Data Explorer access, please
|
||||||
|
re-enable "Allow access from Azure Portal" on the Networking blade for your account.`}
|
||||||
</MessageBar>
|
</MessageBar>
|
||||||
)}
|
)}
|
||||||
<div id="content" className="flexContainer hideOverflows">
|
<div id="content" className="flexContainer hideOverflows">
|
||||||
@@ -299,3 +323,59 @@ const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): J
|
|||||||
throw Error(`Unsupported tab kind ${ReactTabKind[activeReactTab]}`);
|
throw Error(`Unsupported tab kind ${ReactTabKind[activeReactTab]}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const showMongoAndCassandraProxiesNetworkSettingsWarning = (): boolean => {
|
||||||
|
const ipRules: IpRule[] = userContext.databaseAccount?.properties?.ipRules;
|
||||||
|
if ((userContext.apiType === "Mongo" || userContext.apiType === "Cassandra") && ipRules?.length) {
|
||||||
|
const legacyPortalBackendIPs: string[] = PortalBackendIPs[configContext.BACKEND_ENDPOINT];
|
||||||
|
const ipAddressesFromIPRules: string[] = ipRules.map((ipRule) => ipRule.ipAddressOrRange);
|
||||||
|
const ipRulesIncludeLegacyPortalBackend: boolean =
|
||||||
|
ipAddressesFromIPRules.filter((ipAddressFromIPRule) => legacyPortalBackendIPs.includes(ipAddressFromIPRule))
|
||||||
|
?.length === legacyPortalBackendIPs.length;
|
||||||
|
|
||||||
|
if (!ipRulesIncludeLegacyPortalBackend) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userContext.apiType === "Mongo") {
|
||||||
|
const isProdOrMpacMongoProxyEndpoint: boolean = [MongoProxyEndpoints.Mpac, MongoProxyEndpoints.Prod].includes(
|
||||||
|
configContext.MONGO_PROXY_ENDPOINT,
|
||||||
|
);
|
||||||
|
|
||||||
|
const mongoProxyOutboundIPs: string[] = isProdOrMpacMongoProxyEndpoint
|
||||||
|
? [...MongoProxyOutboundIPs[MongoProxyEndpoints.Mpac], ...MongoProxyOutboundIPs[MongoProxyEndpoints.Prod]]
|
||||||
|
: MongoProxyOutboundIPs[configContext.MONGO_PROXY_ENDPOINT];
|
||||||
|
|
||||||
|
const ipRulesIncludeMongoProxy: boolean =
|
||||||
|
ipAddressesFromIPRules.filter((ipAddressFromIPRule) => mongoProxyOutboundIPs.includes(ipAddressFromIPRule))
|
||||||
|
?.length === mongoProxyOutboundIPs.length;
|
||||||
|
|
||||||
|
if (ipRulesIncludeMongoProxy) {
|
||||||
|
updateConfigContext({
|
||||||
|
MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return !ipRulesIncludeMongoProxy;
|
||||||
|
} else if (userContext.apiType === "Cassandra") {
|
||||||
|
const isProdOrMpacCassandraProxyEndpoint: boolean = [
|
||||||
|
CassandraProxyEndpoints.Mpac,
|
||||||
|
CassandraProxyEndpoints.Prod,
|
||||||
|
].includes(configContext.CASSANDRA_PROXY_ENDPOINT);
|
||||||
|
|
||||||
|
const cassandraProxyOutboundIPs: string[] = isProdOrMpacCassandraProxyEndpoint
|
||||||
|
? [
|
||||||
|
...CassandraProxyOutboundIPs[CassandraProxyEndpoints.Mpac],
|
||||||
|
...CassandraProxyOutboundIPs[CassandraProxyEndpoints.Prod],
|
||||||
|
]
|
||||||
|
: CassandraProxyOutboundIPs[configContext.CASSANDRA_PROXY_ENDPOINT];
|
||||||
|
|
||||||
|
const ipRulesIncludeCassandraProxy: boolean =
|
||||||
|
ipAddressesFromIPRules.filter((ipAddressFromIPRule) => cassandraProxyOutboundIPs.includes(ipAddressFromIPRule))
|
||||||
|
?.length === cassandraProxyOutboundIPs.length;
|
||||||
|
|
||||||
|
return !ipRulesIncludeCassandraProxy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ class NotebookTerminalComponentAdapter implements ReactAdapter {
|
|||||||
private getTabId: () => string,
|
private getTabId: () => string,
|
||||||
private getUsername: () => string,
|
private getUsername: () => string,
|
||||||
private isAllPublicIPAddressesEnabled: ko.Observable<boolean>,
|
private isAllPublicIPAddressesEnabled: ko.Observable<boolean>,
|
||||||
|
private kind: ViewModels.TerminalKind,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public renderComponent(): JSX.Element {
|
public renderComponent(): JSX.Element {
|
||||||
@@ -42,7 +43,7 @@ class NotebookTerminalComponentAdapter implements ReactAdapter {
|
|||||||
<QuickstartFirewallNotification
|
<QuickstartFirewallNotification
|
||||||
messageType={MessageTypes.OpenPostgresNetworkingBlade}
|
messageType={MessageTypes.OpenPostgresNetworkingBlade}
|
||||||
screenshot={FirewallRuleScreenshot}
|
screenshot={FirewallRuleScreenshot}
|
||||||
shellName="PostgreSQL"
|
shellName={this.getShellNameForDisplay(this.kind)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -58,6 +59,18 @@ class NotebookTerminalComponentAdapter implements ReactAdapter {
|
|||||||
<Spinner styles={{ root: { marginTop: 10 } }} size={SpinnerSize.large}></Spinner>
|
<Spinner styles={{ root: { marginTop: 10 } }} size={SpinnerSize.large}></Spinner>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getShellNameForDisplay(terminalKind: ViewModels.TerminalKind): string {
|
||||||
|
switch (terminalKind) {
|
||||||
|
case ViewModels.TerminalKind.Postgres:
|
||||||
|
return "PostgreSQL";
|
||||||
|
case ViewModels.TerminalKind.Mongo:
|
||||||
|
case ViewModels.TerminalKind.VCoreMongo:
|
||||||
|
return "MongoDB";
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class TerminalTab extends TabsBase {
|
export default class TerminalTab extends TabsBase {
|
||||||
@@ -76,6 +89,7 @@ export default class TerminalTab extends TabsBase {
|
|||||||
() => this.tabId,
|
() => this.tabId,
|
||||||
() => this.getUsername(),
|
() => this.getUsername(),
|
||||||
this.isAllPublicIPAddressesEnabled,
|
this.isAllPublicIPAddressesEnabled,
|
||||||
|
options.kind,
|
||||||
);
|
);
|
||||||
this.notebookTerminalComponentAdapter.parameters = ko.computed<boolean>(() => {
|
this.notebookTerminalComponentAdapter.parameters = ko.computed<boolean>(() => {
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -29,9 +29,12 @@ const requestDatabaseResourceTokens = async (): Promise<void> => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateUserContext({
|
updateUserContext({
|
||||||
fabricContext: { ...userContext.fabricContext, databaseConnectionInfo: fabricDatabaseConnectionInfo },
|
fabricContext: {
|
||||||
|
...userContext.fabricContext,
|
||||||
|
databaseConnectionInfo: fabricDatabaseConnectionInfo,
|
||||||
|
isReadOnly: true,
|
||||||
|
},
|
||||||
databaseAccount: { ...userContext.databaseAccount },
|
databaseAccount: { ...userContext.databaseAccount },
|
||||||
hasWriteAccess: false, // TODO: receive from fabricDatabaseConnectionInfo
|
|
||||||
});
|
});
|
||||||
scheduleRefreshDatabaseResourceToken();
|
scheduleRefreshDatabaseResourceToken();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ export interface VCoreMongoConnectionParams {
|
|||||||
interface FabricContext {
|
interface FabricContext {
|
||||||
connectionId: string;
|
connectionId: string;
|
||||||
databaseConnectionInfo: FabricDatabaseConnectionInfo | undefined;
|
databaseConnectionInfo: FabricDatabaseConnectionInfo | undefined;
|
||||||
|
isReadOnly: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UserContext {
|
interface UserContext {
|
||||||
@@ -86,7 +87,7 @@ interface UserContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type ApiType = "SQL" | "Mongo" | "Gremlin" | "Tables" | "Cassandra" | "Postgres" | "VCoreMongo";
|
export type ApiType = "SQL" | "Mongo" | "Gremlin" | "Tables" | "Cassandra" | "Postgres" | "VCoreMongo";
|
||||||
export type PortalEnv = "localhost" | "blackforest" | "fairfax" | "mooncake" | "prod" | "dev";
|
export type PortalEnv = "localhost" | "blackforest" | "fairfax" | "mooncake" | "prod1" | "rx" | "ex" | "prod" | "dev";
|
||||||
|
|
||||||
const ONE_WEEK_IN_MS = 604800000;
|
const ONE_WEEK_IN_MS = 604800000;
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { userContext } from "../UserContext";
|
import { userContext } from "../UserContext";
|
||||||
|
|
||||||
export function isRunningOnNationalCloud(): boolean {
|
export function isRunningOnNationalCloud(): boolean {
|
||||||
return (
|
return !isRunningOnPublicCloud();
|
||||||
userContext.portalEnv === "blackforest" ||
|
}
|
||||||
userContext.portalEnv === "fairfax" ||
|
|
||||||
userContext.portalEnv === "mooncake"
|
export function isRunningOnPublicCloud(): boolean {
|
||||||
);
|
return userContext?.portalEnv === "prod1" || userContext?.portalEnv === "prod";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { JunoEndpoints } from "Common/Constants";
|
import { CassandraProxyEndpoints, JunoEndpoints, MongoProxyEndpoints } from "Common/Constants";
|
||||||
import * as Logger from "../Common/Logger";
|
import * as Logger from "../Common/Logger";
|
||||||
|
|
||||||
export function validateEndpoint(
|
export function validateEndpoint(
|
||||||
@@ -67,17 +67,16 @@ export const PortalBackendIPs: { [key: string]: string[] } = {
|
|||||||
//usnat: ["7.28.202.68"],
|
//usnat: ["7.28.202.68"],
|
||||||
};
|
};
|
||||||
|
|
||||||
export class MongoProxyEndpoints {
|
export const MongoProxyOutboundIPs: { [key: string]: string[] } = {
|
||||||
public static readonly Development: string = "https://localhost:7238";
|
[MongoProxyEndpoints.Mpac]: ["20.245.81.54", "40.118.23.126"],
|
||||||
public static readonly MPAC: string = "https://cdb-ms-mpac-mp.cosmos.azure.com";
|
[MongoProxyEndpoints.Prod]: ["40.80.152.199", "13.95.130.121"],
|
||||||
public static readonly Prod: string = "https://cdb-ms-prod-mp.cosmos.azure.com";
|
[MongoProxyEndpoints.Fairfax]: ["52.244.176.112", "52.247.148.42"],
|
||||||
public static readonly Fairfax: string = "https://cdb-ff-prod-mp.cosmos.azure.us";
|
[MongoProxyEndpoints.Mooncake]: ["52.131.240.99", "143.64.61.130"],
|
||||||
public static readonly Mooncake: string = "https://cdb-mc-prod-mp.cosmos.azure.cn";
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export const allowedMongoProxyEndpoints: ReadonlyArray<string> = [
|
export const allowedMongoProxyEndpoints: ReadonlyArray<string> = [
|
||||||
MongoProxyEndpoints.Development,
|
MongoProxyEndpoints.Development,
|
||||||
MongoProxyEndpoints.MPAC,
|
MongoProxyEndpoints.Mpac,
|
||||||
MongoProxyEndpoints.Prod,
|
MongoProxyEndpoints.Prod,
|
||||||
MongoProxyEndpoints.Fairfax,
|
MongoProxyEndpoints.Fairfax,
|
||||||
MongoProxyEndpoints.Mooncake,
|
MongoProxyEndpoints.Mooncake,
|
||||||
@@ -91,6 +90,21 @@ export const allowedMongoProxyEndpoints_ToBeDeprecated: ReadonlyArray<string> =
|
|||||||
"https://localhost:12901",
|
"https://localhost:12901",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const allowedCassandraProxyEndpoints: ReadonlyArray<string> = [
|
||||||
|
CassandraProxyEndpoints.Development,
|
||||||
|
CassandraProxyEndpoints.Mpac,
|
||||||
|
CassandraProxyEndpoints.Prod,
|
||||||
|
CassandraProxyEndpoints.Fairfax,
|
||||||
|
CassandraProxyEndpoints.Mooncake,
|
||||||
|
];
|
||||||
|
|
||||||
|
export const CassandraProxyOutboundIPs: { [key: string]: string[] } = {
|
||||||
|
[CassandraProxyEndpoints.Mpac]: ["40.113.96.14", "104.42.11.145"],
|
||||||
|
[CassandraProxyEndpoints.Prod]: ["137.117.230.240", "168.61.72.237"],
|
||||||
|
[CassandraProxyEndpoints.Fairfax]: ["52.244.50.101", "52.227.165.24"],
|
||||||
|
[CassandraProxyEndpoints.Mooncake]: ["40.73.99.146", "143.64.62.47"],
|
||||||
|
};
|
||||||
|
|
||||||
export const allowedEmulatorEndpoints: ReadonlyArray<string> = ["https://localhost:8081"];
|
export const allowedEmulatorEndpoints: ReadonlyArray<string> = ["https://localhost:8081"];
|
||||||
|
|
||||||
export const allowedMongoBackendEndpoints: ReadonlyArray<string> = ["https://localhost:1234"];
|
export const allowedMongoBackendEndpoints: ReadonlyArray<string> = ["https://localhost:1234"];
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
/*
|
||||||
|
AUTOGENERATED FILE
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { configContext } from "../../../../ConfigContext";
|
||||||
|
import { armRequest } from "../../request";
|
||||||
|
import * as Types from "./types";
|
||||||
|
const apiVersion = "2023-11-15-preview";
|
||||||
|
|
||||||
|
/* Creates a Data Transfer Job. */
|
||||||
|
export async function create(
|
||||||
|
subscriptionId: string,
|
||||||
|
resourceGroupName: string,
|
||||||
|
accountName: string,
|
||||||
|
jobName: string,
|
||||||
|
body: Types.CreateJobRequest,
|
||||||
|
): Promise<Types.DataTransferJobGetResults> {
|
||||||
|
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/dataTransferJobs/${jobName}`;
|
||||||
|
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body });
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Get a Data Transfer Job. */
|
||||||
|
export async function get(
|
||||||
|
subscriptionId: string,
|
||||||
|
resourceGroupName: string,
|
||||||
|
accountName: string,
|
||||||
|
jobName: string,
|
||||||
|
): Promise<Types.DataTransferJobGetResults> {
|
||||||
|
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/dataTransferJobs/${jobName}`;
|
||||||
|
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "GET", apiVersion });
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pause a Data Transfer Job. */
|
||||||
|
export async function pause(
|
||||||
|
subscriptionId: string,
|
||||||
|
resourceGroupName: string,
|
||||||
|
accountName: string,
|
||||||
|
jobName: string,
|
||||||
|
): Promise<Types.DataTransferJobGetResults> {
|
||||||
|
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/dataTransferJobs/${jobName}/pause`;
|
||||||
|
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "POST", apiVersion });
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Resumes a Data Transfer Job. */
|
||||||
|
export async function resume(
|
||||||
|
subscriptionId: string,
|
||||||
|
resourceGroupName: string,
|
||||||
|
accountName: string,
|
||||||
|
jobName: string,
|
||||||
|
): Promise<Types.DataTransferJobGetResults> {
|
||||||
|
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/dataTransferJobs/${jobName}/resume`;
|
||||||
|
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "POST", apiVersion });
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cancels a Data Transfer Job. */
|
||||||
|
export async function cancel(
|
||||||
|
subscriptionId: string,
|
||||||
|
resourceGroupName: string,
|
||||||
|
accountName: string,
|
||||||
|
jobName: string,
|
||||||
|
): Promise<Types.DataTransferJobGetResults> {
|
||||||
|
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/dataTransferJobs/${jobName}/cancel`;
|
||||||
|
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,
|
||||||
|
): Promise<Types.DataTransferJobFeedResults> {
|
||||||
|
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/dataTransferJobs`;
|
||||||
|
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "GET", apiVersion });
|
||||||
|
}
|
||||||
101
src/Utils/arm/generatedClients/dataTransferService/types.ts
Normal file
101
src/Utils/arm/generatedClients/dataTransferService/types.ts
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
/*
|
||||||
|
AUTOGENERATED FILE
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Base class for all DataTransfer source/sink */
|
||||||
|
export interface DataTransferDataSourceSink {
|
||||||
|
/* undocumented */
|
||||||
|
component: "CosmosDBCassandra" | "CosmosDBMongo" | "CosmosDBSql" | "AzureBlobStorage";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A base CosmosDB data source/sink */
|
||||||
|
export type BaseCosmosDataTransferDataSourceSink = DataTransferDataSourceSink & {
|
||||||
|
/* undocumented */
|
||||||
|
remoteAccountName?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* A CosmosDB Cassandra API data source/sink */
|
||||||
|
export type CosmosCassandraDataTransferDataSourceSink = BaseCosmosDataTransferDataSourceSink & {
|
||||||
|
/* undocumented */
|
||||||
|
keyspaceName: string;
|
||||||
|
/* undocumented */
|
||||||
|
tableName: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* A CosmosDB Mongo API data source/sink */
|
||||||
|
export type CosmosMongoDataTransferDataSourceSink = BaseCosmosDataTransferDataSourceSink & {
|
||||||
|
/* undocumented */
|
||||||
|
databaseName: string;
|
||||||
|
/* undocumented */
|
||||||
|
collectionName: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* A CosmosDB No Sql API data source/sink */
|
||||||
|
export type CosmosSqlDataTransferDataSourceSink = BaseCosmosDataTransferDataSourceSink & {
|
||||||
|
/* undocumented */
|
||||||
|
databaseName: string;
|
||||||
|
/* undocumented */
|
||||||
|
containerName: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* An Azure Blob Storage data source/sink */
|
||||||
|
export type AzureBlobDataTransferDataSourceSink = DataTransferDataSourceSink & {
|
||||||
|
/* undocumented */
|
||||||
|
containerName: string;
|
||||||
|
/* undocumented */
|
||||||
|
endpointUrl?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* The properties of a DataTransfer Job */
|
||||||
|
export interface DataTransferJobProperties {
|
||||||
|
/* Job Name */
|
||||||
|
readonly jobName?: string;
|
||||||
|
/* Source DataStore details */
|
||||||
|
source: DataTransferDataSourceSink;
|
||||||
|
|
||||||
|
/* Destination DataStore details */
|
||||||
|
destination: DataTransferDataSourceSink;
|
||||||
|
|
||||||
|
/* Job Status */
|
||||||
|
readonly status?: string;
|
||||||
|
/* Processed Count. */
|
||||||
|
readonly processedCount?: number;
|
||||||
|
/* Total Count. */
|
||||||
|
readonly totalCount?: number;
|
||||||
|
/* Last Updated Time (ISO-8601 format). */
|
||||||
|
readonly lastUpdatedUtcTime?: string;
|
||||||
|
/* Worker count */
|
||||||
|
workerCount?: number;
|
||||||
|
/* Error response for Faulted job */
|
||||||
|
readonly error?: unknown;
|
||||||
|
|
||||||
|
/* Total Duration of Job */
|
||||||
|
readonly duration?: string;
|
||||||
|
/* Mode of job execution */
|
||||||
|
mode?: "Offline" | "Online";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Parameters to create Data Transfer Job */
|
||||||
|
export type CreateJobRequest = unknown & {
|
||||||
|
/* Data Transfer Create Job Properties */
|
||||||
|
properties: DataTransferJobProperties;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* A Cosmos DB Data Transfer Job */
|
||||||
|
export type DataTransferJobGetResults = unknown & {
|
||||||
|
/* undocumented */
|
||||||
|
properties?: DataTransferJobProperties;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* The List operation response, that contains the Data Transfer jobs and their properties. */
|
||||||
|
export interface DataTransferJobFeedResults {
|
||||||
|
/* List of Data Transfer jobs and their properties. */
|
||||||
|
readonly value?: DataTransferJobGetResults[];
|
||||||
|
|
||||||
|
/* URL to get the next set of Data Transfer job list results if there are any. */
|
||||||
|
readonly nextLink?: string;
|
||||||
|
}
|
||||||
60
src/hooks/useDataTransferJobs.tsx
Normal file
60
src/hooks/useDataTransferJobs.tsx
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import { getDataTransferJobs } from "Common/dataAccess/dataTransfers";
|
||||||
|
import { DataTransferJobGetResults } from "Utils/arm/generatedClients/dataTransferService/types";
|
||||||
|
import create, { UseStore } from "zustand";
|
||||||
|
|
||||||
|
export interface DataTransferJobsState {
|
||||||
|
dataTransferJobs: DataTransferJobGetResults[];
|
||||||
|
pollingDataTransferJobs: Set<string>;
|
||||||
|
setDataTransferJobs: (dataTransferJobs: DataTransferJobGetResults[]) => void;
|
||||||
|
setPollingDataTransferJobs: (pollingDataTransferJobs: Set<string>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
type DataTransferJobStore = UseStore<DataTransferJobsState>;
|
||||||
|
|
||||||
|
export const useDataTransferJobs: DataTransferJobStore = create((set) => ({
|
||||||
|
dataTransferJobs: [],
|
||||||
|
pollingDataTransferJobs: new Set<string>(),
|
||||||
|
setDataTransferJobs: (dataTransferJobs: DataTransferJobGetResults[]) => set({ dataTransferJobs }),
|
||||||
|
setPollingDataTransferJobs: (pollingDataTransferJobs: Set<string>) => set({ pollingDataTransferJobs }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const refreshDataTransferJobs = async (
|
||||||
|
subscriptionId: string,
|
||||||
|
resourceGroup: string,
|
||||||
|
accountName: string,
|
||||||
|
): Promise<DataTransferJobGetResults[]> => {
|
||||||
|
const dataTransferJobs: DataTransferJobGetResults[] = await getDataTransferJobs(
|
||||||
|
subscriptionId,
|
||||||
|
resourceGroup,
|
||||||
|
accountName,
|
||||||
|
);
|
||||||
|
const jobRegex = /^Portal_(.+)_(\d{10,})$/;
|
||||||
|
const sortedJobs: DataTransferJobGetResults[] = dataTransferJobs?.sort(
|
||||||
|
(a, b) =>
|
||||||
|
new Date(b?.properties?.lastUpdatedUtcTime).getTime() - new Date(a?.properties?.lastUpdatedUtcTime).getTime(),
|
||||||
|
);
|
||||||
|
const filteredJobs = sortedJobs.filter((job) => jobRegex.test(job?.properties?.jobName));
|
||||||
|
useDataTransferJobs.getState().setDataTransferJobs(filteredJobs);
|
||||||
|
return filteredJobs;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateDataTransferJob = (updateJob: DataTransferJobGetResults) => {
|
||||||
|
const updatedDataTransferJobs = useDataTransferJobs
|
||||||
|
.getState()
|
||||||
|
.dataTransferJobs.map((job: DataTransferJobGetResults) =>
|
||||||
|
job?.properties?.jobName === updateJob?.properties?.jobName ? updateJob : job,
|
||||||
|
);
|
||||||
|
useDataTransferJobs.getState().setDataTransferJobs(updatedDataTransferJobs);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const addToPolling = (addJob: string) => {
|
||||||
|
const pollingJobs = useDataTransferJobs.getState().pollingDataTransferJobs;
|
||||||
|
pollingJobs.add(addJob);
|
||||||
|
useDataTransferJobs.getState().setPollingDataTransferJobs(pollingJobs);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const removeFromPolling = (removeJob: string) => {
|
||||||
|
const pollingJobs = useDataTransferJobs.getState().pollingDataTransferJobs;
|
||||||
|
pollingJobs.delete(removeJob);
|
||||||
|
useDataTransferJobs.getState().setPollingDataTransferJobs(pollingJobs);
|
||||||
|
};
|
||||||
@@ -328,6 +328,7 @@ function createExplorerFabric(params: { connectionId: string }): Explorer {
|
|||||||
fabricContext: {
|
fabricContext: {
|
||||||
connectionId: params.connectionId,
|
connectionId: params.connectionId,
|
||||||
databaseConnectionInfo: undefined,
|
databaseConnectionInfo: undefined,
|
||||||
|
isReadOnly: true,
|
||||||
},
|
},
|
||||||
authType: AuthType.ConnectionString,
|
authType: AuthType.ConnectionString,
|
||||||
databaseAccount: {
|
databaseAccount: {
|
||||||
@@ -479,6 +480,7 @@ function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) {
|
|||||||
BACKEND_ENDPOINT: inputs.extensionEndpoint || configContext.BACKEND_ENDPOINT,
|
BACKEND_ENDPOINT: inputs.extensionEndpoint || configContext.BACKEND_ENDPOINT,
|
||||||
ARM_ENDPOINT: normalizeArmEndpoint(inputs.csmEndpoint || configContext.ARM_ENDPOINT),
|
ARM_ENDPOINT: normalizeArmEndpoint(inputs.csmEndpoint || configContext.ARM_ENDPOINT),
|
||||||
MONGO_PROXY_ENDPOINT: inputs.mongoProxyEndpoint,
|
MONGO_PROXY_ENDPOINT: inputs.mongoProxyEndpoint,
|
||||||
|
CASSANDRA_PROXY_ENDPOINT: inputs.cassandraProxyEndpoint,
|
||||||
});
|
});
|
||||||
|
|
||||||
updateUserContext({
|
updateUserContext({
|
||||||
|
|||||||
@@ -16,13 +16,13 @@ Results of this file should be checked into the repo.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// CHANGE THESE VALUES TO GENERATE NEW CLIENTS
|
// CHANGE THESE VALUES TO GENERATE NEW CLIENTS
|
||||||
const version = "2023-09-15-preview";
|
const version = "2023-11-15-preview";
|
||||||
/* The following are legal options for resourceName but you generally will only use cosmos-db:
|
/* The following are legal options for resourceName but you generally will only use cosmos-db:
|
||||||
"cosmos-db" | "managedCassandra" | "mongorbac" | "notebook" | "privateEndpointConnection" | "privateLinkResources" |
|
"cosmos-db" | "managedCassandra" | "mongorbac" | "notebook" | "privateEndpointConnection" | "privateLinkResources" |
|
||||||
"rbac" | "restorable" | "services"
|
"rbac" | "restorable" | "services" | "dataTransferService"
|
||||||
*/
|
*/
|
||||||
const githubResourceName = "cosmos-db";
|
const githubResourceName = "cosmos-db";
|
||||||
const deResourceName = "cosmos";
|
const deResourceName = "cosmos-db";
|
||||||
const schemaURL = `https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/${version}/${githubResourceName}.json`;
|
const schemaURL = `https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/${version}/${githubResourceName}.json`;
|
||||||
const outputDir = path.join(__dirname, `../../src/Utils/arm/generatedClients/${deResourceName}`);
|
const outputDir = path.join(__dirname, `../../src/Utils/arm/generatedClients/${deResourceName}`);
|
||||||
|
|
||||||
@@ -117,9 +117,9 @@ const propertyToType = (property: Property, prop: string, required: boolean) =>
|
|||||||
if (property.allOf) {
|
if (property.allOf) {
|
||||||
outputTypes.push(`
|
outputTypes.push(`
|
||||||
/* ${property.description || "undocumented"} */
|
/* ${property.description || "undocumented"} */
|
||||||
${property.readOnly ? "readonly " : ""}${prop}${
|
${property.readOnly ? "readonly " : ""}${prop}${required ? "" : "?"}: ${property.allOf
|
||||||
required ? "" : "?"
|
.map((allof: { $ref: string }) => refToType(allof.$ref))
|
||||||
}: ${property.allOf.map((allof: { $ref: string }) => refToType(allof.$ref)).join(" & ")}`);
|
.join(" & ")}`);
|
||||||
} else if (property.$ref) {
|
} else if (property.$ref) {
|
||||||
const type = refToType(property.$ref);
|
const type = refToType(property.$ref);
|
||||||
outputTypes.push(`
|
outputTypes.push(`
|
||||||
@@ -142,8 +142,8 @@ const propertyToType = (property: Property, prop: string, required: boolean) =>
|
|||||||
outputTypes.push(`
|
outputTypes.push(`
|
||||||
/* ${property.description || "undocumented"} */
|
/* ${property.description || "undocumented"} */
|
||||||
${property.readOnly ? "readonly " : ""}${prop}${required ? "" : "?"}: ${property.enum
|
${property.readOnly ? "readonly " : ""}${prop}${required ? "" : "?"}: ${property.enum
|
||||||
.map((v: string) => `"${v}"`)
|
.map((v: string) => `"${v}"`)
|
||||||
.join(" | ")}
|
.join(" | ")}
|
||||||
`);
|
`);
|
||||||
} else {
|
} else {
|
||||||
if (property.type === undefined) {
|
if (property.type === undefined) {
|
||||||
@@ -153,8 +153,8 @@ const propertyToType = (property: Property, prop: string, required: boolean) =>
|
|||||||
outputTypes.push(`
|
outputTypes.push(`
|
||||||
/* ${property.description || "undocumented"} */
|
/* ${property.description || "undocumented"} */
|
||||||
${property.readOnly ? "readonly " : ""}${prop}${required ? "" : "?"}: ${
|
${property.readOnly ? "readonly " : ""}${prop}${required ? "" : "?"}: ${
|
||||||
propertyMap[property.type] ? propertyMap[property.type] : property.type
|
propertyMap[property.type] ? propertyMap[property.type] : property.type
|
||||||
}`);
|
}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -247,7 +247,7 @@ async function main() {
|
|||||||
const operation = schema.paths[path][method];
|
const operation = schema.paths[path][method];
|
||||||
const [, methodName] = operation.operationId.split("_");
|
const [, methodName] = operation.operationId.split("_");
|
||||||
const bodyParameter = operation.parameters.find(
|
const bodyParameter = operation.parameters.find(
|
||||||
(parameter: { in: string; required: boolean }) => parameter.in === "body" && parameter.required === true
|
(parameter: { in: string; required: boolean }) => parameter.in === "body" && parameter.required === true,
|
||||||
);
|
);
|
||||||
outputClient.push(`
|
outputClient.push(`
|
||||||
/* ${operation.description || "undocumented"} */
|
/* ${operation.description || "undocumented"} */
|
||||||
@@ -259,8 +259,8 @@ async function main() {
|
|||||||
) : Promise<${responseType(operation, "Types")}> {
|
) : Promise<${responseType(operation, "Types")}> {
|
||||||
const path = \`${path.replace(/{/g, "${")}\`
|
const path = \`${path.replace(/{/g, "${")}\`
|
||||||
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "${method.toLocaleUpperCase()}", apiVersion, ${
|
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "${method.toLocaleUpperCase()}", apiVersion, ${
|
||||||
bodyParameter ? "body" : ""
|
bodyParameter ? "body" : ""
|
||||||
} })
|
} })
|
||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user