mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-01-07 11:36:47 +00:00
Compare commits
1 Commits
revert-173
...
2950560
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ada04da73 |
@@ -25,30 +25,29 @@ a:focus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.resourceTreeAndTabs {
|
.resourceTreeAndTabs {
|
||||||
border-radius: 0px;
|
border-radius: @FabricBoxBorderRadius;
|
||||||
box-shadow: @FabricBoxBorderShadow;
|
box-shadow: @FabricBoxBorderShadow;
|
||||||
margin: @FabricBoxMargin;
|
margin: @FabricBoxMargin;
|
||||||
margin-top: 0px;
|
margin-top: 4px;
|
||||||
margin-bottom: 0px;
|
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabsManagerContainer {
|
.tabsManagerContainer {
|
||||||
background-color: #ffffff
|
background-color: #fafafa
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-tabs-margin {
|
.nav-tabs-margin {
|
||||||
padding-top: 8px;
|
padding-top: 8px;
|
||||||
background-color: #ffffff
|
background-color: #fafafa
|
||||||
}
|
}
|
||||||
|
|
||||||
.commandBarContainer {
|
.commandBarContainer {
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
border-radius: @FabricBoxBorderRadius @FabricBoxBorderRadius 0px 0px;
|
border-bottom: none;
|
||||||
|
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;
|
||||||
@@ -163,10 +162,9 @@ a:focus {
|
|||||||
|
|
||||||
|
|
||||||
.dataExplorerErrorConsoleContainer {
|
.dataExplorerErrorConsoleContainer {
|
||||||
border-radius: 0px 0px @FabricBoxBorderRadius @FabricBoxBorderRadius;
|
border-radius: @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,22 +435,6 @@ 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,9 +1,5 @@
|
|||||||
import { Constants as CosmosSDKConstants } from "@azure/cosmos";
|
import { Constants as CosmosSDKConstants } from "@azure/cosmos";
|
||||||
import {
|
import { MongoProxyEndpoints, allowedMongoProxyEndpoints_ToBeDeprecated, validateEndpoint } from "Utils/EndpointUtils";
|
||||||
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";
|
||||||
@@ -14,7 +10,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, MongoProxyEndpoints } from "./Constants";
|
import { ApiType, ContentType, HttpHeaders, HttpStatusCodes } from "./Constants";
|
||||||
import { MinimalQueryIterator } from "./IteratorUtilities";
|
import { MinimalQueryIterator } from "./IteratorUtilities";
|
||||||
import { sendMessage } from "./MessageHandler";
|
import { sendMessage } from "./MessageHandler";
|
||||||
|
|
||||||
@@ -465,7 +461,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")) {
|
||||||
return deleteDocument_ToBeDeprecated(databaseId, collection, documentId);
|
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;
|
||||||
@@ -554,7 +550,7 @@ export function createMongoCollectionWithProxy(
|
|||||||
params: DataModels.CreateCollectionParams,
|
params: DataModels.CreateCollectionParams,
|
||||||
): Promise<DataModels.Collection> {
|
): Promise<DataModels.Collection> {
|
||||||
if (!useMongoProxyEndpoint("createCollectionWithProxy")) {
|
if (!useMongoProxyEndpoint("createCollectionWithProxy")) {
|
||||||
return createMongoCollectionWithProxy_ToBeDeprecated(params);
|
createMongoCollectionWithProxy_ToBeDeprecated(params);
|
||||||
}
|
}
|
||||||
const { databaseAccount } = userContext;
|
const { databaseAccount } = userContext;
|
||||||
const shardKey: string = params.partitionKey?.paths[0];
|
const shardKey: string = params.partitionKey?.paths[0];
|
||||||
@@ -648,10 +644,7 @@ export function getFeatureEndpointOrDefault(feature: string): string {
|
|||||||
} else {
|
} else {
|
||||||
endpoint =
|
endpoint =
|
||||||
hasFlag(userContext.features.mongoProxyAPIs, feature) &&
|
hasFlag(userContext.features.mongoProxyAPIs, feature) &&
|
||||||
validateEndpoint(userContext.features.mongoProxyEndpoint, [
|
validateEndpoint(userContext.features.mongoProxyEndpoint, allowedMongoProxyEndpoints_ToBeDeprecated)
|
||||||
...allowedMongoProxyEndpoints,
|
|
||||||
...allowedMongoProxyEndpoints_ToBeDeprecated,
|
|
||||||
])
|
|
||||||
? userContext.features.mongoProxyEndpoint
|
? userContext.features.mongoProxyEndpoint
|
||||||
: configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_ENDPOINT;
|
: configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_ENDPOINT;
|
||||||
}
|
}
|
||||||
@@ -690,14 +683,8 @@ 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)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,188 +0,0 @@
|
|||||||
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,8 +1,7 @@
|
|||||||
import { CassandraProxyEndpoints, JunoEndpoints, MongoProxyEndpoints } from "Common/Constants";
|
import { JunoEndpoints } from "Common/Constants";
|
||||||
import {
|
import {
|
||||||
allowedAadEndpoints,
|
allowedAadEndpoints,
|
||||||
allowedArcadiaEndpoints,
|
allowedArcadiaEndpoints,
|
||||||
allowedCassandraProxyEndpoints,
|
|
||||||
allowedEmulatorEndpoints,
|
allowedEmulatorEndpoints,
|
||||||
allowedGraphEndpoints,
|
allowedGraphEndpoints,
|
||||||
allowedHostedExplorerEndpoints,
|
allowedHostedExplorerEndpoints,
|
||||||
@@ -41,9 +40,7 @@ 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;
|
||||||
@@ -88,7 +85,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: MongoProxyEndpoints.Prod,
|
MONGO_PROXY_ENDPOINT: "https://cdb-ms-prod-mp.cosmos.azure.com",
|
||||||
NEW_MONGO_APIS: [
|
NEW_MONGO_APIS: [
|
||||||
// "resourcelist",
|
// "resourcelist",
|
||||||
// "createDocument",
|
// "createDocument",
|
||||||
@@ -97,8 +94,6 @@ 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,
|
||||||
};
|
};
|
||||||
@@ -152,10 +147,6 @@ 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;
|
||||||
}
|
}
|
||||||
@@ -173,7 +164,10 @@ 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,7 +387,6 @@ 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,6 +1,5 @@
|
|||||||
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";
|
||||||
@@ -19,10 +18,6 @@ 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";
|
||||||
@@ -133,7 +128,6 @@ 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;
|
||||||
|
|
||||||
@@ -146,7 +140,6 @@ 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;
|
||||||
|
|
||||||
@@ -1063,12 +1056,6 @@ 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({
|
||||||
@@ -1104,13 +1091,6 @@ 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],
|
||||||
|
|||||||
@@ -1,216 +0,0 @@
|
|||||||
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, false);
|
const prices: PriceBreakdown = getRuPriceBreakdown(throughput, serverId, numberOfRegions, isMultimaster, true);
|
||||||
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.0080
|
0.012
|
||||||
/hr
|
/hr
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
@@ -929,7 +929,7 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
|
|||||||
>
|
>
|
||||||
$
|
$
|
||||||
|
|
||||||
0.19
|
0.29
|
||||||
/day
|
/day
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
@@ -941,7 +941,7 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
|
|||||||
>
|
>
|
||||||
$
|
$
|
||||||
|
|
||||||
5.84
|
8.76
|
||||||
/mo
|
/mo
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -1354,7 +1354,7 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = `
|
|||||||
>
|
>
|
||||||
$
|
$
|
||||||
|
|
||||||
0.0080
|
0.012
|
||||||
/hr
|
/hr
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
@@ -1366,7 +1366,7 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = `
|
|||||||
>
|
>
|
||||||
$
|
$
|
||||||
|
|
||||||
0.19
|
0.29
|
||||||
/day
|
/day
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
@@ -1378,7 +1378,7 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = `
|
|||||||
>
|
>
|
||||||
$
|
$
|
||||||
|
|
||||||
5.84
|
8.76
|
||||||
/mo
|
/mo
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ export enum SettingsV2TabTypes {
|
|||||||
ConflictResolutionTab,
|
ConflictResolutionTab,
|
||||||
SubSettingsTab,
|
SubSettingsTab,
|
||||||
IndexingPolicyTab,
|
IndexingPolicyTab,
|
||||||
PartitionKeyTab,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IsComponentDirtyResult {
|
export interface IsComponentDirtyResult {
|
||||||
@@ -147,8 +146,6 @@ 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}`);
|
||||||
}
|
}
|
||||||
@@ -202,49 +199,3 @@ 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,98 +204,6 @@ 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,22 +196,18 @@ 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} />),
|
||||||
iconSrc: SettingsIcon,
|
commandButtonLabel: undefined,
|
||||||
iconAlt: "Settings",
|
ariaLabel: "Settings",
|
||||||
onCommandClick: () =>
|
tooltipText: "Settings",
|
||||||
useSidePanel.getState().openSidePanel("Settings", <SettingsPane explorer={container} />),
|
hasPopup: true,
|
||||||
commandButtonLabel: undefined,
|
disabled: false,
|
||||||
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";
|
||||||
|
|||||||
@@ -1,396 +0,0 @@
|
|||||||
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,7 +24,9 @@ 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 variant="xLarge">{getHeaderText(page)}</Text>
|
<Text role="heading" aria-level={1} variant="xLarge">
|
||||||
|
{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,5 +1,4 @@
|
|||||||
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";
|
||||||
@@ -882,7 +881,7 @@ export default class DocumentsTab extends TabsBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected getTabsButtons(): CommandButtonComponentProps[] {
|
protected getTabsButtons(): CommandButtonComponentProps[] {
|
||||||
if (configContext.platform === Platform.Fabric && userContext.fabricContext?.isReadOnly) {
|
if (!userContext.hasWriteAccess) {
|
||||||
// All the following buttons require write access
|
// All the following buttons require write access
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
import { Link, MessageBar, MessageBarButton, MessageBarType } from "@fluentui/react";
|
import { 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";
|
||||||
@@ -15,7 +12,6 @@ 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";
|
||||||
@@ -37,10 +33,6 @@ 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 && (
|
||||||
@@ -77,25 +69,9 @@ 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
|
{
|
||||||
the limit, go to the Settings cog on the right and find "RU Threshold".`}
|
"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'."
|
||||||
<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">
|
||||||
@@ -323,59 +299,3 @@ 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,7 +34,6 @@ 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 {
|
||||||
@@ -43,7 +42,7 @@ class NotebookTerminalComponentAdapter implements ReactAdapter {
|
|||||||
<QuickstartFirewallNotification
|
<QuickstartFirewallNotification
|
||||||
messageType={MessageTypes.OpenPostgresNetworkingBlade}
|
messageType={MessageTypes.OpenPostgresNetworkingBlade}
|
||||||
screenshot={FirewallRuleScreenshot}
|
screenshot={FirewallRuleScreenshot}
|
||||||
shellName={this.getShellNameForDisplay(this.kind)}
|
shellName="PostgreSQL"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -59,18 +58,6 @@ 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 {
|
||||||
@@ -89,7 +76,6 @@ 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,12 +29,9 @@ const requestDatabaseResourceTokens = async (): Promise<void> => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateUserContext({
|
updateUserContext({
|
||||||
fabricContext: {
|
fabricContext: { ...userContext.fabricContext, databaseConnectionInfo: fabricDatabaseConnectionInfo },
|
||||||
...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,7 +50,6 @@ export interface VCoreMongoConnectionParams {
|
|||||||
interface FabricContext {
|
interface FabricContext {
|
||||||
connectionId: string;
|
connectionId: string;
|
||||||
databaseConnectionInfo: FabricDatabaseConnectionInfo | undefined;
|
databaseConnectionInfo: FabricDatabaseConnectionInfo | undefined;
|
||||||
isReadOnly: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UserContext {
|
interface UserContext {
|
||||||
@@ -87,7 +86,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" | "prod1" | "rx" | "ex" | "prod" | "dev";
|
export type PortalEnv = "localhost" | "blackforest" | "fairfax" | "mooncake" | "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 !isRunningOnPublicCloud();
|
return (
|
||||||
}
|
userContext.portalEnv === "blackforest" ||
|
||||||
|
userContext.portalEnv === "fairfax" ||
|
||||||
export function isRunningOnPublicCloud(): boolean {
|
userContext.portalEnv === "mooncake"
|
||||||
return userContext?.portalEnv === "prod1" || userContext?.portalEnv === "prod";
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { CassandraProxyEndpoints, JunoEndpoints, MongoProxyEndpoints } from "Common/Constants";
|
import { JunoEndpoints } from "Common/Constants";
|
||||||
import * as Logger from "../Common/Logger";
|
import * as Logger from "../Common/Logger";
|
||||||
|
|
||||||
export function validateEndpoint(
|
export function validateEndpoint(
|
||||||
@@ -67,16 +67,17 @@ export const PortalBackendIPs: { [key: string]: string[] } = {
|
|||||||
//usnat: ["7.28.202.68"],
|
//usnat: ["7.28.202.68"],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MongoProxyOutboundIPs: { [key: string]: string[] } = {
|
export class MongoProxyEndpoints {
|
||||||
[MongoProxyEndpoints.Mpac]: ["20.245.81.54", "40.118.23.126"],
|
public static readonly Development: string = "https://localhost:7238";
|
||||||
[MongoProxyEndpoints.Prod]: ["40.80.152.199", "13.95.130.121"],
|
public static readonly MPAC: string = "https://cdb-ms-mpac-mp.cosmos.azure.com";
|
||||||
[MongoProxyEndpoints.Fairfax]: ["52.244.176.112", "52.247.148.42"],
|
public static readonly Prod: string = "https://cdb-ms-prod-mp.cosmos.azure.com";
|
||||||
[MongoProxyEndpoints.Mooncake]: ["52.131.240.99", "143.64.61.130"],
|
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 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,
|
||||||
@@ -90,21 +91,6 @@ 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"];
|
||||||
|
|||||||
@@ -1,78 +0,0 @@
|
|||||||
/*
|
|
||||||
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 });
|
|
||||||
}
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
/*
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
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,7 +328,6 @@ 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: {
|
||||||
@@ -480,7 +479,6 @@ 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-11-15-preview";
|
const version = "2023-09-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" | "dataTransferService"
|
"rbac" | "restorable" | "services"
|
||||||
*/
|
*/
|
||||||
const githubResourceName = "cosmos-db";
|
const githubResourceName = "cosmos-db";
|
||||||
const deResourceName = "cosmos-db";
|
const deResourceName = "cosmos";
|
||||||
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}${required ? "" : "?"}: ${property.allOf
|
${property.readOnly ? "readonly " : ""}${prop}${
|
||||||
.map((allof: { $ref: string }) => refToType(allof.$ref))
|
required ? "" : "?"
|
||||||
.join(" & ")}`);
|
}: ${property.allOf.map((allof: { $ref: string }) => refToType(allof.$ref)).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