mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-23 10:51:30 +00:00
Compare commits
41 Commits
revert-173
...
ashleyst/c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ba3f39d10 | ||
|
|
e0cb3da6aa | ||
|
|
6c9673975a | ||
|
|
d35e2a325e | ||
|
|
00a816c488 | ||
|
|
953bef404b | ||
|
|
dfcb771939 | ||
|
|
6925fa8e4e | ||
|
|
7f6338b68b | ||
|
|
db50f42832 | ||
|
|
f533eeb0fc | ||
|
|
3c5d899e47 | ||
|
|
b44778b00a | ||
|
|
1464745659 | ||
|
|
18cc2a4195 | ||
|
|
86f2bc171f | ||
|
|
cabedf4a29 | ||
|
|
5aa6b0abe1 | ||
|
|
f24b0bcf1b | ||
|
|
56408a97d7 | ||
|
|
0df68c4967 | ||
|
|
e09930d9d0 | ||
|
|
da2e874ae6 | ||
|
|
a524138ac9 | ||
|
|
39b0fb9e2c | ||
|
|
ac22e88d9c | ||
|
|
91d9e27049 | ||
|
|
f881f7fd2f | ||
|
|
4c74525b5d | ||
|
|
1a6d8d5357 | ||
|
|
56c0049e9a | ||
|
|
b3837a089d | ||
|
|
e68aaebca6 | ||
|
|
6e1c4fd037 | ||
|
|
0039adf1c2 | ||
|
|
5d4e9d82bb | ||
|
|
47bdc9c426 | ||
|
|
b8457e3bf9 | ||
|
|
533e9c887c | ||
|
|
76ad930930 | ||
|
|
932f211038 |
14
.github/workflows/ci.yml
vendored
14
.github/workflows/ci.yml
vendored
@@ -8,6 +8,9 @@ on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
jobs:
|
||||
codemetrics:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -134,7 +137,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
||||
NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET }}
|
||||
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -145,11 +148,18 @@ jobs:
|
||||
- ./test/mongo/container.spec.ts
|
||||
- ./test/mongo/container32.spec.ts
|
||||
- ./test/selfServe/selfServeExample.spec.ts
|
||||
# - ./test/notebooks/upload.spec.ts // TEMP disabled since notebooks service is off
|
||||
- ./test/sql/resourceToken.spec.ts
|
||||
- ./test/tables/container.spec.ts
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: "Az CLI login"
|
||||
uses: azure/login@v1
|
||||
with:
|
||||
client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
|
||||
- name: Use Node.js 18.x
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
|
||||
15
.github/workflows/cleanup.yml
vendored
15
.github/workflows/cleanup.yml
vendored
@@ -9,6 +9,10 @@ on:
|
||||
# Once every hour
|
||||
- cron: "0 15 * * *"
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
|
||||
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
|
||||
jobs:
|
||||
# This workflow contains a single job called "build"
|
||||
@@ -16,10 +20,17 @@ jobs:
|
||||
name: "Cleanup Test Database Accounts"
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
NOTEBOOKS_TEST_RUNNER_CLIENT_ID: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_ID }}
|
||||
NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET }}
|
||||
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: "Az CLI login"
|
||||
uses: azure/login@v1
|
||||
with:
|
||||
client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
|
||||
- name: Use Node.js 18.x
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
|
||||
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@@ -20,8 +20,8 @@
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true,
|
||||
"source.organizeImports": true
|
||||
"source.fixAll.eslint": "explicit",
|
||||
"source.organizeImports": "explicit"
|
||||
},
|
||||
"typescript.preferences.importModuleSpecifier": "non-relative",
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"JUNO_ENDPOINT": "https://tools-staging.cosmos.azure.com",
|
||||
"JUNO_ENDPOINT": "https://tools.cosmos.azure.com",
|
||||
"isTerminalEnabled": true,
|
||||
"isPhoenixEnabled": true
|
||||
}
|
||||
3
images/Home_16.svg
Normal file
3
images/Home_16.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.31299 1.26164C7.69849 0.897163 8.30151 0.897163 8.68701 1.26164L13.5305 5.84098C13.8302 6.12431 14 6.51853 14 6.93094V12.5002C14 13.3286 13.3284 14.0002 12.5 14.0002H10.5C9.67157 14.0002 9 13.3286 9 12.5002V10.0002C9 9.72407 8.77614 9.50021 8.5 9.50021H7.5C7.22386 9.50021 7 9.72407 7 10.0002V12.5002C7 13.3286 6.32843 14.0002 5.5 14.0002H3.5C2.67157 14.0002 2 13.3286 2 12.5002V6.93094C2 6.51853 2.1698 6.12431 2.46948 5.84098L7.31299 1.26164ZM8 1.98828L3.15649 6.56762C3.0566 6.66207 3 6.79347 3 6.93094V12.5002C3 12.7763 3.22386 13.0002 3.5 13.0002H5.5C5.77614 13.0002 6 12.7763 6 12.5002V10.0002C6 9.17179 6.67157 8.50022 7.5 8.50022H8.5C9.32843 8.50022 10 9.17179 10 10.0002V12.5002C10 12.7763 10.2239 13.0002 10.5 13.0002H12.5C12.7761 13.0002 13 12.7763 13 12.5002V6.93094C13 6.79347 12.9434 6.66207 12.8435 6.56762L8 1.98828Z" fill="#0078D4" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 968 B |
@@ -163,6 +163,7 @@
|
||||
/**********************************************************************************/
|
||||
|
||||
@FabricFont: "Segoe UI", "Segoe UI Web (West European)", "Segoe UI", -apple-system, BlinkMacSystemFont, Roboto, "Helvetica Neue", sans-serif;
|
||||
@FabricToolbarIconColor: "brightness(0) saturate(100%) invert(50%) sepia(17%) saturate(1459%) hue-rotate(81deg) brightness(99%) contrast(94%)";
|
||||
|
||||
@FabricBoxBorderRadius: 8px;
|
||||
@FabricBoxBorderShadow: rgba(0, 0, 0, 0.133) 0px 1.6px 3.6px 0px, rgba(0, 0, 0, 0.11) 0px 0.3px 0.9px 0px;
|
||||
|
||||
@@ -2296,6 +2296,17 @@ a:link {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.monaco-editor .quick-input-list-label {
|
||||
/* Restore some of Monaco's default styles that are clobbered by our global styles */
|
||||
padding: 0;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.monaco-editor .quick-input-list .highlight {
|
||||
/* Padding in highlighted text within the quick input list breaks the flow of the text */
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
td a {
|
||||
color: #393939;
|
||||
}
|
||||
|
||||
@@ -78,8 +78,8 @@
|
||||
"mkdirp": "1.0.4",
|
||||
"monaco-editor": "0.44.0",
|
||||
"ms": "2.1.3",
|
||||
"patch-package": "8.0.0",
|
||||
"p-retry": "4.6.2",
|
||||
"patch-package": "8.0.0",
|
||||
"plotly.js-cartesian-dist-min": "1.52.3",
|
||||
"post-robot": "10.0.42",
|
||||
"q": "1.5.1",
|
||||
|
||||
@@ -124,7 +124,36 @@ export enum MongoBackendEndpointType {
|
||||
remote,
|
||||
}
|
||||
|
||||
// TODO: 435619 Add default endpoints per cloud and use regional only when available
|
||||
export class BackendApi {
|
||||
public static readonly GenerateToken: string = "GenerateToken";
|
||||
public static readonly PortalSettings: string = "PortalSettings";
|
||||
}
|
||||
|
||||
export class PortalBackendEndpoints {
|
||||
public static readonly Development: string = "https://localhost:7235";
|
||||
public static readonly Mpac: string = "https://cdb-ms-mpac-pbe.cosmos.azure.com";
|
||||
public static readonly Prod: string = "https://cdb-ms-prod-pbe.cosmos.azure.com";
|
||||
public static readonly Fairfax: string = "https://cdb-ff-prod-pbe.cosmos.azure.us";
|
||||
public static readonly Mooncake: string = "https://cdb-mc-prod-pbe.cosmos.azure.cn";
|
||||
}
|
||||
|
||||
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";
|
||||
}
|
||||
|
||||
//TODO: Remove this when new backend is migrated over
|
||||
export class CassandraBackend {
|
||||
public static readonly createOrDeleteApi: string = "api/cassandra/createordelete";
|
||||
public static readonly guestCreateOrDeleteApi: string = "api/guest/cassandra/createordelete";
|
||||
@@ -136,6 +165,17 @@ export class CassandraBackend {
|
||||
public static readonly guestSchemaApi: string = "api/guest/cassandra/schema";
|
||||
}
|
||||
|
||||
export class CassandraProxyAPIs {
|
||||
public static readonly createOrDeleteApi: string = "api/cassandra/createordelete";
|
||||
public static readonly connectionStringCreateOrDeleteApi: string = "api/connectionstring/cassandra/createordelete";
|
||||
public static readonly queryApi: string = "api/cassandra";
|
||||
public static readonly connectionStringQueryApi: string = "api/connectionstring/cassandra";
|
||||
public static readonly keysApi: string = "api/cassandra/keys";
|
||||
public static readonly connectionStringKeysApi: string = "api/connectionstring/cassandra/keys";
|
||||
public static readonly schemaApi: string = "api/cassandra/schema";
|
||||
public static readonly connectionStringSchemaApi: string = "api/connectionstring/cassandra/schema";
|
||||
}
|
||||
|
||||
export class Queries {
|
||||
public static CustomPageOption: string = "custom";
|
||||
public static UnlimitedPageOption: string = "unlimited";
|
||||
@@ -435,22 +475,6 @@ export class JunoEndpoints {
|
||||
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 {
|
||||
public static readonly High = "high";
|
||||
public static readonly Low = "low";
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import * as Cosmos from "@azure/cosmos";
|
||||
import { sendCachedDataMessage } from "Common/MessageHandler";
|
||||
import { getAuthorizationTokenUsingResourceTokens } from "Common/getAuthorizationTokenUsingResourceTokens";
|
||||
import { AuthorizationToken, MessageTypes } from "Contracts/MessageTypes";
|
||||
import { AuthorizationToken } from "Contracts/MessageTypes";
|
||||
import { checkDatabaseResourceTokensValidity } from "Platform/Fabric/FabricUtil";
|
||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||
import { AuthType } from "../AuthType";
|
||||
@@ -51,6 +50,13 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
|
||||
case Cosmos.ResourceType.offer:
|
||||
case Cosmos.ResourceType.user:
|
||||
case Cosmos.ResourceType.permission:
|
||||
// For now, these operations aren't used, so fetching the authorization token is commented out.
|
||||
// This provider must return a real token to pass validation by the client, so we return the cached resource token
|
||||
// (which is a valid token, but won't work for these operations).
|
||||
const resourceTokens2 = userContext.fabricContext.databaseConnectionInfo.resourceTokens;
|
||||
return getAuthorizationTokenUsingResourceTokens(resourceTokens2, requestInfo.path, requestInfo.resourceId);
|
||||
|
||||
/* ************** TODO: Uncomment this code if we need to support these operations **************
|
||||
// User master tokens
|
||||
const authorizationToken = await sendCachedDataMessage<AuthorizationToken>(
|
||||
MessageTypes.GetAuthorizationToken,
|
||||
@@ -60,6 +66,7 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
|
||||
console.log("Response from Fabric: ", authorizationToken);
|
||||
headers[HttpHeaders.msDate] = authorizationToken.XDate;
|
||||
return decodeURIComponent(authorizationToken.PrimaryReadWriteToken);
|
||||
***********************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ export function queryDocuments(
|
||||
query: string,
|
||||
continuationToken?: string,
|
||||
): Promise<QueryResponse> {
|
||||
if (!useMongoProxyEndpoint("resourcelist")) {
|
||||
if (!useMongoProxyEndpoint("resourcelist") || !useMongoProxyEndpoint("queryDocuments")) {
|
||||
return queryDocuments_ToBeDeprecated(databaseId, collection, isResourceList, query, continuationToken);
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ export function queryDocuments(
|
||||
headers[CosmosSDKConstants.HttpHeaders.Continuation] = continuationToken;
|
||||
}
|
||||
|
||||
const path = isResourceList ? "/resourcelist" : "";
|
||||
const path = isResourceList ? "/resourcelist" : "/queryDocuments";
|
||||
|
||||
return window
|
||||
.fetch(`${endpoint}${path}`, {
|
||||
@@ -690,14 +690,22 @@ export function getARMCreateCollectionEndpoint(params: DataModels.MongoParameter
|
||||
}
|
||||
|
||||
function useMongoProxyEndpoint(api: string): boolean {
|
||||
const activeMongoProxyEndpoints: string[] = [
|
||||
MongoProxyEndpoints.Development,
|
||||
MongoProxyEndpoints.Mpac,
|
||||
MongoProxyEndpoints.Prod,
|
||||
];
|
||||
let canAccessMongoProxy: boolean = userContext.databaseAccount.properties.publicNetworkAccess === "Enabled";
|
||||
if (userContext.databaseAccount.properties.ipRules?.length > 0) {
|
||||
if (
|
||||
configContext.MONGO_PROXY_ENDPOINT !== MongoProxyEndpoints.Development &&
|
||||
userContext.databaseAccount.properties.ipRules?.length > 0
|
||||
) {
|
||||
canAccessMongoProxy = canAccessMongoProxy && configContext.MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED;
|
||||
}
|
||||
|
||||
return (
|
||||
canAccessMongoProxy &&
|
||||
configContext.NEW_MONGO_APIS?.includes(api) &&
|
||||
[MongoProxyEndpoints.Development, MongoProxyEndpoints.Mpac].includes(configContext.MONGO_PROXY_ENDPOINT)
|
||||
activeMongoProxyEndpoints.includes(configContext.MONGO_PROXY_ENDPOINT)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -122,14 +122,21 @@ const pollDataTransferJobOperation = async (
|
||||
|
||||
updateDataTransferJob(body);
|
||||
|
||||
if (status === "Cancelled" || status === "Failed" || status === "Faulted") {
|
||||
if (status === "Cancelled") {
|
||||
removeFromPolling(jobName);
|
||||
clearMessage && clearMessage();
|
||||
const cancelMessage = `Data transfer job ${jobName} cancelled`;
|
||||
NotificationConsoleUtils.logConsoleError(cancelMessage);
|
||||
throw new AbortError(cancelMessage);
|
||||
}
|
||||
if (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`);
|
||||
NotificationConsoleUtils.logConsoleError(`Data transfer job ${jobName} failed: ${errorMessage}`);
|
||||
throw new AbortError(error);
|
||||
}
|
||||
if (status === "Completed") {
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { CassandraProxyEndpoints, JunoEndpoints, MongoProxyEndpoints } from "Common/Constants";
|
||||
import {
|
||||
BackendApi,
|
||||
CassandraProxyEndpoints,
|
||||
JunoEndpoints,
|
||||
MongoProxyEndpoints,
|
||||
PortalBackendEndpoints,
|
||||
} from "Common/Constants";
|
||||
import {
|
||||
allowedAadEndpoints,
|
||||
allowedArcadiaEndpoints,
|
||||
@@ -39,11 +45,15 @@ export interface ConfigContext {
|
||||
ARCADIA_ENDPOINT: string;
|
||||
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: string;
|
||||
BACKEND_ENDPOINT?: string;
|
||||
PORTAL_BACKEND_ENDPOINT?: string;
|
||||
NEW_BACKEND_APIS?: BackendApi[];
|
||||
MONGO_BACKEND_ENDPOINT?: string;
|
||||
MONGO_PROXY_ENDPOINT?: string;
|
||||
MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED?: boolean;
|
||||
NEW_MONGO_APIS?: string[];
|
||||
CASSANDRA_PROXY_ENDPOINT?: string;
|
||||
CASSANDRA_PROXY_OUTBOUND_IPS_ALLOWLISTED: boolean;
|
||||
NEW_CASSANDRA_APIS?: string[];
|
||||
PROXY_PATH?: string;
|
||||
JUNO_ENDPOINT: string;
|
||||
GITHUB_CLIENT_ID: string;
|
||||
@@ -88,17 +98,21 @@ let configContext: Readonly<ConfigContext> = {
|
||||
GITHUB_TEST_ENV_CLIENT_ID: "b63fc8cbf87fd3c6e2eb", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1777772
|
||||
JUNO_ENDPOINT: JunoEndpoints.Prod,
|
||||
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
||||
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Prod,
|
||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||
NEW_MONGO_APIS: [
|
||||
// "resourcelist",
|
||||
// "createDocument",
|
||||
// "readDocument",
|
||||
// "updateDocument",
|
||||
// "deleteDocument",
|
||||
// "createCollectionWithProxy",
|
||||
"resourcelist",
|
||||
"queryDocuments",
|
||||
"createDocument",
|
||||
"readDocument",
|
||||
"updateDocument",
|
||||
"deleteDocument",
|
||||
"createCollectionWithProxy",
|
||||
],
|
||||
MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED: false,
|
||||
CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Prod,
|
||||
NEW_CASSANDRA_APIS: ["postQuery", "createOrDelete", "getKeys", "getSchema"],
|
||||
CASSANDRA_PROXY_OUTBOUND_IPS_ALLOWLISTED: false,
|
||||
isTerminalEnabled: false,
|
||||
isPhoenixEnabled: false,
|
||||
};
|
||||
|
||||
@@ -159,6 +159,7 @@ export interface Collection extends Resource {
|
||||
geospatialConfig?: GeospatialConfig;
|
||||
schema?: ISchema;
|
||||
requestSchema?: () => void;
|
||||
computedProperties?: ComputedProperties;
|
||||
}
|
||||
|
||||
export interface CollectionsWithPagination {
|
||||
@@ -197,6 +198,13 @@ export interface IndexingPolicy {
|
||||
spatialIndexes?: any;
|
||||
}
|
||||
|
||||
export interface ComputedProperty {
|
||||
name: string;
|
||||
query: string;
|
||||
}
|
||||
|
||||
export type ComputedProperties = ComputedProperty[];
|
||||
|
||||
export interface PartitionKey {
|
||||
paths: string[];
|
||||
kind: "Hash" | "Range" | "MultiHash";
|
||||
|
||||
@@ -53,6 +53,7 @@ export type FabricMessageV2 =
|
||||
id: string;
|
||||
message: {
|
||||
connectionId: string;
|
||||
isVisible: boolean;
|
||||
};
|
||||
}
|
||||
| {
|
||||
@@ -72,7 +73,7 @@ export type FabricMessageV2 =
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: "setToolbarStatus";
|
||||
type: "explorerVisible";
|
||||
message: {
|
||||
visible: boolean;
|
||||
};
|
||||
|
||||
@@ -47,6 +47,7 @@ export enum MessageTypes {
|
||||
GetAllResourceTokens, // Data Explorer -> Fabric
|
||||
Ready, // Data Explorer -> Fabric
|
||||
OpenCESCVAFeedbackBlade,
|
||||
ActivateTab,
|
||||
}
|
||||
|
||||
export interface AuthorizationToken {
|
||||
|
||||
@@ -135,6 +135,7 @@ export interface Collection extends CollectionBase {
|
||||
changeFeedPolicy: ko.Observable<DataModels.ChangeFeedPolicy>;
|
||||
geospatialConfig: ko.Observable<DataModels.GeospatialConfig>;
|
||||
documentIds: ko.ObservableArray<DocumentId>;
|
||||
computedProperties: ko.Observable<DataModels.ComputedProperties>;
|
||||
|
||||
cassandraKeys: CassandraTableKeys;
|
||||
cassandraSchema: CassandraTableKey[];
|
||||
@@ -386,6 +387,7 @@ export interface DataExplorerInputsFrame {
|
||||
dnsSuffix?: string;
|
||||
serverId?: string;
|
||||
extensionEndpoint?: string;
|
||||
portalBackendEndpoint?: string;
|
||||
mongoProxyEndpoint?: string;
|
||||
cassandraProxyEndpoint?: string;
|
||||
subscriptionType?: SubscriptionType;
|
||||
@@ -407,6 +409,7 @@ export interface DataExplorerInputsFrame {
|
||||
features?: {
|
||||
[key: string]: string;
|
||||
};
|
||||
feedbackPolicies?: any;
|
||||
}
|
||||
|
||||
export interface SelfServeFrameInputs {
|
||||
|
||||
@@ -46,9 +46,25 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
|
||||
}, 100);
|
||||
}
|
||||
|
||||
public componentDidUpdate(previous: EditorReactProps) {
|
||||
if (this.props.content !== previous.content) {
|
||||
this.editor?.setValue(this.props.content);
|
||||
public componentDidUpdate() {
|
||||
if (!this.editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
const existingContent = this.editor.getModel().getValue();
|
||||
|
||||
if (this.props.content !== existingContent) {
|
||||
if (this.props.isReadOnly) {
|
||||
this.editor.setValue(this.props.content);
|
||||
} else {
|
||||
this.editor.pushUndoStop();
|
||||
this.editor.executeEdits("", [
|
||||
{
|
||||
range: this.editor.getModel().getFullModelRange(),
|
||||
text: this.props.content,
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,9 +87,14 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
|
||||
|
||||
protected configureEditor(editor: monaco.editor.IStandaloneCodeEditor) {
|
||||
this.editor = editor;
|
||||
const queryEditorModel = this.editor.getModel();
|
||||
if (!this.props.isReadOnly && this.props.onContentChanged) {
|
||||
queryEditorModel.onDidChangeContent(() => {
|
||||
// Hooking the model's onDidChangeContent event because of some event ordering issues.
|
||||
// If a single user input causes BOTH the editor content to change AND the cursor selection to change (which is likely),
|
||||
// then there are some inconsistencies as to which event fires first.
|
||||
// But the editor.onDidChangeModelContent event seems to always fire before the cursor selection event.
|
||||
// (This is NOT true for the model's onDidChangeContent event, which sometimes fires after the cursor selection event.)
|
||||
// If the cursor selection event fires first, then the calling component may re-render the component with old content, so we want to ensure the model content changed event always fires first.
|
||||
this.editor.onDidChangeModelContent(() => {
|
||||
const queryEditorModel = this.editor.getModel();
|
||||
this.props.onContentChanged(queryEditorModel.getValue());
|
||||
});
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { IPivotItemProps, IPivotProps, Pivot, PivotItem } from "@fluentui/react";
|
||||
import {
|
||||
ComputedPropertiesComponent,
|
||||
ComputedPropertiesComponentProps,
|
||||
} from "Explorer/Controls/Settings/SettingsSubComponents/ComputedPropertiesComponent";
|
||||
import { useDatabases } from "Explorer/useDatabases";
|
||||
import { isRunningOnPublicCloud } from "Utils/CloudUtils";
|
||||
import * as React from "react";
|
||||
@@ -108,6 +112,11 @@ export interface SettingsComponentState {
|
||||
indexesToAdd: AddMongoIndexProps[];
|
||||
indexTransformationProgress: number;
|
||||
|
||||
computedPropertiesContent: DataModels.ComputedProperties;
|
||||
computedPropertiesContentBaseline: DataModels.ComputedProperties;
|
||||
shouldDiscardComputedProperties: boolean;
|
||||
isComputedPropertiesDirty: boolean;
|
||||
|
||||
conflictResolutionPolicyMode: DataModels.ConflictResolutionMode;
|
||||
conflictResolutionPolicyModeBaseline: DataModels.ConflictResolutionMode;
|
||||
conflictResolutionPolicyPath: string;
|
||||
@@ -132,6 +141,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
private offer: DataModels.Offer;
|
||||
private changeFeedPolicyVisible: boolean;
|
||||
private isFixedContainer: boolean;
|
||||
private shouldShowComputedPropertiesEditor: boolean;
|
||||
private shouldShowIndexingPolicyEditor: boolean;
|
||||
private shouldShowPartitionKeyEditor: boolean;
|
||||
private totalThroughputUsed: number;
|
||||
@@ -145,6 +155,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
this.collection = this.props.settingsTab.collection as ViewModels.Collection;
|
||||
this.offer = this.collection?.offer();
|
||||
this.isAnalyticalStorageEnabled = !!this.collection?.analyticalStorageTtl();
|
||||
this.shouldShowComputedPropertiesEditor = userContext.apiType === "SQL";
|
||||
this.shouldShowIndexingPolicyEditor = userContext.apiType !== "Cassandra" && userContext.apiType !== "Mongo";
|
||||
this.shouldShowPartitionKeyEditor = userContext.apiType === "SQL" && isRunningOnPublicCloud();
|
||||
|
||||
@@ -198,6 +209,11 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
isMongoIndexingPolicyDiscardable: false,
|
||||
indexTransformationProgress: undefined,
|
||||
|
||||
computedPropertiesContent: undefined,
|
||||
computedPropertiesContentBaseline: undefined,
|
||||
shouldDiscardComputedProperties: false,
|
||||
isComputedPropertiesDirty: false,
|
||||
|
||||
conflictResolutionPolicyMode: undefined,
|
||||
conflictResolutionPolicyModeBaseline: undefined,
|
||||
conflictResolutionPolicyPath: undefined,
|
||||
@@ -288,6 +304,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
this.state.isSubSettingsSaveable ||
|
||||
this.state.isIndexingPolicyDirty ||
|
||||
this.state.isConflictResolutionDirty ||
|
||||
this.state.isComputedPropertiesDirty ||
|
||||
(!!this.state.currentMongoIndexes && this.state.isMongoIndexingPolicySaveable)
|
||||
);
|
||||
};
|
||||
@@ -298,6 +315,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
this.state.isSubSettingsDiscardable ||
|
||||
this.state.isIndexingPolicyDirty ||
|
||||
this.state.isConflictResolutionDirty ||
|
||||
this.state.isComputedPropertiesDirty ||
|
||||
(!!this.state.currentMongoIndexes && this.state.isMongoIndexingPolicyDiscardable)
|
||||
);
|
||||
};
|
||||
@@ -402,6 +420,9 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
isMongoIndexingPolicySaveable: false,
|
||||
isMongoIndexingPolicyDiscardable: false,
|
||||
isConflictResolutionDirty: false,
|
||||
computedPropertiesContent: this.state.computedPropertiesContentBaseline,
|
||||
shouldDiscardComputedProperties: true,
|
||||
isComputedPropertiesDirty: false,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -521,6 +542,31 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
private onMongoIndexingPolicyDiscardableChange = (isMongoIndexingPolicyDiscardable: boolean): void =>
|
||||
this.setState({ isMongoIndexingPolicyDiscardable });
|
||||
|
||||
private onComputedPropertiesContentChange = (newComputedProperties: DataModels.ComputedProperties): void =>
|
||||
this.setState({ computedPropertiesContent: newComputedProperties });
|
||||
|
||||
private resetShouldDiscardComputedProperties = (): void => this.setState({ shouldDiscardComputedProperties: false });
|
||||
|
||||
private logComputedPropertiesSuccessMessage = (): void => {
|
||||
if (this.props.settingsTab.onLoadStartKey) {
|
||||
traceSuccess(
|
||||
Action.Tab,
|
||||
{
|
||||
databaseName: this.collection.databaseId,
|
||||
collectionName: this.collection.id(),
|
||||
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
tabTitle: this.props.settingsTab.tabTitle(),
|
||||
},
|
||||
this.props.settingsTab.onLoadStartKey,
|
||||
);
|
||||
this.props.settingsTab.onLoadStartKey = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
private onComputedPropertiesDirtyChange = (isComputedPropertiesDirty: boolean): void =>
|
||||
this.setState({ isComputedPropertiesDirty: isComputedPropertiesDirty });
|
||||
|
||||
private calculateTotalThroughputUsed = (): void => {
|
||||
this.totalThroughputUsed = 0;
|
||||
(useDatabases.getState().databases || []).forEach(async (database) => {
|
||||
@@ -643,7 +689,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
const indexingPolicyContent = this.collection.indexingPolicy();
|
||||
const conflictResolutionPolicy: DataModels.ConflictResolutionPolicy =
|
||||
this.collection.conflictResolutionPolicy && this.collection.conflictResolutionPolicy();
|
||||
|
||||
const conflictResolutionPolicyMode = parseConflictResolutionMode(conflictResolutionPolicy?.mode);
|
||||
const conflictResolutionPolicyPath = conflictResolutionPolicy?.conflictResolutionPath;
|
||||
const conflictResolutionPolicyProcedure = parseConflictResolutionProcedure(
|
||||
@@ -652,6 +697,12 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
const geospatialConfigTypeString: string =
|
||||
(this.collection.geospatialConfig && this.collection.geospatialConfig()?.type) || GeospatialConfigType.Geometry;
|
||||
const geoSpatialConfigType = GeospatialConfigType[geospatialConfigTypeString as keyof typeof GeospatialConfigType];
|
||||
let computedPropertiesContent = this.collection.computedProperties();
|
||||
if (!computedPropertiesContent || computedPropertiesContent.length === 0) {
|
||||
computedPropertiesContent = [
|
||||
{ name: "name_of_property", query: "query_to_compute_property" },
|
||||
] as DataModels.ComputedProperties;
|
||||
}
|
||||
|
||||
return {
|
||||
throughput: offerThroughput,
|
||||
@@ -678,6 +729,8 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
conflictResolutionPolicyProcedureBaseline: conflictResolutionPolicyProcedure,
|
||||
geospatialConfigType: geoSpatialConfigType,
|
||||
geospatialConfigTypeBaseline: geoSpatialConfigType,
|
||||
computedPropertiesContent: computedPropertiesContent,
|
||||
computedPropertiesContentBaseline: computedPropertiesContent,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -794,7 +847,12 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
private saveCollectionSettings = async (startKey: number): Promise<void> => {
|
||||
const newCollection: DataModels.Collection = { ...this.collection.rawDataModel };
|
||||
|
||||
if (this.state.isSubSettingsSaveable || this.state.isIndexingPolicyDirty || this.state.isConflictResolutionDirty) {
|
||||
if (
|
||||
this.state.isSubSettingsSaveable ||
|
||||
this.state.isIndexingPolicyDirty ||
|
||||
this.state.isConflictResolutionDirty ||
|
||||
this.state.isComputedPropertiesDirty
|
||||
) {
|
||||
let defaultTtl: number;
|
||||
switch (this.state.timeToLive) {
|
||||
case TtlType.On:
|
||||
@@ -832,6 +890,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
newCollection.conflictResolutionPolicy = conflictResolutionChanges;
|
||||
}
|
||||
|
||||
if (this.state.isComputedPropertiesDirty) {
|
||||
newCollection.computedProperties = this.state.computedPropertiesContent;
|
||||
}
|
||||
|
||||
const updatedCollection: DataModels.Collection = await updateCollection(
|
||||
this.collection.databaseId,
|
||||
this.collection.id(),
|
||||
@@ -845,6 +907,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
this.collection.conflictResolutionPolicy(updatedCollection.conflictResolutionPolicy);
|
||||
this.collection.changeFeedPolicy(updatedCollection.changeFeedPolicy);
|
||||
this.collection.geospatialConfig(updatedCollection.geospatialConfig);
|
||||
this.collection.computedProperties(updatedCollection.computedProperties);
|
||||
|
||||
if (wasIndexingPolicyModified) {
|
||||
await this.refreshIndexTransformationProgress();
|
||||
@@ -855,6 +918,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
isSubSettingsDiscardable: false,
|
||||
isIndexingPolicyDirty: false,
|
||||
isConflictResolutionDirty: false,
|
||||
isComputedPropertiesDirty: false,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1049,6 +1113,16 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
onMongoIndexingPolicyDiscardableChange: this.onMongoIndexingPolicyDiscardableChange,
|
||||
};
|
||||
|
||||
const computedPropertiesComponentProps: ComputedPropertiesComponentProps = {
|
||||
computedPropertiesContent: this.state.computedPropertiesContent,
|
||||
computedPropertiesContentBaseline: this.state.computedPropertiesContentBaseline,
|
||||
logComputedPropertiesSuccessMessage: this.logComputedPropertiesSuccessMessage,
|
||||
onComputedPropertiesContentChange: this.onComputedPropertiesContentChange,
|
||||
onComputedPropertiesDirtyChange: this.onComputedPropertiesDirtyChange,
|
||||
resetShouldDiscardComputedProperties: this.resetShouldDiscardComputedProperties,
|
||||
shouldDiscardComputedProperties: this.state.shouldDiscardComputedProperties,
|
||||
};
|
||||
|
||||
const conflictResolutionPolicyComponentProps: ConflictResolutionComponentProps = {
|
||||
collection: this.collection,
|
||||
conflictResolutionPolicyMode: this.state.conflictResolutionPolicyMode,
|
||||
@@ -1111,6 +1185,13 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
});
|
||||
}
|
||||
|
||||
if (this.shouldShowComputedPropertiesEditor) {
|
||||
tabs.push({
|
||||
tab: SettingsV2TabTypes.ComputedPropertiesTab,
|
||||
content: <ComputedPropertiesComponent {...computedPropertiesComponentProps} />,
|
||||
});
|
||||
}
|
||||
|
||||
const pivotProps: IPivotProps = {
|
||||
onLinkClick: this.onPivotChange,
|
||||
selectedKey: SettingsV2TabTypes[this.state.selectedTab],
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
getThroughputApplyLongDelayMessage,
|
||||
getThroughputApplyShortDelayMessage,
|
||||
getToolTipContainer,
|
||||
indexingPolicynUnsavedWarningMessage,
|
||||
manualToAutoscaleDisclaimerElement,
|
||||
mongoIndexTransformationRefreshingMessage,
|
||||
mongoIndexingPolicyAADError,
|
||||
@@ -39,7 +38,6 @@ class SettingsRenderUtilsTestComponent extends React.Component {
|
||||
|
||||
{manualToAutoscaleDisclaimerElement}
|
||||
{ttlWarning}
|
||||
{indexingPolicynUnsavedWarningMessage}
|
||||
{updateThroughputDelayedApplyWarningMessage}
|
||||
|
||||
{getThroughputApplyDelayedMessage(false, 1000, "RU/s", "sampleDb", "sampleCollection", 2000)}
|
||||
|
||||
@@ -61,6 +61,8 @@ export interface PriceBreakdown {
|
||||
currencySign: string;
|
||||
}
|
||||
|
||||
export type editorType = "indexPolicy" | "computedProperties";
|
||||
|
||||
export const infoAndToolTipTextStyle: ITextStyles = { root: { fontSize: 14, color: "windowtext" } };
|
||||
|
||||
export const noLeftPaddingCheckBoxStyle: ICheckboxStyles = {
|
||||
@@ -254,9 +256,10 @@ export const ttlWarning: JSX.Element = (
|
||||
</Text>
|
||||
);
|
||||
|
||||
export const indexingPolicynUnsavedWarningMessage: JSX.Element = (
|
||||
export const unsavedEditorWarningMessage = (editor: editorType): JSX.Element => (
|
||||
<Text styles={infoAndToolTipTextStyle}>
|
||||
You have not saved the latest changes made to your indexing policy. Please click save to confirm the changes.
|
||||
You have not saved the latest changes made to your{" "}
|
||||
{editor === "indexPolicy" ? "indexing policy" : "computed properties"}. Please click save to confirm the changes.
|
||||
</Text>
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import * as DataModels from "Contracts/DataModels";
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import { ComputedPropertiesComponent, ComputedPropertiesComponentProps } from "./ComputedPropertiesComponent";
|
||||
|
||||
describe("ComputedPropertiesComponent", () => {
|
||||
const initialComputedPropertiesContent: DataModels.ComputedProperties = [
|
||||
{
|
||||
name: "prop1",
|
||||
query: "query1",
|
||||
},
|
||||
];
|
||||
const baseProps: ComputedPropertiesComponentProps = {
|
||||
computedPropertiesContent: initialComputedPropertiesContent,
|
||||
computedPropertiesContentBaseline: initialComputedPropertiesContent,
|
||||
logComputedPropertiesSuccessMessage: () => {
|
||||
return;
|
||||
},
|
||||
onComputedPropertiesContentChange: () => {
|
||||
return;
|
||||
},
|
||||
onComputedPropertiesDirtyChange: () => {
|
||||
return;
|
||||
},
|
||||
resetShouldDiscardComputedProperties: () => {
|
||||
return;
|
||||
},
|
||||
shouldDiscardComputedProperties: false,
|
||||
};
|
||||
|
||||
it("renders", () => {
|
||||
const wrapper = shallow(<ComputedPropertiesComponent {...baseProps} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("computed properties are reset", () => {
|
||||
const wrapper = shallow(<ComputedPropertiesComponent {...baseProps} />);
|
||||
|
||||
const computedPropertiesComponentInstance = wrapper.instance() as ComputedPropertiesComponent;
|
||||
const resetComputedPropertiesEditorMockFn = jest.fn();
|
||||
computedPropertiesComponentInstance.resetComputedPropertiesEditor = resetComputedPropertiesEditorMockFn;
|
||||
|
||||
wrapper.setProps({ shouldDiscardComputedProperties: true });
|
||||
wrapper.update();
|
||||
expect(resetComputedPropertiesEditorMockFn.mock.calls.length).toEqual(1);
|
||||
});
|
||||
|
||||
it("dirty is set", () => {
|
||||
let computedPropertiesComponent = new ComputedPropertiesComponent(baseProps);
|
||||
expect(computedPropertiesComponent.IsComponentDirty()).toEqual(false);
|
||||
|
||||
const newProps = { ...baseProps, computedPropertiesContent: undefined as DataModels.ComputedProperties };
|
||||
computedPropertiesComponent = new ComputedPropertiesComponent(newProps);
|
||||
expect(computedPropertiesComponent.IsComponentDirty()).toEqual(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,128 @@
|
||||
import { FontIcon, Link, MessageBar, MessageBarType, Stack, Text } from "@fluentui/react";
|
||||
import * as DataModels from "Contracts/DataModels";
|
||||
import { titleAndInputStackProps, unsavedEditorWarningMessage } from "Explorer/Controls/Settings/SettingsRenderUtils";
|
||||
import { isDirty } from "Explorer/Controls/Settings/SettingsUtils";
|
||||
import { loadMonaco } from "Explorer/LazyMonaco";
|
||||
import * as monaco from "monaco-editor";
|
||||
import * as React from "react";
|
||||
|
||||
export interface ComputedPropertiesComponentProps {
|
||||
computedPropertiesContent: DataModels.ComputedProperties;
|
||||
computedPropertiesContentBaseline: DataModels.ComputedProperties;
|
||||
logComputedPropertiesSuccessMessage: () => void;
|
||||
onComputedPropertiesContentChange: (newComputedProperties: DataModels.ComputedProperties) => void;
|
||||
onComputedPropertiesDirtyChange: (isComputedPropertiesDirty: boolean) => void;
|
||||
resetShouldDiscardComputedProperties: () => void;
|
||||
shouldDiscardComputedProperties: boolean;
|
||||
}
|
||||
|
||||
interface ComputedPropertiesComponentState {
|
||||
computedPropertiesContentIsValid: boolean;
|
||||
}
|
||||
|
||||
export class ComputedPropertiesComponent extends React.Component<
|
||||
ComputedPropertiesComponentProps,
|
||||
ComputedPropertiesComponentState
|
||||
> {
|
||||
private shouldCheckComponentIsDirty = true;
|
||||
private computedPropertiesDiv = React.createRef<HTMLDivElement>();
|
||||
private computedPropertiesEditor: monaco.editor.IStandaloneCodeEditor;
|
||||
|
||||
constructor(props: ComputedPropertiesComponentProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
computedPropertiesContentIsValid: true,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(): void {
|
||||
if (this.props.shouldDiscardComputedProperties) {
|
||||
this.resetComputedPropertiesEditor();
|
||||
this.props.resetShouldDiscardComputedProperties();
|
||||
}
|
||||
this.onComponentUpdate();
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
this.resetComputedPropertiesEditor();
|
||||
this.onComponentUpdate();
|
||||
}
|
||||
|
||||
public resetComputedPropertiesEditor = (): void => {
|
||||
if (!this.computedPropertiesEditor) {
|
||||
this.createComputedPropertiesEditor();
|
||||
} else {
|
||||
const indexingPolicyEditorModel = this.computedPropertiesEditor.getModel();
|
||||
const value: string = JSON.stringify(this.props.computedPropertiesContent, undefined, 4);
|
||||
indexingPolicyEditorModel.setValue(value);
|
||||
}
|
||||
this.onComponentUpdate();
|
||||
};
|
||||
|
||||
private onComponentUpdate = (): void => {
|
||||
if (!this.shouldCheckComponentIsDirty) {
|
||||
this.shouldCheckComponentIsDirty = true;
|
||||
return;
|
||||
}
|
||||
this.props.onComputedPropertiesDirtyChange(this.IsComponentDirty());
|
||||
this.shouldCheckComponentIsDirty = false;
|
||||
};
|
||||
|
||||
public IsComponentDirty = (): boolean => {
|
||||
if (
|
||||
isDirty(this.props.computedPropertiesContent, this.props.computedPropertiesContentBaseline) &&
|
||||
this.state.computedPropertiesContentIsValid
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
private async createComputedPropertiesEditor(): Promise<void> {
|
||||
const value: string = JSON.stringify(this.props.computedPropertiesContent, undefined, 4);
|
||||
const monaco = await loadMonaco();
|
||||
this.computedPropertiesEditor = monaco.editor.create(this.computedPropertiesDiv.current, {
|
||||
value: value,
|
||||
language: "json",
|
||||
ariaLabel: "Computed properties",
|
||||
});
|
||||
if (this.computedPropertiesEditor) {
|
||||
const computedPropertiesEditorModel = this.computedPropertiesEditor.getModel();
|
||||
computedPropertiesEditorModel.onDidChangeContent(this.onEditorContentChange.bind(this));
|
||||
this.props.logComputedPropertiesSuccessMessage();
|
||||
}
|
||||
}
|
||||
|
||||
private onEditorContentChange = (): void => {
|
||||
const computedPropertiesEditorModel = this.computedPropertiesEditor.getModel();
|
||||
try {
|
||||
const newComputedPropertiesContent = JSON.parse(
|
||||
computedPropertiesEditorModel.getValue(),
|
||||
) as DataModels.ComputedProperties;
|
||||
this.props.onComputedPropertiesContentChange(newComputedPropertiesContent);
|
||||
this.setState({ computedPropertiesContentIsValid: true });
|
||||
} catch (e) {
|
||||
this.setState({ computedPropertiesContentIsValid: false });
|
||||
}
|
||||
};
|
||||
|
||||
public render(): JSX.Element {
|
||||
return (
|
||||
<Stack {...titleAndInputStackProps}>
|
||||
{isDirty(this.props.computedPropertiesContent, this.props.computedPropertiesContentBaseline) && (
|
||||
<MessageBar messageBarType={MessageBarType.warning}>
|
||||
{unsavedEditorWarningMessage("computedProperties")}
|
||||
</MessageBar>
|
||||
)}
|
||||
<Text style={{ marginLeft: "30px", marginBottom: "10px" }}>
|
||||
<Link target="_blank" href="https://aka.ms/computed-properties-preview/">
|
||||
{"Learn more"} <FontIcon iconName="NavigateExternalInline" />
|
||||
</Link>
|
||||
  about how to define computed properties and how to use them.
|
||||
</Text>
|
||||
<div className="settingsV2IndexingPolicyEditor" tabIndex={0} ref={this.computedPropertiesDiv}></div>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import * as monaco from "monaco-editor";
|
||||
import * as React from "react";
|
||||
import * as DataModels from "../../../../Contracts/DataModels";
|
||||
import { loadMonaco } from "../../../LazyMonaco";
|
||||
import { indexingPolicynUnsavedWarningMessage, titleAndInputStackProps } from "../SettingsRenderUtils";
|
||||
import { titleAndInputStackProps, unsavedEditorWarningMessage } from "../SettingsRenderUtils";
|
||||
import { isDirty, isIndexTransforming } from "../SettingsUtils";
|
||||
import { IndexingPolicyRefreshComponent } from "./IndexingPolicyRefresh/IndexingPolicyRefreshComponent";
|
||||
|
||||
@@ -120,7 +120,7 @@ export class IndexingPolicyComponent extends React.Component<
|
||||
refreshIndexTransformationProgress={this.props.refreshIndexTransformationProgress}
|
||||
/>
|
||||
{isDirty(this.props.indexingPolicyContent, this.props.indexingPolicyContentBaseline) && (
|
||||
<MessageBar messageBarType={MessageBarType.warning}>{indexingPolicynUnsavedWarningMessage}</MessageBar>
|
||||
<MessageBar messageBarType={MessageBarType.warning}>{unsavedEditorWarningMessage("indexPolicy")}</MessageBar>
|
||||
)}
|
||||
<div className="settingsV2IndexingPolicyEditor" tabIndex={0} ref={this.indexingPolicyDiv}></div>
|
||||
</Stack>
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
addMongoIndexStackProps,
|
||||
createAndAddMongoIndexStackProps,
|
||||
customDetailsListStyles,
|
||||
indexingPolicynUnsavedWarningMessage,
|
||||
infoAndToolTipTextStyle,
|
||||
mediumWidthStackStyles,
|
||||
mongoCompoundIndexNotSupportedMessage,
|
||||
@@ -27,15 +26,16 @@ import {
|
||||
onRenderRow,
|
||||
separatorStyles,
|
||||
subComponentStackProps,
|
||||
unsavedEditorWarningMessage,
|
||||
} from "../../SettingsRenderUtils";
|
||||
import {
|
||||
AddMongoIndexProps,
|
||||
getMongoIndexType,
|
||||
getMongoIndexTypeText,
|
||||
isIndexTransforming,
|
||||
MongoIndexIdField,
|
||||
MongoIndexTypes,
|
||||
MongoNotificationType,
|
||||
getMongoIndexType,
|
||||
getMongoIndexTypeText,
|
||||
isIndexTransforming,
|
||||
} from "../../SettingsUtils";
|
||||
import { IndexingPolicyRefreshComponent } from "../IndexingPolicyRefresh/IndexingPolicyRefreshComponent";
|
||||
import { AddMongoIndexComponent } from "./AddMongoIndexComponent";
|
||||
@@ -297,7 +297,7 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
|
||||
if (this.getMongoWarningNotificationMessage()) {
|
||||
warningMessage = this.getMongoWarningNotificationMessage();
|
||||
} else if (this.isMongoIndexingPolicySaveable()) {
|
||||
warningMessage = indexingPolicynUnsavedWarningMessage;
|
||||
warningMessage = unsavedEditorWarningMessage("indexPolicy");
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -136,15 +136,15 @@ export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({ da
|
||||
};
|
||||
|
||||
const getPercentageComplete = () => {
|
||||
const jobStatus = portalDataTransferJob?.properties?.status;
|
||||
const isCompleted = jobStatus === "Completed";
|
||||
if (isCompleted) {
|
||||
return 1;
|
||||
}
|
||||
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;
|
||||
const isJobInProgress = isCurrentJobInProgress(portalDataTransferJob);
|
||||
return isJobInProgress ? (totalCount > 0 ? processedCount / totalCount : null) : 0;
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ComputedPropertiesComponent renders 1`] = `
|
||||
<Stack
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 5,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"marginBottom": "10px",
|
||||
"marginLeft": "30px",
|
||||
}
|
||||
}
|
||||
>
|
||||
<StyledLinkBase
|
||||
href="https://aka.ms/computed-properties-preview/"
|
||||
target="_blank"
|
||||
>
|
||||
Learn more
|
||||
|
||||
<FontIcon
|
||||
iconName="NavigateExternalInline"
|
||||
/>
|
||||
</StyledLinkBase>
|
||||
about how to define computed properties and how to use them.
|
||||
</Text>
|
||||
<div
|
||||
className="settingsV2IndexingPolicyEditor"
|
||||
tabIndex={0}
|
||||
/>
|
||||
</Stack>
|
||||
`;
|
||||
@@ -4,7 +4,7 @@ import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { MongoIndex } from "../../../Utils/arm/generatedClients/cosmos/types";
|
||||
|
||||
const zeroValue = 0;
|
||||
export type isDirtyTypes = boolean | string | number | DataModels.IndexingPolicy;
|
||||
export type isDirtyTypes = boolean | string | number | DataModels.IndexingPolicy | DataModels.ComputedProperties;
|
||||
export const TtlOff = "off";
|
||||
export const TtlOn = "on";
|
||||
export const TtlOnNoDefault = "on-nodefault";
|
||||
@@ -46,6 +46,7 @@ export enum SettingsV2TabTypes {
|
||||
SubSettingsTab,
|
||||
IndexingPolicyTab,
|
||||
PartitionKeyTab,
|
||||
ComputedPropertiesTab,
|
||||
}
|
||||
|
||||
export interface IsComponentDirtyResult {
|
||||
@@ -148,7 +149,9 @@ export const getTabTitle = (tab: SettingsV2TabTypes): string => {
|
||||
case SettingsV2TabTypes.IndexingPolicyTab:
|
||||
return "Indexing Policy";
|
||||
case SettingsV2TabTypes.PartitionKeyTab:
|
||||
return "Partition Keys";
|
||||
return "Partition Keys (preview)";
|
||||
case SettingsV2TabTypes.ComputedPropertiesTab:
|
||||
return "Computed Properties (preview)";
|
||||
default:
|
||||
throw new Error(`Unknown tab ${tab}`);
|
||||
}
|
||||
|
||||
@@ -40,6 +40,12 @@ export const collection = {
|
||||
version: 2,
|
||||
},
|
||||
partitionKeyProperties: ["partitionKey"],
|
||||
computedProperties: ko.observable<DataModels.ComputedProperties>([
|
||||
{
|
||||
name: "queryName",
|
||||
query: "query",
|
||||
},
|
||||
]),
|
||||
readSettings: () => {
|
||||
return;
|
||||
},
|
||||
|
||||
@@ -26,6 +26,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
Object {
|
||||
"analyticalStorageTtl": [Function],
|
||||
"changeFeedPolicy": [Function],
|
||||
"computedProperties": [Function],
|
||||
"conflictResolutionPolicy": [Function],
|
||||
"container": Explorer {
|
||||
"_isInitializingNotebooks": false,
|
||||
@@ -103,6 +104,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
Object {
|
||||
"analyticalStorageTtl": [Function],
|
||||
"changeFeedPolicy": [Function],
|
||||
"computedProperties": [Function],
|
||||
"conflictResolutionPolicy": [Function],
|
||||
"container": Explorer {
|
||||
"_isInitializingNotebooks": false,
|
||||
@@ -205,7 +207,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
/>
|
||||
</PivotItem>
|
||||
<PivotItem
|
||||
headerText="Partition Keys"
|
||||
headerText="Partition Keys (preview)"
|
||||
itemKey="PartitionKeyTab"
|
||||
key="PartitionKeyTab"
|
||||
style={
|
||||
@@ -219,6 +221,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
Object {
|
||||
"analyticalStorageTtl": [Function],
|
||||
"changeFeedPolicy": [Function],
|
||||
"computedProperties": [Function],
|
||||
"conflictResolutionPolicy": [Function],
|
||||
"container": Explorer {
|
||||
"_isInitializingNotebooks": false,
|
||||
@@ -296,6 +299,40 @@ exports[`SettingsComponent renders 1`] = `
|
||||
}
|
||||
/>
|
||||
</PivotItem>
|
||||
<PivotItem
|
||||
headerText="Computed Properties (preview)"
|
||||
itemKey="ComputedPropertiesTab"
|
||||
key="ComputedPropertiesTab"
|
||||
style={
|
||||
Object {
|
||||
"marginTop": 20,
|
||||
}
|
||||
}
|
||||
>
|
||||
<ComputedPropertiesComponent
|
||||
computedPropertiesContent={
|
||||
Array [
|
||||
Object {
|
||||
"name": "queryName",
|
||||
"query": "query",
|
||||
},
|
||||
]
|
||||
}
|
||||
computedPropertiesContentBaseline={
|
||||
Array [
|
||||
Object {
|
||||
"name": "queryName",
|
||||
"query": "query",
|
||||
},
|
||||
]
|
||||
}
|
||||
logComputedPropertiesSuccessMessage={[Function]}
|
||||
onComputedPropertiesContentChange={[Function]}
|
||||
onComputedPropertiesDirtyChange={[Function]}
|
||||
resetShouldDiscardComputedProperties={[Function]}
|
||||
shouldDiscardComputedProperties={false}
|
||||
/>
|
||||
</PivotItem>
|
||||
</StyledPivot>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -99,18 +99,6 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
</StyledLinkBase>
|
||||
.
|
||||
</Text>
|
||||
<Text
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
You have not saved the latest changes made to your indexing policy. Please click save to confirm the changes.
|
||||
</Text>
|
||||
<Text
|
||||
id="updateThroughputDelayedApplyWarningMessage"
|
||||
styles={
|
||||
|
||||
@@ -14,7 +14,13 @@
|
||||
.throughputInputSpacing > :not(:last-child) {
|
||||
margin-bottom: @DefaultSpace;
|
||||
}
|
||||
|
||||
.capacitycalculator-link:focus {
|
||||
text-decoration: underline;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.copyQuery:focus::after,
|
||||
.deleteQuery:focus::after {
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
@@ -247,7 +247,7 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
|
||||
name="More"
|
||||
title="More"
|
||||
className="treeMenuEllipsis"
|
||||
ariaLabel={menuItemLabel}
|
||||
ariaLabel={`${menuItemLabel} options`}
|
||||
menuIconProps={{
|
||||
iconName: menuItemLabel,
|
||||
styles: { root: { fontSize: "18px", fontWeight: "bold" } },
|
||||
|
||||
@@ -172,7 +172,7 @@ exports[`TreeNodeComponent renders a simple node (sorted children, expanded) 1`]
|
||||
onKeyPress={[Function]}
|
||||
>
|
||||
<CustomizedIconButton
|
||||
ariaLabel="More"
|
||||
ariaLabel="More options"
|
||||
className="treeMenuEllipsis"
|
||||
menuIconProps={
|
||||
Object {
|
||||
@@ -397,7 +397,7 @@ exports[`TreeNodeComponent renders sorted children, expanded, leaves and parents
|
||||
onKeyPress={[Function]}
|
||||
>
|
||||
<CustomizedIconButton
|
||||
ariaLabel="More"
|
||||
ariaLabel="More options"
|
||||
className="treeMenuEllipsis"
|
||||
menuIconProps={
|
||||
Object {
|
||||
|
||||
@@ -296,6 +296,14 @@ export default class Explorer {
|
||||
}
|
||||
}
|
||||
|
||||
public async openCESCVAFeedbackBlade(): Promise<void> {
|
||||
sendMessage({ type: MessageTypes.OpenCESCVAFeedbackBlade });
|
||||
Logger.logInfo(
|
||||
`CES CVA Feedback logging current date when survey is shown ${Date.now().toString()}`,
|
||||
"Explorer/openCESCVAFeedbackBlade",
|
||||
);
|
||||
}
|
||||
|
||||
public async refreshDatabaseForResourceToken(): Promise<void> {
|
||||
const databaseId = userContext.parsedResourceToken?.databaseId;
|
||||
const collectionId = userContext.parsedResourceToken?.collectionId;
|
||||
|
||||
@@ -2,10 +2,8 @@ import * as ko from "knockout";
|
||||
import { AuthType } from "../../../AuthType";
|
||||
import { DatabaseAccount } from "../../../Contracts/DataModels";
|
||||
import { CollectionBase } from "../../../Contracts/ViewModels";
|
||||
import { GitHubOAuthService } from "../../../GitHub/GitHubOAuthService";
|
||||
import { updateUserContext } from "../../../UserContext";
|
||||
import Explorer from "../../Explorer";
|
||||
import NotebookManager from "../../Notebook/NotebookManager";
|
||||
import { useNotebook } from "../../Notebook/useNotebook";
|
||||
import { useDatabases } from "../../useDatabases";
|
||||
import { useSelectedNode } from "../../useSelectedNode";
|
||||
@@ -72,181 +70,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Enable notebook button", () => {
|
||||
const enableNotebookBtnLabel = "Enable Notebooks (Preview)";
|
||||
const selectedNodeState = useSelectedNode.getState();
|
||||
|
||||
beforeAll(() => {
|
||||
mockExplorer = {} as Explorer;
|
||||
updateUserContext({
|
||||
portalEnv: "prod",
|
||||
databaseAccount: {
|
||||
properties: {
|
||||
capabilities: [{ name: "EnableTable" }],
|
||||
},
|
||||
} as DatabaseAccount,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
updateUserContext({
|
||||
portalEnv: "prod",
|
||||
});
|
||||
useNotebook.getState().setIsNotebookEnabled(false);
|
||||
useNotebook.getState().setIsNotebooksEnabledForAccount(false);
|
||||
});
|
||||
|
||||
it("Notebooks is already enabled - button should be hidden", () => {
|
||||
useNotebook.getState().setIsNotebookEnabled(true);
|
||||
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel);
|
||||
expect(enableNotebookBtn).toBeUndefined();
|
||||
});
|
||||
|
||||
it("Account is running on one of the national clouds - button should be hidden", () => {
|
||||
updateUserContext({
|
||||
portalEnv: "mooncake",
|
||||
});
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel);
|
||||
expect(enableNotebookBtn).toBeUndefined();
|
||||
});
|
||||
|
||||
it("Notebooks is not enabled but is available - button should be shown and enabled", () => {
|
||||
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel);
|
||||
|
||||
//TODO: modify once notebooks are available
|
||||
expect(enableNotebookBtn).toBeUndefined();
|
||||
//expect(enableNotebookBtn).toBeDefined();
|
||||
//expect(enableNotebookBtn.disabled).toBe(false);
|
||||
//expect(enableNotebookBtn.tooltipText).toBe("");
|
||||
});
|
||||
|
||||
it("Notebooks is not enabled and is unavailable - button should be shown and disabled", () => {
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel);
|
||||
|
||||
//TODO: modify once notebooks are available
|
||||
expect(enableNotebookBtn).toBeUndefined();
|
||||
//expect(enableNotebookBtn).toBeDefined();
|
||||
//expect(enableNotebookBtn.disabled).toBe(true);
|
||||
//expect(enableNotebookBtn.tooltipText).toBe(
|
||||
// "Notebooks are not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks."
|
||||
//);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Open Mongo shell button", () => {
|
||||
const openMongoShellBtnLabel = "Open Mongo shell";
|
||||
const selectedNodeState = useSelectedNode.getState();
|
||||
|
||||
beforeAll(() => {
|
||||
mockExplorer = {} as Explorer;
|
||||
updateUserContext({
|
||||
databaseAccount: {
|
||||
properties: {
|
||||
capabilities: [{ name: "EnableTable" }],
|
||||
},
|
||||
} as DatabaseAccount,
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
updateUserContext({
|
||||
apiType: "SQL",
|
||||
});
|
||||
useNotebook.getState().setIsShellEnabled(false);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
updateUserContext({
|
||||
apiType: "Mongo",
|
||||
});
|
||||
useNotebook.getState().setIsShellEnabled(true);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
useNotebook.getState().setIsNotebookEnabled(false);
|
||||
useNotebook.getState().setIsNotebooksEnabledForAccount(false);
|
||||
});
|
||||
|
||||
it("Mongo Api not available - button should be hidden", () => {
|
||||
updateUserContext({
|
||||
apiType: "SQL",
|
||||
});
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
|
||||
expect(openMongoShellBtn).toBeUndefined();
|
||||
});
|
||||
|
||||
it("Running on a national cloud - button should be hidden", () => {
|
||||
updateUserContext({
|
||||
portalEnv: "mooncake",
|
||||
});
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
|
||||
expect(openMongoShellBtn).toBeUndefined();
|
||||
});
|
||||
|
||||
it("Notebooks is not enabled and is unavailable - button should be hidden", () => {
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
|
||||
expect(openMongoShellBtn).toBeUndefined();
|
||||
});
|
||||
|
||||
it("Notebooks is not enabled and is available - button should be hidden", () => {
|
||||
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
|
||||
expect(openMongoShellBtn).toBeUndefined();
|
||||
});
|
||||
|
||||
it("Notebooks is enabled and is unavailable - button should be shown and enabled", () => {
|
||||
useNotebook.getState().setIsNotebookEnabled(true);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
|
||||
expect(openMongoShellBtn).toBeDefined();
|
||||
|
||||
//TODO: modify once notebooks are available
|
||||
expect(openMongoShellBtn.disabled).toBe(true);
|
||||
//expect(openMongoShellBtn.disabled).toBe(false);
|
||||
//expect(openMongoShellBtn.tooltipText).toBe("");
|
||||
});
|
||||
|
||||
it("Notebooks is enabled and is available - button should be shown and enabled", () => {
|
||||
useNotebook.getState().setIsNotebookEnabled(true);
|
||||
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
|
||||
expect(openMongoShellBtn).toBeDefined();
|
||||
|
||||
//TODO: modify once notebooks are available
|
||||
expect(openMongoShellBtn.disabled).toBe(true);
|
||||
//expect(openMongoShellBtn.disabled).toBe(false);
|
||||
//expect(openMongoShellBtn.tooltipText).toBe("");
|
||||
});
|
||||
|
||||
it("Notebooks is enabled and is available, terminal is unavailable due to ipRules - button should be hidden", () => {
|
||||
useNotebook.getState().setIsNotebookEnabled(true);
|
||||
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
|
||||
useNotebook.getState().setIsShellEnabled(false);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
|
||||
expect(openMongoShellBtn).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Open Cassandra shell button", () => {
|
||||
const openCassandraShellBtnLabel = "Open Cassandra shell";
|
||||
const selectedNodeState = useSelectedNode.getState();
|
||||
@@ -305,42 +128,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
|
||||
expect(openCassandraShellBtn).toBeUndefined();
|
||||
});
|
||||
|
||||
it("Notebooks is not enabled and is available - button should be shown and enabled", () => {
|
||||
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
|
||||
expect(openCassandraShellBtn).toBeUndefined();
|
||||
});
|
||||
|
||||
it("Notebooks is enabled and is unavailable - button should be shown and enabled", () => {
|
||||
useNotebook.getState().setIsNotebookEnabled(true);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
|
||||
|
||||
expect(openCassandraShellBtn).toBeDefined();
|
||||
|
||||
//TODO: modify once notebooks are available
|
||||
expect(openCassandraShellBtn.disabled).toBe(true);
|
||||
//expect(openCassandraShellBtn.disabled).toBe(false);
|
||||
//expect(openCassandraShellBtn.tooltipText).toBe("");
|
||||
});
|
||||
|
||||
it("Notebooks is enabled and is available - button should be shown and enabled", () => {
|
||||
useNotebook.getState().setIsNotebookEnabled(true);
|
||||
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
|
||||
expect(openCassandraShellBtn).toBeDefined();
|
||||
|
||||
//TODO: modify once notebooks are available
|
||||
expect(openCassandraShellBtn.disabled).toBe(true);
|
||||
//expect(openCassandraShellBtn.disabled).toBe(false);
|
||||
//expect(openCassandraShellBtn.tooltipText).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Open Postgres and vCore Mongo buttons", () => {
|
||||
@@ -368,62 +155,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("GitHub buttons", () => {
|
||||
const connectToGitHubBtnLabel = "Connect to GitHub";
|
||||
const manageGitHubSettingsBtnLabel = "Manage GitHub settings";
|
||||
const selectedNodeState = useSelectedNode.getState();
|
||||
|
||||
beforeAll(() => {
|
||||
mockExplorer = {} as Explorer;
|
||||
updateUserContext({
|
||||
databaseAccount: {
|
||||
properties: {
|
||||
capabilities: [{ name: "EnableTable" }],
|
||||
},
|
||||
} as DatabaseAccount,
|
||||
});
|
||||
|
||||
mockExplorer.notebookManager = new NotebookManager();
|
||||
mockExplorer.notebookManager.gitHubOAuthService = new GitHubOAuthService(undefined);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
useNotebook.getState().setIsNotebookEnabled(false);
|
||||
});
|
||||
|
||||
it("Notebooks is enabled and GitHubOAuthService is not logged in - connect to github button should be visible", () => {
|
||||
useNotebook.getState().setIsNotebookEnabled(true);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const connectToGitHubBtn = buttons.find((button) => button.commandButtonLabel === connectToGitHubBtnLabel);
|
||||
expect(connectToGitHubBtn).toBeDefined();
|
||||
});
|
||||
|
||||
it("Notebooks is enabled and GitHubOAuthService is logged in - manage github settings button should be visible", () => {
|
||||
useNotebook.getState().setIsNotebookEnabled(true);
|
||||
mockExplorer.notebookManager.gitHubOAuthService.isLoggedIn = jest.fn().mockReturnValue(true);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const manageGitHubSettingsBtn = buttons.find(
|
||||
(button) => button.commandButtonLabel === manageGitHubSettingsBtnLabel,
|
||||
);
|
||||
expect(manageGitHubSettingsBtn).toBeDefined();
|
||||
});
|
||||
|
||||
it("Notebooks is not enabled - connect to github and manage github settings buttons should be hidden", () => {
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
|
||||
const connectToGitHubBtn = buttons.find((button) => button.commandButtonLabel === connectToGitHubBtnLabel);
|
||||
expect(connectToGitHubBtn).toBeUndefined();
|
||||
|
||||
const manageGitHubSettingsBtn = buttons.find(
|
||||
(button) => button.commandButtonLabel === manageGitHubSettingsBtnLabel,
|
||||
);
|
||||
expect(manageGitHubSettingsBtn).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Resource token", () => {
|
||||
const mockCollection = { id: ko.observable("test") } as CollectionBase;
|
||||
useSelectedNode.getState().setSelectedNode(mockCollection);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
||||
import * as React from "react";
|
||||
import AddCollectionIcon from "../../../../images/AddCollection.svg";
|
||||
import AddDatabaseIcon from "../../../../images/AddDatabase.svg";
|
||||
@@ -6,13 +7,10 @@ import AddStoredProcedureIcon from "../../../../images/AddStoredProcedure.svg";
|
||||
import AddTriggerIcon from "../../../../images/AddTrigger.svg";
|
||||
import AddUdfIcon from "../../../../images/AddUdf.svg";
|
||||
import BrowseQueriesIcon from "../../../../images/BrowseQuery.svg";
|
||||
import CosmosTerminalIcon from "../../../../images/Cosmos-Terminal.svg";
|
||||
import FeedbackIcon from "../../../../images/Feedback-Command.svg";
|
||||
import HomeIcon from "../../../../images/Home_16.svg";
|
||||
import HostedTerminalIcon from "../../../../images/Hosted-Terminal.svg";
|
||||
import OpenQueryFromDiskIcon from "../../../../images/OpenQueryFromDisk.svg";
|
||||
import GitHubIcon from "../../../../images/github.svg";
|
||||
import NewNotebookIcon from "../../../../images/notebook/Notebook-new.svg";
|
||||
import ResetWorkspaceIcon from "../../../../images/notebook/Notebook-reset-workspace.svg";
|
||||
import OpenInTabIcon from "../../../../images/open-in-tab.svg";
|
||||
import SettingsIcon from "../../../../images/settings_15x15.svg";
|
||||
import SynapseIcon from "../../../../images/synapse-link.svg";
|
||||
@@ -20,7 +18,6 @@ import { AuthType } from "../../../AuthType";
|
||||
import * as Constants from "../../../Common/Constants";
|
||||
import { Platform, configContext } from "../../../ConfigContext";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { JunoClient } from "../../../Juno/JunoClient";
|
||||
import { userContext } from "../../../UserContext";
|
||||
import { getCollectionName, getDatabaseName } from "../../../Utils/APITypeUtils";
|
||||
import { isRunningOnNationalCloud } from "../../../Utils/CloudUtils";
|
||||
@@ -31,7 +28,6 @@ import { useNotebook } from "../../Notebook/useNotebook";
|
||||
import { OpenFullScreen } from "../../OpenFullScreen";
|
||||
import { AddDatabasePanel } from "../../Panes/AddDatabasePanel/AddDatabasePanel";
|
||||
import { BrowseQueriesPane } from "../../Panes/BrowseQueriesPane/BrowseQueriesPane";
|
||||
import { GitHubReposPanel } from "../../Panes/GitHubReposPanel/GitHubReposPanel";
|
||||
import { LoadQueryPane } from "../../Panes/LoadQueryPane/LoadQueryPane";
|
||||
import { SettingsPane } from "../../Panes/SettingsPane/SettingsPane";
|
||||
import { useDatabases } from "../../useDatabases";
|
||||
@@ -57,6 +53,9 @@ export function createStaticCommandBarButtons(
|
||||
};
|
||||
|
||||
if (configContext.platform !== Platform.Fabric) {
|
||||
const homeBtn = createHomeButton();
|
||||
buttons.push(homeBtn);
|
||||
|
||||
const newCollectionBtn = createNewCollectionGroup(container);
|
||||
buttons.push(newCollectionBtn);
|
||||
if (userContext.apiType !== "Tables" && userContext.apiType !== "Cassandra") {
|
||||
@@ -75,57 +74,6 @@ export function createStaticCommandBarButtons(
|
||||
}
|
||||
}
|
||||
|
||||
if (useNotebook.getState().isNotebookEnabled) {
|
||||
addDivider();
|
||||
const notebookButtons: CommandButtonComponentProps[] = [];
|
||||
|
||||
const newNotebookButton = createNewNotebookButton(container);
|
||||
newNotebookButton.children = [createNewNotebookButton(container), createuploadNotebookButton(container)];
|
||||
notebookButtons.push(newNotebookButton);
|
||||
|
||||
if (container.notebookManager?.gitHubOAuthService) {
|
||||
notebookButtons.push(createManageGitHubAccountButton(container));
|
||||
}
|
||||
if (useNotebook.getState().isPhoenixFeatures && configContext.isTerminalEnabled) {
|
||||
notebookButtons.push(createOpenTerminalButton(container));
|
||||
}
|
||||
if (useNotebook.getState().isPhoenixNotebooks && selectedNodeState.isConnectedToContainer()) {
|
||||
notebookButtons.push(createNotebookWorkspaceResetButton(container));
|
||||
}
|
||||
if (
|
||||
(userContext.apiType === "Mongo" &&
|
||||
useNotebook.getState().isShellEnabled &&
|
||||
selectedNodeState.isDatabaseNodeOrNoneSelected()) ||
|
||||
userContext.apiType === "Cassandra"
|
||||
) {
|
||||
notebookButtons.push(createDivider());
|
||||
if (userContext.apiType === "Cassandra") {
|
||||
notebookButtons.push(createOpenTerminalButtonByKind(container, ViewModels.TerminalKind.Cassandra));
|
||||
} else {
|
||||
notebookButtons.push(createOpenTerminalButtonByKind(container, ViewModels.TerminalKind.Mongo));
|
||||
}
|
||||
}
|
||||
|
||||
notebookButtons.forEach((btn) => {
|
||||
if (btn.commandButtonLabel.indexOf("Cassandra") !== -1) {
|
||||
if (!useNotebook.getState().isPhoenixFeatures) {
|
||||
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.cassandraShellTemporarilyDownMsg);
|
||||
}
|
||||
} else if (btn.commandButtonLabel.indexOf("Mongo") !== -1) {
|
||||
if (!useNotebook.getState().isPhoenixFeatures) {
|
||||
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.mongoShellTemporarilyDownMsg);
|
||||
}
|
||||
} else if (btn.commandButtonLabel.indexOf("Open Terminal") !== -1) {
|
||||
if (!useNotebook.getState().isPhoenixFeatures) {
|
||||
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.temporarilyDownMsg);
|
||||
}
|
||||
} else if (!useNotebook.getState().isPhoenixNotebooks) {
|
||||
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.temporarilyDownMsg);
|
||||
}
|
||||
buttons.push(btn);
|
||||
});
|
||||
}
|
||||
|
||||
if (!selectedNodeState.isDatabaseNodeOrNoneSelected()) {
|
||||
const isQuerySupported = userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
|
||||
|
||||
@@ -235,12 +183,12 @@ export function createControlCommandBarButtons(container: Explorer): CommandButt
|
||||
buttons.push(fullScreenButton);
|
||||
}
|
||||
|
||||
if (configContext.platform !== Platform.Emulator) {
|
||||
if (configContext.platform === Platform.Portal) {
|
||||
const label = "Feedback";
|
||||
const feedbackButtonOptions: CommandButtonComponentProps = {
|
||||
iconSrc: FeedbackIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => container.provideFeedbackEmail(),
|
||||
onCommandClick: () => container.openCESCVAFeedbackBlade(),
|
||||
commandButtonLabel: undefined,
|
||||
ariaLabel: label,
|
||||
tooltipText: label,
|
||||
@@ -285,6 +233,18 @@ function createNewCollectionGroup(container: Explorer): CommandButtonComponentPr
|
||||
};
|
||||
}
|
||||
|
||||
function createHomeButton(): CommandButtonComponentProps {
|
||||
const label = "Home";
|
||||
return {
|
||||
iconSrc: HomeIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => useTabs.getState().openAndActivateReactTab(ReactTabKind.Home),
|
||||
commandButtonLabel: label,
|
||||
hasPopup: false,
|
||||
ariaLabel: label,
|
||||
};
|
||||
}
|
||||
|
||||
function createOpenSynapseLinkDialogButton(container: Explorer): CommandButtonComponentProps {
|
||||
if (configContext.platform === Platform.Emulator) {
|
||||
return undefined;
|
||||
@@ -432,40 +392,6 @@ export function createScriptCommandButtons(selectedNodeState: SelectedNodeState)
|
||||
return buttons;
|
||||
}
|
||||
|
||||
function applyNotebooksTemporarilyDownStyle(buttonProps: CommandButtonComponentProps, tooltip: string): void {
|
||||
if (!buttonProps.isDivider) {
|
||||
buttonProps.disabled = true;
|
||||
buttonProps.tooltipText = tooltip;
|
||||
}
|
||||
}
|
||||
|
||||
function createNewNotebookButton(container: Explorer): CommandButtonComponentProps {
|
||||
const label = "New Notebook";
|
||||
return {
|
||||
id: "newNotebookBtn",
|
||||
iconSrc: NewNotebookIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => container.onNewNotebookClicked(),
|
||||
commandButtonLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(),
|
||||
ariaLabel: label,
|
||||
};
|
||||
}
|
||||
|
||||
function createuploadNotebookButton(container: Explorer): CommandButtonComponentProps {
|
||||
const label = "Upload to Notebook Server";
|
||||
return {
|
||||
iconSrc: NewNotebookIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => container.openUploadFilePanel(),
|
||||
commandButtonLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(),
|
||||
ariaLabel: label,
|
||||
};
|
||||
}
|
||||
|
||||
function createOpenQueryButton(container: Explorer): CommandButtonComponentProps {
|
||||
const label = "Open Query";
|
||||
return {
|
||||
@@ -493,19 +419,6 @@ function createOpenQueryFromDiskButton(): CommandButtonComponentProps {
|
||||
};
|
||||
}
|
||||
|
||||
function createOpenTerminalButton(container: Explorer): CommandButtonComponentProps {
|
||||
const label = "Open Terminal";
|
||||
return {
|
||||
iconSrc: CosmosTerminalIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => container.openNotebookTerminal(ViewModels.TerminalKind.Default),
|
||||
commandButtonLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(),
|
||||
ariaLabel: label,
|
||||
};
|
||||
}
|
||||
|
||||
function createOpenTerminalButtonByKind(
|
||||
container: Explorer,
|
||||
terminalKind: ViewModels.TerminalKind,
|
||||
@@ -545,45 +458,6 @@ function createOpenTerminalButtonByKind(
|
||||
};
|
||||
}
|
||||
|
||||
function createNotebookWorkspaceResetButton(container: Explorer): CommandButtonComponentProps {
|
||||
const label = "Reset Workspace";
|
||||
return {
|
||||
iconSrc: ResetWorkspaceIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => container.resetNotebookWorkspace(),
|
||||
commandButtonLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(),
|
||||
ariaLabel: label,
|
||||
};
|
||||
}
|
||||
|
||||
function createManageGitHubAccountButton(container: Explorer): CommandButtonComponentProps {
|
||||
const connectedToGitHub: boolean = container.notebookManager?.gitHubOAuthService.isLoggedIn();
|
||||
const label = connectedToGitHub ? "Manage GitHub settings" : "Connect to GitHub";
|
||||
const junoClient = new JunoClient();
|
||||
return {
|
||||
iconSrc: GitHubIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => {
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
label,
|
||||
<GitHubReposPanel
|
||||
explorer={container}
|
||||
gitHubClientProp={container.notebookManager.gitHubClient}
|
||||
junoClientProp={junoClient}
|
||||
/>,
|
||||
);
|
||||
},
|
||||
commandButtonLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(),
|
||||
ariaLabel: label,
|
||||
};
|
||||
}
|
||||
|
||||
function createStaticCommandBarButtonsForResourceToken(
|
||||
container: Explorer,
|
||||
selectedNodeState: SelectedNodeState,
|
||||
|
||||
@@ -37,7 +37,7 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol
|
||||
if (isDisabled) {
|
||||
return StyleConstants.GrayScale;
|
||||
}
|
||||
return configContext.platform == Platform.Fabric ? StyleConstants.NoColor : undefined;
|
||||
return configContext.platform == Platform.Fabric ? StyleConstants.FabricToolbarIconColor : undefined;
|
||||
};
|
||||
|
||||
return btns
|
||||
@@ -96,7 +96,12 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol
|
||||
},
|
||||
width: 16,
|
||||
},
|
||||
label: { fontSize: StyleConstants.mediumFontSize },
|
||||
label: {
|
||||
fontSize:
|
||||
configContext.platform == Platform.Fabric
|
||||
? StyleConstants.DefaultFontSize
|
||||
: StyleConstants.mediumFontSize,
|
||||
},
|
||||
rootHovered: { backgroundColor: hoverColor },
|
||||
rootPressed: { backgroundColor: hoverColor },
|
||||
splitButtonMenuButtonExpanded: {
|
||||
@@ -133,7 +138,12 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol
|
||||
// TODO Figure out how to do it the proper way with subComponentStyles.
|
||||
// TODO Remove all this crazy styling once we adopt Ui-Fabric Azure themes
|
||||
selectors: {
|
||||
".ms-ContextualMenu-itemText": { fontSize: StyleConstants.mediumFontSize },
|
||||
".ms-ContextualMenu-itemText": {
|
||||
fontSize:
|
||||
configContext.platform == Platform.Fabric
|
||||
? StyleConstants.DefaultFontSize
|
||||
: StyleConstants.mediumFontSize,
|
||||
},
|
||||
".ms-ContextualMenu-link:hover": { backgroundColor: hoverColor },
|
||||
".ms-ContextualMenu-icon": { width: 16, height: 16 },
|
||||
},
|
||||
|
||||
@@ -162,6 +162,7 @@ export class NotificationConsoleComponent extends React.Component<
|
||||
role="button"
|
||||
onKeyDown={(event: React.KeyboardEvent<HTMLSpanElement>) => this.onClearNotificationsKeyPress(event)}
|
||||
tabIndex={0}
|
||||
style={{ border: "1px solid black", borderRadius: "2px" }}
|
||||
>
|
||||
<img src={ClearIcon} alt="clear notifications image" />
|
||||
Clear Notifications
|
||||
|
||||
@@ -146,6 +146,12 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
role="button"
|
||||
style={
|
||||
Object {
|
||||
"border": "1px solid black",
|
||||
"borderRadius": "2px",
|
||||
}
|
||||
}
|
||||
tabIndex={0}
|
||||
>
|
||||
<img
|
||||
@@ -311,6 +317,12 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
role="button"
|
||||
style={
|
||||
Object {
|
||||
"border": "1px solid black",
|
||||
"borderRadius": "2px",
|
||||
}
|
||||
}
|
||||
tabIndex={0}
|
||||
>
|
||||
<img
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// TODO convert this file to an action registry in order to have actions and their handlers be more tightly coupled.
|
||||
import { useDatabases } from "Explorer/useDatabases";
|
||||
import React from "react";
|
||||
import { ActionContracts } from "../../Contracts/ExplorerContracts";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
@@ -40,12 +41,26 @@ function openCollectionTab(
|
||||
databases: ViewModels.Database[],
|
||||
initialDatabaseIndex = 0,
|
||||
) {
|
||||
//if databases are not yet loaded, wait until loaded
|
||||
if (!databases || databases.length === 0) {
|
||||
const databaseActionHandler = (databases: ViewModels.Database[]) => {
|
||||
databasesUnsubscription();
|
||||
openCollectionTab(action, databases, 0);
|
||||
return;
|
||||
};
|
||||
const databasesUnsubscription = useDatabases.subscribe(databaseActionHandler, (state) => state.databases);
|
||||
} else {
|
||||
for (let i = initialDatabaseIndex; i < databases.length; i++) {
|
||||
const database: ViewModels.Database = databases[i];
|
||||
if (!!action.databaseResourceId && database.id() !== action.databaseResourceId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
//expand database first if not expanded to load the collections
|
||||
if (!database.isDatabaseExpanded?.()) {
|
||||
database.expandDatabase?.();
|
||||
}
|
||||
|
||||
const collectionActionHandler = (collections: ViewModels.Collection[]) => {
|
||||
if (!action.collectionResourceId && collections.length === 0) {
|
||||
subscription.dispose();
|
||||
@@ -133,6 +148,7 @@ function openCollectionTab(
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function openPane(action: ActionContracts.OpenPane, explorer: Explorer) {
|
||||
if (
|
||||
|
||||
@@ -202,8 +202,8 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
styles={getTextFieldStyles()}
|
||||
pattern="[^/?#\\]*[^/?# \\]"
|
||||
title="May not end with space nor contain characters '\' '/' '#' '?'"
|
||||
pattern="[^/?#\\-]*[^/?#- \\]"
|
||||
title="May not end with space nor contain characters '\' '/' '#' '?' '-'"
|
||||
placeholder="Type a new keyspace id"
|
||||
size={40}
|
||||
value={newKeyspaceId}
|
||||
@@ -292,8 +292,8 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
|
||||
required={true}
|
||||
ariaLabel="addCollection-table Id Create table"
|
||||
autoComplete="off"
|
||||
pattern="[^/?#\\]*[^/?# \\]"
|
||||
title="May not end with space nor contain characters '\' '/' '#' '?'"
|
||||
pattern="[^/?#\\-]*[^/?#- \\]"
|
||||
title="May not end with space nor contain characters '\' '/' '#' '?' '-'"
|
||||
placeholder="Enter table Id"
|
||||
size={20}
|
||||
value={tableId}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
Icon,
|
||||
IconButton,
|
||||
Link,
|
||||
MessageBar,
|
||||
Stack,
|
||||
Text,
|
||||
TooltipHost,
|
||||
@@ -207,6 +208,7 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
|
||||
</Stack>
|
||||
{createNewContainer ? (
|
||||
<Stack>
|
||||
<MessageBar>All configurations except for unique keys will be copied from the source container</MessageBar>
|
||||
<Stack className="panelGroupSpacing">
|
||||
<Stack horizontal>
|
||||
<span className="mandatoryStar">* </span>
|
||||
|
||||
@@ -58,7 +58,7 @@ export class PanelContainerComponent extends React.Component<PanelContainerProps
|
||||
onDismiss={this.onDissmiss}
|
||||
isLightDismiss
|
||||
type={PanelType.custom}
|
||||
closeButtonAriaLabel="Close"
|
||||
closeButtonAriaLabel={`Close ${this.props.headerText}`}
|
||||
customWidth={this.props.panelWidth ? this.props.panelWidth : "440px"}
|
||||
headerClassName="panelHeader"
|
||||
onRenderNavigationContent={this.props.onRenderNavigationContent}
|
||||
|
||||
@@ -124,8 +124,8 @@ export const AddTableEntityPanel: FunctionComponent<AddTableEntityPanelProps> =
|
||||
|
||||
setIsExecuting(true);
|
||||
const entity: Entities.ITableEntity = entityFromAttributes(entities);
|
||||
const newEntity: Entities.ITableEntity = await tableDataClient.createDocument(queryTablesTab.collection, entity);
|
||||
try {
|
||||
const newEntity: Entities.ITableEntity = await tableDataClient.createDocument(queryTablesTab.collection, entity);
|
||||
await tableEntityListViewModel.addEntityToCache(newEntity);
|
||||
if (!tryInsertNewHeaders(tableEntityListViewModel, newEntity)) {
|
||||
tableEntityListViewModel.redrawTableThrottled();
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
exports[`PaneContainerComponent test should be resize if notification console is expanded 1`] = `
|
||||
<StyledPanelBase
|
||||
closeButtonAriaLabel="Close"
|
||||
closeButtonAriaLabel="Close test"
|
||||
customWidth="440px"
|
||||
headerClassName="panelHeader"
|
||||
headerText="test"
|
||||
@@ -42,7 +42,7 @@ exports[`PaneContainerComponent test should render nothing if content is undefin
|
||||
|
||||
exports[`PaneContainerComponent test should render with panel content and header 1`] = `
|
||||
<StyledPanelBase
|
||||
closeButtonAriaLabel="Close"
|
||||
closeButtonAriaLabel="Close test"
|
||||
customWidth="440px"
|
||||
headerClassName="panelHeader"
|
||||
headerText="test"
|
||||
|
||||
@@ -49,7 +49,7 @@ export const QueryCopilotFeedbackModal = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal isOpen={showFeedbackModal}>
|
||||
<Modal isOpen={showFeedbackModal} styles={{ main: { borderRadius: 8, maxWidth: 600 } }}>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Stack style={{ padding: 24 }}>
|
||||
<Stack horizontal horizontalAlign="space-between">
|
||||
@@ -68,9 +68,14 @@ export const QueryCopilotFeedbackModal = ({
|
||||
rows={3}
|
||||
/>
|
||||
<TextField
|
||||
styles={{ root: { marginBottom: 14 } }}
|
||||
styles={{
|
||||
root: { marginBottom: 14 },
|
||||
fieldGroup: { backgroundColor: "#F3F2F1", borderRadius: 4, borderColor: "#D1D1D1" },
|
||||
}}
|
||||
label="Query generated"
|
||||
defaultValue={generatedQuery}
|
||||
multiline
|
||||
rows={3}
|
||||
readOnly
|
||||
/>
|
||||
<Text style={{ fontSize: 12, marginBottom: 14 }}>
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
exports[`Query Copilot Feedback Modal snapshot test shoud render and match snapshot 1`] = `
|
||||
<Modal
|
||||
isOpen={true}
|
||||
styles={
|
||||
Object {
|
||||
"main": Object {
|
||||
"borderRadius": 8,
|
||||
"maxWidth": 600,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<form
|
||||
onSubmit={[Function]}
|
||||
@@ -67,9 +75,16 @@ exports[`Query Copilot Feedback Modal snapshot test shoud render and match snaps
|
||||
<StyledTextFieldBase
|
||||
defaultValue="test query"
|
||||
label="Query generated"
|
||||
multiline={true}
|
||||
readOnly={true}
|
||||
rows={3}
|
||||
styles={
|
||||
Object {
|
||||
"fieldGroup": Object {
|
||||
"backgroundColor": "#F3F2F1",
|
||||
"borderColor": "#D1D1D1",
|
||||
"borderRadius": 4,
|
||||
},
|
||||
"root": Object {
|
||||
"marginBottom": 14,
|
||||
},
|
||||
@@ -125,6 +140,14 @@ exports[`Query Copilot Feedback Modal snapshot test shoud render and match snaps
|
||||
exports[`Query Copilot Feedback Modal snapshot test should cancel submission 1`] = `
|
||||
<Modal
|
||||
isOpen={false}
|
||||
styles={
|
||||
Object {
|
||||
"main": Object {
|
||||
"borderRadius": 8,
|
||||
"maxWidth": 600,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<form
|
||||
onSubmit={[Function]}
|
||||
@@ -189,9 +212,16 @@ exports[`Query Copilot Feedback Modal snapshot test should cancel submission 1`]
|
||||
<StyledTextFieldBase
|
||||
defaultValue="test query"
|
||||
label="Query generated"
|
||||
multiline={true}
|
||||
readOnly={true}
|
||||
rows={3}
|
||||
styles={
|
||||
Object {
|
||||
"fieldGroup": Object {
|
||||
"backgroundColor": "#F3F2F1",
|
||||
"borderColor": "#D1D1D1",
|
||||
"borderRadius": 4,
|
||||
},
|
||||
"root": Object {
|
||||
"marginBottom": 14,
|
||||
},
|
||||
@@ -247,6 +277,14 @@ exports[`Query Copilot Feedback Modal snapshot test should cancel submission 1`]
|
||||
exports[`Query Copilot Feedback Modal snapshot test should close on cancel click 1`] = `
|
||||
<Modal
|
||||
isOpen={false}
|
||||
styles={
|
||||
Object {
|
||||
"main": Object {
|
||||
"borderRadius": 8,
|
||||
"maxWidth": 600,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<form
|
||||
onSubmit={[Function]}
|
||||
@@ -311,9 +349,16 @@ exports[`Query Copilot Feedback Modal snapshot test should close on cancel click
|
||||
<StyledTextFieldBase
|
||||
defaultValue="test query"
|
||||
label="Query generated"
|
||||
multiline={true}
|
||||
readOnly={true}
|
||||
rows={3}
|
||||
styles={
|
||||
Object {
|
||||
"fieldGroup": Object {
|
||||
"backgroundColor": "#F3F2F1",
|
||||
"borderColor": "#D1D1D1",
|
||||
"borderRadius": 4,
|
||||
},
|
||||
"root": Object {
|
||||
"marginBottom": 14,
|
||||
},
|
||||
@@ -369,6 +414,14 @@ exports[`Query Copilot Feedback Modal snapshot test should close on cancel click
|
||||
exports[`Query Copilot Feedback Modal snapshot test should get user unput 1`] = `
|
||||
<Modal
|
||||
isOpen={false}
|
||||
styles={
|
||||
Object {
|
||||
"main": Object {
|
||||
"borderRadius": 8,
|
||||
"maxWidth": 600,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<form
|
||||
onSubmit={[Function]}
|
||||
@@ -433,9 +486,16 @@ exports[`Query Copilot Feedback Modal snapshot test should get user unput 1`] =
|
||||
<StyledTextFieldBase
|
||||
defaultValue="test query"
|
||||
label="Query generated"
|
||||
multiline={true}
|
||||
readOnly={true}
|
||||
rows={3}
|
||||
styles={
|
||||
Object {
|
||||
"fieldGroup": Object {
|
||||
"backgroundColor": "#F3F2F1",
|
||||
"borderColor": "#D1D1D1",
|
||||
"borderRadius": 4,
|
||||
},
|
||||
"root": Object {
|
||||
"marginBottom": 14,
|
||||
},
|
||||
@@ -491,6 +551,14 @@ exports[`Query Copilot Feedback Modal snapshot test should get user unput 1`] =
|
||||
exports[`Query Copilot Feedback Modal snapshot test should not render dont show again button 1`] = `
|
||||
<Modal
|
||||
isOpen={false}
|
||||
styles={
|
||||
Object {
|
||||
"main": Object {
|
||||
"borderRadius": 8,
|
||||
"maxWidth": 600,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<form
|
||||
onSubmit={[Function]}
|
||||
@@ -555,9 +623,16 @@ exports[`Query Copilot Feedback Modal snapshot test should not render dont show
|
||||
<StyledTextFieldBase
|
||||
defaultValue="test query"
|
||||
label="Query generated"
|
||||
multiline={true}
|
||||
readOnly={true}
|
||||
rows={3}
|
||||
styles={
|
||||
Object {
|
||||
"fieldGroup": Object {
|
||||
"backgroundColor": "#F3F2F1",
|
||||
"borderColor": "#D1D1D1",
|
||||
"borderRadius": 4,
|
||||
},
|
||||
"root": Object {
|
||||
"marginBottom": 14,
|
||||
},
|
||||
@@ -613,6 +688,14 @@ exports[`Query Copilot Feedback Modal snapshot test should not render dont show
|
||||
exports[`Query Copilot Feedback Modal snapshot test should render dont show again button and check it 1`] = `
|
||||
<Modal
|
||||
isOpen={true}
|
||||
styles={
|
||||
Object {
|
||||
"main": Object {
|
||||
"borderRadius": 8,
|
||||
"maxWidth": 600,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<form
|
||||
onSubmit={[Function]}
|
||||
@@ -677,9 +760,16 @@ exports[`Query Copilot Feedback Modal snapshot test should render dont show agai
|
||||
<StyledTextFieldBase
|
||||
defaultValue="test query"
|
||||
label="Query generated"
|
||||
multiline={true}
|
||||
readOnly={true}
|
||||
rows={3}
|
||||
styles={
|
||||
Object {
|
||||
"fieldGroup": Object {
|
||||
"backgroundColor": "#F3F2F1",
|
||||
"borderColor": "#D1D1D1",
|
||||
"borderRadius": 4,
|
||||
},
|
||||
"root": Object {
|
||||
"marginBottom": 14,
|
||||
},
|
||||
@@ -750,6 +840,14 @@ exports[`Query Copilot Feedback Modal snapshot test should render dont show agai
|
||||
exports[`Query Copilot Feedback Modal snapshot test should submit submission 1`] = `
|
||||
<Modal
|
||||
isOpen={false}
|
||||
styles={
|
||||
Object {
|
||||
"main": Object {
|
||||
"borderRadius": 8,
|
||||
"maxWidth": 600,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<form
|
||||
onSubmit={[Function]}
|
||||
@@ -814,9 +912,16 @@ exports[`Query Copilot Feedback Modal snapshot test should submit submission 1`]
|
||||
<StyledTextFieldBase
|
||||
defaultValue="test query"
|
||||
label="Query generated"
|
||||
multiline={true}
|
||||
readOnly={true}
|
||||
rows={3}
|
||||
styles={
|
||||
Object {
|
||||
"fieldGroup": Object {
|
||||
"backgroundColor": "#F3F2F1",
|
||||
"borderColor": "#D1D1D1",
|
||||
"borderRadius": 4,
|
||||
},
|
||||
"root": Object {
|
||||
"marginBottom": 14,
|
||||
},
|
||||
|
||||
@@ -11,8 +11,8 @@ import {
|
||||
Link,
|
||||
MessageBar,
|
||||
MessageBarType,
|
||||
ProgressIndicator,
|
||||
Separator,
|
||||
Spinner,
|
||||
Stack,
|
||||
TeachingBubble,
|
||||
Text,
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
import { HttpStatusCodes } from "Common/Constants";
|
||||
import { handleError } from "Common/ErrorHandlingUtils";
|
||||
import { createUri } from "Common/UrlUtility";
|
||||
import { WelcomeModal } from "Explorer/QueryCopilot/Modal/WelcomeModal";
|
||||
import { CopyPopup } from "Explorer/QueryCopilot/Popup/CopyPopup";
|
||||
import { DeletePopup } from "Explorer/QueryCopilot/Popup/DeletePopup";
|
||||
import {
|
||||
@@ -37,7 +36,6 @@ import { userContext } from "UserContext";
|
||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||
import React, { useRef, useState } from "react";
|
||||
import HintIcon from "../../../images/Hint.svg";
|
||||
import CopilotIcon from "../../../images/QueryCopilotNewLogo.svg";
|
||||
import RecentIcon from "../../../images/Recent.svg";
|
||||
import errorIcon from "../../../images/close-black.svg";
|
||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
@@ -216,12 +214,12 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
||||
const generateSQLQueryResponse: GenerateSQLQueryResponse = await response?.json();
|
||||
if (response.ok) {
|
||||
if (generateSQLQueryResponse?.sql !== "N/A") {
|
||||
let query = `-- **Prompt:** ${userPrompt}\r\n`;
|
||||
if (generateSQLQueryResponse.explanation) {
|
||||
query += `-- **Explanation of query:** ${generateSQLQueryResponse.explanation}\r\n`;
|
||||
}
|
||||
query += generateSQLQueryResponse.sql;
|
||||
setQuery(query);
|
||||
const queryExplanation = `-- **Explanation of query:** ${
|
||||
generateSQLQueryResponse.explanation ? generateSQLQueryResponse.explanation : "N/A"
|
||||
}\r\n`;
|
||||
const currentGeneratedQuery = queryExplanation + generateSQLQueryResponse.sql;
|
||||
const lastQuery = query ? `${query}\r\n` : "";
|
||||
setQuery(`${lastQuery}${currentGeneratedQuery}`);
|
||||
setGeneratedQuery(generateSQLQueryResponse.sql);
|
||||
setGeneratedQueryComments(generateSQLQueryResponse.explanation);
|
||||
setShowFeedbackBar(true);
|
||||
@@ -272,28 +270,11 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const showTeachingBubble = (): void => {
|
||||
if (showPromptTeachingBubble && !inputEdited.current) {
|
||||
setTimeout(() => {
|
||||
if (!inputEdited.current && !isWelcomModalVisible()) {
|
||||
setCopilotTeachingBubbleVisible(true);
|
||||
inputEdited.current = true;
|
||||
}
|
||||
}, 30000);
|
||||
} else {
|
||||
toggleCopilotTeachingBubbleVisible(false);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleCopilotTeachingBubbleVisible = (visible: boolean): void => {
|
||||
setCopilotTeachingBubbleVisible(visible);
|
||||
setShowPromptTeachingBubble(visible);
|
||||
};
|
||||
|
||||
const isWelcomModalVisible = (): boolean => {
|
||||
return localStorage.getItem("hideWelcomeModal") !== "true";
|
||||
};
|
||||
|
||||
const clearFeedback = () => {
|
||||
resetButtonState();
|
||||
resetQueryResults();
|
||||
@@ -322,36 +303,30 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
showTeachingBubble();
|
||||
useTabs.getState().setIsQueryErrorThrown(false);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Stack
|
||||
className="copilot-prompt-pane"
|
||||
styles={{ root: { backgroundColor: "#FAFAFA", padding: "16px 24px 0px" } }}
|
||||
styles={{ root: { backgroundColor: "#FAFAFA", padding: "8px" } }}
|
||||
id="copilot-textfield-label"
|
||||
>
|
||||
<Stack horizontal>
|
||||
<Image src={CopilotIcon} style={{ width: 24, height: 24 }} alt="Copilot" role="none" />
|
||||
<Text style={{ marginLeft: 8, fontWeight: 600, fontSize: 16 }}>Copilot</Text>
|
||||
<IconButton
|
||||
iconProps={{ imageProps: { src: errorIcon } }}
|
||||
onClick={() => {
|
||||
toggleCopilot(false);
|
||||
clearFeedback();
|
||||
resetMessageStates();
|
||||
}}
|
||||
<Stack
|
||||
horizontal
|
||||
styles={{
|
||||
root: {
|
||||
marginLeft: "auto !important",
|
||||
width: "100%",
|
||||
borderWidth: 1,
|
||||
borderStyle: "solid",
|
||||
borderColor: "#D1D1D1",
|
||||
borderRadius: 8,
|
||||
boxShadow: "0px 4px 8px 0px #00000024",
|
||||
},
|
||||
}}
|
||||
ariaLabel="Close"
|
||||
title="Close copilot"
|
||||
/>
|
||||
</Stack>
|
||||
<Stack horizontal verticalAlign="center">
|
||||
>
|
||||
<Stack style={{ width: "100%" }}>
|
||||
<Stack horizontal verticalAlign="center" style={{ padding: "8px 8px 0px 8px" }}>
|
||||
<TextField
|
||||
id="naturalLanguageInput"
|
||||
value={userPrompt}
|
||||
@@ -367,11 +342,38 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
||||
}
|
||||
}}
|
||||
style={{ lineHeight: 30 }}
|
||||
styles={{ root: { width: "95%" }, fieldGroup: { borderRadius: 6 } }}
|
||||
styles={{
|
||||
root: { width: "100%" },
|
||||
suffix: {
|
||||
background: "none",
|
||||
padding: 0,
|
||||
},
|
||||
fieldGroup: {
|
||||
borderRadius: 4,
|
||||
borderColor: "#D1D1D1",
|
||||
"::after": {
|
||||
border: "inherit",
|
||||
borderWidth: 2,
|
||||
borderBottomColor: "#464FEB",
|
||||
borderRadius: 4,
|
||||
},
|
||||
},
|
||||
}}
|
||||
disabled={isGeneratingQuery}
|
||||
autoComplete="off"
|
||||
placeholder="Ask a question in natural language and we’ll generate the query for you."
|
||||
aria-labelledby="copilot-textfield-label"
|
||||
onRenderSuffix={() => {
|
||||
return (
|
||||
<IconButton
|
||||
iconProps={{ iconName: "Send" }}
|
||||
disabled={isGeneratingQuery || !userPrompt.trim()}
|
||||
style={{ background: "none" }}
|
||||
onClick={() => startGenerateQueryProcess()}
|
||||
aria-label="Send"
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{showPromptTeachingBubble && copilotTeachingBubbleVisible && (
|
||||
<TeachingBubble
|
||||
@@ -396,16 +398,6 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
||||
or write your own query
|
||||
</TeachingBubble>
|
||||
)}
|
||||
<IconButton
|
||||
iconProps={{ iconName: "Send" }}
|
||||
disabled={isGeneratingQuery || !userPrompt.trim()}
|
||||
style={{ marginLeft: 8 }}
|
||||
onClick={() => startGenerateQueryProcess()}
|
||||
aria-label="Send"
|
||||
/>
|
||||
<div role="alert" aria-label={getAriaLabel()}>
|
||||
{isGeneratingQuery && <Spinner style={{ marginLeft: 8 }} />}
|
||||
</div>
|
||||
{showSamplePrompts && (
|
||||
<Callout
|
||||
styles={{ root: { minWidth: 400, maxWidth: "70vw" } }}
|
||||
@@ -507,11 +499,12 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
||||
</Callout>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
<Stack style={{ margin: "8px 0" }}>
|
||||
{!isGeneratingQuery && (
|
||||
<Stack style={{ padding: 8 }}>
|
||||
{!showFeedbackBar && (
|
||||
<Text style={{ fontSize: 12 }}>
|
||||
AI-generated content can have mistakes. Make sure it's accurate and appropriate before using it.{" "}
|
||||
<Link href="https://aka.ms/cdb-copilot-preview-terms" target="_blank" style={{ color: "#0072c9" }}>
|
||||
<Link href="https://aka.ms/cdb-copilot-preview-terms" target="_blank" style={{ color: "#0072D4" }}>
|
||||
Read preview terms
|
||||
</Link>
|
||||
{showErrorMessageBar && (
|
||||
@@ -524,8 +517,8 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
||||
messageBarType={MessageBarType.info}
|
||||
styles={{ root: { backgroundColor: "#F0F6FF" }, icon: { color: "#015CDA" } }}
|
||||
>
|
||||
We were unable to generate a query based upon the prompt provided. Please modify the prompt and try again.
|
||||
For examples of how to write a good prompt, please read
|
||||
We were unable to generate a query based upon the prompt provided. Please modify the prompt and
|
||||
try again. For examples of how to write a good prompt, please read
|
||||
<Link href="https://aka.ms/cdb-copilot-writing" target="_blank">
|
||||
this article.
|
||||
</Link>{" "}
|
||||
@@ -536,15 +529,27 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
||||
</MessageBar>
|
||||
)}
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
)}
|
||||
{showFeedbackBar && (
|
||||
<Stack style={{ backgroundColor: "#FFF8F0", padding: "2px 8px" }} horizontal verticalAlign="center">
|
||||
<Text style={{ fontWeight: 600, fontSize: 12 }}>Provide feedback on the query generated</Text>
|
||||
<Stack horizontal verticalAlign="center" style={{ maxHeight: 20 }}>
|
||||
{userContext.feedbackPolicies?.policyAllowFeedback && (
|
||||
<Stack horizontal verticalAlign="center">
|
||||
<Text style={{ fontSize: 12 }}>Provide feedback</Text>
|
||||
{showCallout && !hideFeedbackModalForLikedQueries && (
|
||||
<Callout
|
||||
role="status"
|
||||
style={{ padding: 8 }}
|
||||
style={{ padding: "6px 12px" }}
|
||||
styles={{
|
||||
root: {
|
||||
borderRadius: 8,
|
||||
},
|
||||
beakCurtain: {
|
||||
borderRadius: 8,
|
||||
},
|
||||
calloutMain: {
|
||||
borderRadius: 8,
|
||||
},
|
||||
}}
|
||||
target="#likeBtn"
|
||||
onDismiss={() => {
|
||||
setShowCallout(false);
|
||||
@@ -578,7 +583,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
||||
)}
|
||||
<IconButton
|
||||
id="likeBtn"
|
||||
style={{ marginLeft: 20 }}
|
||||
style={{ marginLeft: 10 }}
|
||||
aria-label="Like"
|
||||
role="toggle"
|
||||
iconProps={{ iconName: likeQuery === true ? "LikeSolid" : "Like" }}
|
||||
@@ -597,7 +602,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
style={{ margin: "0 10px" }}
|
||||
style={{ margin: "0 4px" }}
|
||||
role="toggle"
|
||||
aria-label="Dislike"
|
||||
iconProps={{ iconName: dislikeQuery === true ? "DislikeSolid" : "Dislike" }}
|
||||
@@ -613,29 +618,90 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
||||
document.getElementById("likeStatus").innerHTML = toggleStatusValue;
|
||||
}}
|
||||
/>
|
||||
|
||||
<span role="status" style={{ position: "absolute", left: "-9999px" }} id="likeStatus"></span>
|
||||
|
||||
<Separator vertical style={{ color: "#EDEBE9" }} />
|
||||
<Separator
|
||||
vertical
|
||||
styles={{
|
||||
root: {
|
||||
"::after": {
|
||||
backgroundColor: "#767676",
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
<CommandBarButton
|
||||
className="copyQuery"
|
||||
onClick={copyGeneratedCode}
|
||||
iconProps={{ iconName: "Copy" }}
|
||||
style={{ margin: "0 10px", backgroundColor: "#FFF8F0", transition: "background-color 0.3s ease" }}
|
||||
style={{ fontSize: 12, transition: "background-color 0.3s ease", height: "100%" }}
|
||||
styles={{
|
||||
root: {
|
||||
backgroundColor: "inherit",
|
||||
},
|
||||
}}
|
||||
>
|
||||
Copy query
|
||||
Copy code
|
||||
</CommandBarButton>
|
||||
<CommandBarButton
|
||||
className="deleteQuery"
|
||||
onClick={() => {
|
||||
setShowDeletePopup(true);
|
||||
}}
|
||||
iconProps={{ iconName: "Delete" }}
|
||||
style={{ margin: "0 10px", backgroundColor: "#FFF8F0", transition: "background-color 0.3s ease" }}
|
||||
style={{ fontSize: 12, transition: "background-color 0.3s ease", height: "100%" }}
|
||||
styles={{
|
||||
root: {
|
||||
backgroundColor: "inherit",
|
||||
},
|
||||
}}
|
||||
>
|
||||
Delete query
|
||||
Clear editor
|
||||
</CommandBarButton>
|
||||
</Stack>
|
||||
)}
|
||||
<WelcomeModal visible={isWelcomModalVisible()} />
|
||||
</Stack>
|
||||
)}
|
||||
{isGeneratingQuery && (
|
||||
<ProgressIndicator
|
||||
label="Thinking..."
|
||||
ariaLabel={getAriaLabel()}
|
||||
barHeight={4}
|
||||
styles={{
|
||||
root: {
|
||||
fontSize: 12,
|
||||
width: "100%",
|
||||
bottom: 0,
|
||||
},
|
||||
itemName: {
|
||||
padding: "0px 8px",
|
||||
},
|
||||
itemProgress: {
|
||||
borderBottomLeftRadius: 8,
|
||||
borderBottomRightRadius: 8,
|
||||
padding: 0,
|
||||
},
|
||||
progressBar: {
|
||||
backgroundImage:
|
||||
"linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, rgb(24, 90, 189) 35%, rgb(71, 207, 250) 70%, rgb(180, 124, 248) 92%, rgba(0, 0, 0, 0))",
|
||||
animationDuration: "5s",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
<IconButton
|
||||
iconProps={{ imageProps: { src: errorIcon } }}
|
||||
onClick={() => {
|
||||
toggleCopilot(false);
|
||||
clearFeedback();
|
||||
resetMessageStates();
|
||||
}}
|
||||
ariaLabel="Close"
|
||||
title="Close copilot"
|
||||
/>
|
||||
</Stack>
|
||||
{isSamplePromptsOpen && <SamplePrompts sampleProps={sampleProps} />}
|
||||
{query !== "" && query.trim().length !== 0 && (
|
||||
<DeletePopup
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { FeedOptions } from "@azure/cosmos";
|
||||
import {
|
||||
Areas,
|
||||
BackendApi,
|
||||
ConnectionStatusType,
|
||||
ContainerStatusType,
|
||||
HttpStatusCodes,
|
||||
@@ -30,6 +31,7 @@ import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||
import { traceFailure, traceStart, traceSuccess } from "Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "UserContext";
|
||||
import { getAuthorizationHeader } from "Utils/AuthorizationUtils";
|
||||
import { useNewPortalBackendEndpoint } from "Utils/EndpointUtils";
|
||||
import { queryPagesUntilContentPresent } from "Utils/QueryUtils";
|
||||
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
|
||||
import { useTabs } from "hooks/useTabs";
|
||||
@@ -80,7 +82,11 @@ export const isCopilotFeatureRegistered = async (subscriptionId: string): Promis
|
||||
};
|
||||
|
||||
export const getCopilotEnabled = async (): Promise<boolean> => {
|
||||
const url = `${configContext.BACKEND_ENDPOINT}/api/portalsettings/querycopilot`;
|
||||
const backendEndpoint: string = useNewPortalBackendEndpoint(BackendApi.PortalSettings)
|
||||
? configContext.PORTAL_BACKEND_ENDPOINT
|
||||
: configContext.BACKEND_ENDPOINT;
|
||||
|
||||
const url = `${backendEndpoint}/api/portalsettings/querycopilot`;
|
||||
const authorizationHeader: AuthorizationTokenHeaderMetadata = getAuthorizationHeader();
|
||||
const headers = { [authorizationHeader.header]: authorizationHeader.token };
|
||||
|
||||
|
||||
@@ -42,6 +42,11 @@ function bindDataTable(element: any, valueAccessor: any, allBindings: any, viewM
|
||||
|
||||
createDataTable(0, tableEntityListViewModel, queryTablesTab); // Fake a DataTable to start.
|
||||
$(window).resize(updateTableScrollableRegionMetrics);
|
||||
operationManager.focusTable(); // Also selects the first row if needed.
|
||||
// Attach the arrow key event handler to the table element
|
||||
$dataTable.on("keydown", (event: JQueryEventObject) => {
|
||||
handleArrowKey(element, valueAccessor, allBindings, viewModel, bindingContext, event);
|
||||
});
|
||||
}
|
||||
|
||||
function onTableColumnChange(enablePrompt: boolean = true, queryTablesTab: QueryTablesTab) {
|
||||
@@ -210,6 +215,39 @@ function selectionChanged(element: any, valueAccessor: any, allBindings: any, vi
|
||||
});
|
||||
//selected = bindingContext.$data.selected();
|
||||
}
|
||||
function handleArrowKey(
|
||||
element: any,
|
||||
valueAccessor: any,
|
||||
allBindings: any,
|
||||
viewModel: any,
|
||||
bindingContext: any,
|
||||
event: JQueryEventObject,
|
||||
) {
|
||||
const isUpArrowKey: boolean = event.keyCode === Constants.keyCodes.UpArrow;
|
||||
const isDownArrowKey: boolean = event.keyCode === Constants.keyCodes.DownArrow;
|
||||
|
||||
if (isUpArrowKey || isDownArrowKey) {
|
||||
const $dataTable = $(element);
|
||||
let $selectedRow = $dataTable.find("tr.selected");
|
||||
|
||||
if ($selectedRow.length === 0) {
|
||||
// No row is currently selected, select the first row
|
||||
$selectedRow = $dataTable.find("tr:first");
|
||||
$selectedRow.addClass("selected");
|
||||
} else {
|
||||
const $targetRow = isUpArrowKey ? $selectedRow.prev("tr") : $selectedRow.next("tr");
|
||||
|
||||
if ($targetRow.length > 0) {
|
||||
// Remove the selected class from the current row and add it to the target row
|
||||
$selectedRow.removeClass("selected").attr("tabindex", "-1");
|
||||
$targetRow.addClass("selected").attr("tabindex", "0");
|
||||
$targetRow.focus();
|
||||
}
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
function dataChanged(element: any, valueAccessor: any, allBindings: any, viewModel: any, bindingContext: any) {
|
||||
// do nothing for now
|
||||
|
||||
@@ -3,6 +3,7 @@ import * as ko from "knockout";
|
||||
import Q from "q";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import * as Constants from "../../Common/Constants";
|
||||
import { CassandraProxyAPIs, CassandraProxyEndpoints } from "../../Common/Constants";
|
||||
import { handleError } from "../../Common/ErrorHandlingUtils";
|
||||
import * as HeadersUtility from "../../Common/HeadersUtility";
|
||||
import { createDocument } from "../../Common/dataAccess/createDocument";
|
||||
@@ -171,8 +172,9 @@ export class CassandraAPIDataClient extends TableDataClient {
|
||||
deferred.resolve(entity);
|
||||
},
|
||||
(error) => {
|
||||
handleError(error, "AddRowCassandra", `Error while adding new row to table ${collection.id()}`);
|
||||
deferred.reject(error);
|
||||
const errorText = error.responseJSON?.message ?? JSON.stringify(error);
|
||||
handleError(errorText, "AddRowCassandra", `Error while adding new row to table ${collection.id()}`);
|
||||
deferred.reject(errorText);
|
||||
},
|
||||
)
|
||||
.finally(clearInProgressMessage);
|
||||
@@ -261,6 +263,57 @@ export class CassandraAPIDataClient extends TableDataClient {
|
||||
query: string,
|
||||
shouldNotify?: boolean,
|
||||
paginationToken?: string,
|
||||
): Promise<Entities.IListTableEntitiesResult> {
|
||||
if (!this.useCassandraProxyEndpoint("postQuery")) {
|
||||
return this.queryDocuments_ToBeDeprecated(collection, query, shouldNotify, paginationToken);
|
||||
}
|
||||
const clearMessage =
|
||||
shouldNotify && NotificationConsoleUtils.logConsoleProgress(`Querying rows for table ${collection.id()}`);
|
||||
try {
|
||||
const { authType, databaseAccount } = userContext;
|
||||
|
||||
const apiEndpoint: string =
|
||||
authType === AuthType.EncryptedToken
|
||||
? CassandraProxyAPIs.connectionStringQueryApi
|
||||
: CassandraProxyAPIs.queryApi;
|
||||
|
||||
const data: any = await $.ajax(`${configContext.CASSANDRA_PROXY_ENDPOINT}/${apiEndpoint}`, {
|
||||
type: "POST",
|
||||
contentType: Constants.ContentType.applicationJson,
|
||||
data: JSON.stringify({
|
||||
accountName: databaseAccount?.name,
|
||||
cassandraEndpoint: this.trimCassandraEndpoint(databaseAccount?.properties.cassandraEndpoint),
|
||||
resourceId: databaseAccount?.id,
|
||||
keyspaceId: collection.databaseId,
|
||||
tableId: collection.id(),
|
||||
query,
|
||||
paginationToken,
|
||||
}),
|
||||
beforeSend: this.setAuthorizationHeader as any,
|
||||
cache: false,
|
||||
});
|
||||
shouldNotify &&
|
||||
NotificationConsoleUtils.logConsoleInfo(
|
||||
`Successfully fetched ${data.result.length} rows for table ${collection.id()}`,
|
||||
);
|
||||
return {
|
||||
Results: data.result,
|
||||
ContinuationToken: data.paginationToken,
|
||||
};
|
||||
} catch (error) {
|
||||
shouldNotify &&
|
||||
handleError(error, "QueryDocumentsCassandra", `Failed to query rows for table ${collection.id()}`);
|
||||
throw error;
|
||||
} finally {
|
||||
clearMessage?.();
|
||||
}
|
||||
}
|
||||
|
||||
public async queryDocuments_ToBeDeprecated(
|
||||
collection: ViewModels.Collection,
|
||||
query: string,
|
||||
shouldNotify?: boolean,
|
||||
paginationToken?: string,
|
||||
): Promise<Entities.IListTableEntitiesResult> {
|
||||
const clearMessage =
|
||||
shouldNotify && NotificationConsoleUtils.logConsoleProgress(`Querying rows for table ${collection.id()}`);
|
||||
@@ -294,7 +347,11 @@ export class CassandraAPIDataClient extends TableDataClient {
|
||||
};
|
||||
} catch (error) {
|
||||
shouldNotify &&
|
||||
handleError(error, "QueryDocumentsCassandra", `Failed to query rows for table ${collection.id()}`);
|
||||
handleError(
|
||||
error,
|
||||
"QueryDocuments_ToBeDeprecated_Cassandra",
|
||||
`Failed to query rows for table ${collection.id()}`,
|
||||
);
|
||||
throw error;
|
||||
} finally {
|
||||
clearMessage?.();
|
||||
@@ -350,12 +407,13 @@ export class CassandraAPIDataClient extends TableDataClient {
|
||||
deferred.resolve();
|
||||
},
|
||||
(error) => {
|
||||
const errorText = error.responseJSON?.message ?? JSON.stringify(error);
|
||||
handleError(
|
||||
error,
|
||||
errorText,
|
||||
"CreateKeyspaceCassandra",
|
||||
`Error while creating a keyspace with query ${createKeyspaceQuery}`,
|
||||
);
|
||||
deferred.reject(error);
|
||||
deferred.reject(errorText);
|
||||
},
|
||||
)
|
||||
.finally(clearInProgressMessage);
|
||||
@@ -388,8 +446,13 @@ export class CassandraAPIDataClient extends TableDataClient {
|
||||
deferred.resolve();
|
||||
},
|
||||
(error) => {
|
||||
handleError(error, "CreateTableCassandra", `Error while creating a table with query ${createTableQuery}`);
|
||||
deferred.reject(error);
|
||||
const errorText = error.responseJSON?.message ?? JSON.stringify(error);
|
||||
handleError(
|
||||
errorText,
|
||||
"CreateTableCassandra",
|
||||
`Error while creating a table with query ${createTableQuery}`,
|
||||
);
|
||||
deferred.reject(errorText);
|
||||
},
|
||||
)
|
||||
.finally(clearInProgressMessage);
|
||||
@@ -402,6 +465,51 @@ export class CassandraAPIDataClient extends TableDataClient {
|
||||
}
|
||||
|
||||
public getTableKeys(collection: ViewModels.Collection): Q.Promise<CassandraTableKeys> {
|
||||
if (!this.useCassandraProxyEndpoint("getKeys")) {
|
||||
return this.getTableKeys_ToBeDeprecated(collection);
|
||||
}
|
||||
|
||||
if (!!collection.cassandraKeys) {
|
||||
return Q.resolve(collection.cassandraKeys);
|
||||
}
|
||||
const clearInProgressMessage = logConsoleProgress(`Fetching keys for table ${collection.id()}`);
|
||||
const { authType, databaseAccount } = userContext;
|
||||
const apiEndpoint: string =
|
||||
authType === AuthType.EncryptedToken ? CassandraProxyAPIs.connectionStringKeysApi : CassandraProxyAPIs.keysApi;
|
||||
|
||||
let endpoint = `${configContext.CASSANDRA_PROXY_ENDPOINT}/${apiEndpoint}`;
|
||||
const deferred = Q.defer<CassandraTableKeys>();
|
||||
|
||||
$.ajax(endpoint, {
|
||||
type: "POST",
|
||||
contentType: Constants.ContentType.applicationJson,
|
||||
data: JSON.stringify({
|
||||
accountName: databaseAccount?.name,
|
||||
cassandraEndpoint: this.trimCassandraEndpoint(databaseAccount?.properties.cassandraEndpoint),
|
||||
resourceId: databaseAccount?.id,
|
||||
keyspaceId: collection.databaseId,
|
||||
tableId: collection.id(),
|
||||
}),
|
||||
beforeSend: this.setAuthorizationHeader as any,
|
||||
cache: false,
|
||||
})
|
||||
.then(
|
||||
(data: CassandraTableKeys) => {
|
||||
collection.cassandraKeys = data;
|
||||
logConsoleInfo(`Successfully fetched keys for table ${collection.id()}`);
|
||||
deferred.resolve(data);
|
||||
},
|
||||
(error: any) => {
|
||||
const errorText = error.responseJSON?.message ?? JSON.stringify(error);
|
||||
handleError(errorText, "FetchKeysCassandra", `Error fetching keys for table ${collection.id()}`);
|
||||
deferred.reject(errorText);
|
||||
},
|
||||
)
|
||||
.done(clearInProgressMessage);
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
public getTableKeys_ToBeDeprecated(collection: ViewModels.Collection): Q.Promise<CassandraTableKeys> {
|
||||
if (!!collection.cassandraKeys) {
|
||||
return Q.resolve(collection.cassandraKeys);
|
||||
}
|
||||
@@ -433,8 +541,9 @@ export class CassandraAPIDataClient extends TableDataClient {
|
||||
deferred.resolve(data);
|
||||
},
|
||||
(error: any) => {
|
||||
handleError(error, "FetchKeysCassandra", `Error fetching keys for table ${collection.id()}`);
|
||||
deferred.reject(error);
|
||||
const errorText = error.responseJSON?.message ?? JSON.stringify(error);
|
||||
handleError(errorText, "FetchKeysCassandra", `Error fetching keys for table ${collection.id()}`);
|
||||
deferred.reject(errorText);
|
||||
},
|
||||
)
|
||||
.done(clearInProgressMessage);
|
||||
@@ -442,6 +551,52 @@ export class CassandraAPIDataClient extends TableDataClient {
|
||||
}
|
||||
|
||||
public getTableSchema(collection: ViewModels.Collection): Q.Promise<CassandraTableKey[]> {
|
||||
if (!this.useCassandraProxyEndpoint("getSchema")) {
|
||||
return this.getTableSchema_ToBeDeprecated(collection);
|
||||
}
|
||||
|
||||
if (!!collection.cassandraSchema) {
|
||||
return Q.resolve(collection.cassandraSchema);
|
||||
}
|
||||
const clearInProgressMessage = logConsoleProgress(`Fetching schema for table ${collection.id()}`);
|
||||
const { databaseAccount, authType } = userContext;
|
||||
const apiEndpoint: string =
|
||||
authType === AuthType.EncryptedToken
|
||||
? CassandraProxyAPIs.connectionStringSchemaApi
|
||||
: CassandraProxyAPIs.schemaApi;
|
||||
let endpoint = `${configContext.CASSANDRA_PROXY_ENDPOINT}/${apiEndpoint}`;
|
||||
const deferred = Q.defer<CassandraTableKey[]>();
|
||||
|
||||
$.ajax(endpoint, {
|
||||
type: "POST",
|
||||
contentType: Constants.ContentType.applicationJson,
|
||||
data: JSON.stringify({
|
||||
accountName: databaseAccount?.name,
|
||||
cassandraEndpoint: this.trimCassandraEndpoint(databaseAccount?.properties.cassandraEndpoint),
|
||||
resourceId: databaseAccount?.id,
|
||||
keyspaceId: collection.databaseId,
|
||||
tableId: collection.id(),
|
||||
}),
|
||||
beforeSend: this.setAuthorizationHeader as any,
|
||||
cache: false,
|
||||
})
|
||||
.then(
|
||||
(data: any) => {
|
||||
collection.cassandraSchema = data.columns;
|
||||
logConsoleInfo(`Successfully fetched schema for table ${collection.id()}`);
|
||||
deferred.resolve(data.columns);
|
||||
},
|
||||
(error: any) => {
|
||||
const errorText = error.responseJSON?.message ?? JSON.stringify(error);
|
||||
handleError(errorText, "FetchSchemaCassandra", `Error fetching schema for table ${collection.id()}`);
|
||||
deferred.reject(errorText);
|
||||
},
|
||||
)
|
||||
.done(clearInProgressMessage);
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
public getTableSchema_ToBeDeprecated(collection: ViewModels.Collection): Q.Promise<CassandraTableKey[]> {
|
||||
if (!!collection.cassandraSchema) {
|
||||
return Q.resolve(collection.cassandraSchema);
|
||||
}
|
||||
@@ -473,8 +628,9 @@ export class CassandraAPIDataClient extends TableDataClient {
|
||||
deferred.resolve(data.columns);
|
||||
},
|
||||
(error: any) => {
|
||||
handleError(error, "FetchSchemaCassandra", `Error fetching schema for table ${collection.id()}`);
|
||||
deferred.reject(error);
|
||||
const errorText = error.responseJSON?.message ?? JSON.stringify(error);
|
||||
handleError(errorText, "FetchSchemaCassandra", `Error fetching schema for table ${collection.id()}`);
|
||||
deferred.reject(errorText);
|
||||
},
|
||||
)
|
||||
.done(clearInProgressMessage);
|
||||
@@ -482,6 +638,44 @@ export class CassandraAPIDataClient extends TableDataClient {
|
||||
}
|
||||
|
||||
private createOrDeleteQuery(cassandraEndpoint: string, resourceId: string, query: string): Q.Promise<any> {
|
||||
if (!this.useCassandraProxyEndpoint("createOrDelete")) {
|
||||
return this.createOrDeleteQuery_ToBeDeprecated(cassandraEndpoint, resourceId, query);
|
||||
}
|
||||
|
||||
const deferred = Q.defer();
|
||||
const { authType, databaseAccount } = userContext;
|
||||
const apiEndpoint: string =
|
||||
authType === AuthType.EncryptedToken
|
||||
? CassandraProxyAPIs.connectionStringCreateOrDeleteApi
|
||||
: CassandraProxyAPIs.createOrDeleteApi;
|
||||
|
||||
$.ajax(`${configContext.CASSANDRA_PROXY_ENDPOINT}/${apiEndpoint}`, {
|
||||
type: "POST",
|
||||
contentType: Constants.ContentType.applicationJson,
|
||||
data: JSON.stringify({
|
||||
accountName: databaseAccount?.name,
|
||||
cassandraEndpoint: this.trimCassandraEndpoint(cassandraEndpoint),
|
||||
resourceId: resourceId,
|
||||
query: query,
|
||||
}),
|
||||
beforeSend: this.setAuthorizationHeader as any,
|
||||
cache: false,
|
||||
}).then(
|
||||
(data: any) => {
|
||||
deferred.resolve();
|
||||
},
|
||||
(reason) => {
|
||||
deferred.reject(reason);
|
||||
},
|
||||
);
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
private createOrDeleteQuery_ToBeDeprecated(
|
||||
cassandraEndpoint: string,
|
||||
resourceId: string,
|
||||
query: string,
|
||||
): Q.Promise<any> {
|
||||
const deferred = Q.defer();
|
||||
const { authType, databaseAccount } = userContext;
|
||||
const apiEndpoint: string =
|
||||
@@ -547,4 +741,25 @@ export class CassandraAPIDataClient extends TableDataClient {
|
||||
private getCassandraPartitionKeyProperty(collection: ViewModels.Collection): string {
|
||||
return collection.cassandraKeys.partitionKeys[0].property;
|
||||
}
|
||||
|
||||
private useCassandraProxyEndpoint(api: string): boolean {
|
||||
const activeCassandraProxyEndpoints: string[] = [
|
||||
CassandraProxyEndpoints.Development,
|
||||
CassandraProxyEndpoints.Mpac,
|
||||
CassandraProxyEndpoints.Prod,
|
||||
];
|
||||
let canAccessCassandraProxy: boolean = userContext.databaseAccount.properties.publicNetworkAccess === "Enabled";
|
||||
if (
|
||||
configContext.CASSANDRA_PROXY_ENDPOINT !== CassandraProxyEndpoints.Development &&
|
||||
userContext.databaseAccount.properties.ipRules?.length > 0
|
||||
) {
|
||||
canAccessCassandraProxy = canAccessCassandraProxy && configContext.CASSANDRA_PROXY_OUTBOUND_IPS_ALLOWLISTED;
|
||||
}
|
||||
|
||||
return (
|
||||
canAccessCassandraProxy &&
|
||||
configContext.NEW_CASSANDRA_APIS?.includes(api) &&
|
||||
activeCassandraProxyEndpoints.includes(configContext.CASSANDRA_PROXY_ENDPOINT)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { sendMessage } from "Common/MessageHandler";
|
||||
import { MessageTypes } from "Contracts/MessageTypes";
|
||||
import { CopilotProvider } from "Explorer/QueryCopilot/QueryCopilotContext";
|
||||
import { userContext } from "UserContext";
|
||||
import React from "react";
|
||||
@@ -54,6 +56,11 @@ export class NewQueryTab extends TabsBase {
|
||||
);
|
||||
}
|
||||
|
||||
public onActivate(): void {
|
||||
this.propagateTabInformation(MessageTypes.ActivateTab);
|
||||
super.onActivate();
|
||||
}
|
||||
|
||||
public onTabClick(): void {
|
||||
useTabs.getState().activateTab(this);
|
||||
this.iTabAccessor.onTabClickEvent();
|
||||
@@ -61,6 +68,7 @@ export class NewQueryTab extends TabsBase {
|
||||
|
||||
public onCloseTabButtonClick(): void {
|
||||
useTabs.getState().closeTab(this);
|
||||
this.propagateTabInformation(MessageTypes.CloseTab);
|
||||
if (this.iTabAccessor) {
|
||||
this.iTabAccessor.onCloseClickEvent(true);
|
||||
}
|
||||
@@ -69,4 +77,15 @@ export class NewQueryTab extends TabsBase {
|
||||
public getContainer(): Explorer {
|
||||
return this.props.container;
|
||||
}
|
||||
|
||||
private propagateTabInformation(type: MessageTypes): void {
|
||||
sendMessage({
|
||||
type,
|
||||
data: {
|
||||
kind: this.tabKind,
|
||||
databaseId: this.collection?.databaseId,
|
||||
collectionId: this.collection?.id?.(),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||
|
||||
this.state = {
|
||||
toggleState: ToggleState.Result,
|
||||
sqlQueryEditorContent: props.queryText || "SELECT * FROM c",
|
||||
sqlQueryEditorContent: props.isPreferredApiMongoDB ? "{}" : props.queryText || "SELECT * FROM c",
|
||||
selectedContent: "",
|
||||
queryResults: undefined,
|
||||
error: "",
|
||||
@@ -496,13 +496,16 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||
};
|
||||
|
||||
public onChangeContent(newContent: string): void {
|
||||
// The copilot store's active query takes precedence over the local state,
|
||||
// and we can't update both states in a single operation.
|
||||
// So, we update the copilot store's state first, then update the local state.
|
||||
if (this.state.copilotActive) {
|
||||
this.props.copilotStore?.setQuery(newContent);
|
||||
}
|
||||
this.setState({
|
||||
sqlQueryEditorContent: newContent,
|
||||
queryCopilotGeneratedQuery: "",
|
||||
});
|
||||
if (this.state.copilotActive) {
|
||||
this.props.copilotStore?.setQuery(newContent);
|
||||
}
|
||||
if (this.isPreferredApiMongoDB) {
|
||||
if (newContent.length > 0) {
|
||||
this.executeQueryButton = {
|
||||
@@ -544,7 +547,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
||||
}
|
||||
|
||||
public setEditorContent(): string {
|
||||
public getEditorContent(): string {
|
||||
if (this.isCopilotTabActive && this.state.queryCopilotGeneratedQuery) {
|
||||
return this.state.queryCopilotGeneratedQuery;
|
||||
}
|
||||
@@ -601,7 +604,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||
<div className="queryEditor" style={{ height: "100%" }}>
|
||||
<EditorReact
|
||||
language={"sql"}
|
||||
content={this.setEditorContent()}
|
||||
content={this.getEditorContent()}
|
||||
isReadOnly={false}
|
||||
wordWrap={"on"}
|
||||
ariaLabel={"Editing Query"}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Resource, StoredProcedureDefinition } from "@azure/cosmos";
|
||||
import { Pivot, PivotItem } from "@fluentui/react";
|
||||
import React from "react";
|
||||
import DiscardIcon from "../../../../images/discard.svg";
|
||||
import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg";
|
||||
import DiscardIcon from "../../../../images/discard.svg";
|
||||
import SaveIcon from "../../../../images/save-cosmos.svg";
|
||||
import { NormalizedEventKey } from "../../../Common/Constants";
|
||||
import { createStoredProcedure } from "../../../Common/dataAccess/createStoredProcedure";
|
||||
@@ -512,7 +512,12 @@ export default class StoredProcedureTabComponent extends React.Component<
|
||||
return (
|
||||
<div className="tab-pane flexContainer stored-procedure-tab" role="tabpanel">
|
||||
<div className="storedTabForm flexContainer">
|
||||
<div className="formTitleFirst">Stored Procedure Id</div>
|
||||
<div className="formTitleFirst">
|
||||
Stored Procedure Id
|
||||
<span className="mandatoryStar" style={{ color: "#ff0707", fontSize: "14px", fontWeight: "bold" }}>
|
||||
*
|
||||
</span>
|
||||
</div>
|
||||
<span className="formTitleTextbox">
|
||||
<input
|
||||
className="formTree"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Link, MessageBar, MessageBarButton, MessageBarType } from "@fluentui/react";
|
||||
import { CassandraProxyEndpoints, MongoProxyEndpoints } from "Common/Constants";
|
||||
import { sendMessage } from "Common/MessageHandler";
|
||||
import { configContext, updateConfigContext } from "ConfigContext";
|
||||
import { Platform, configContext, updateConfigContext } from "ConfigContext";
|
||||
import { IpRule } from "Contracts/DataModels";
|
||||
import { MessageTypes } from "Contracts/ExplorerContracts";
|
||||
import { CollectionTabKind } from "Contracts/ViewModels";
|
||||
@@ -35,7 +35,7 @@ interface TabsProps {
|
||||
export const Tabs = ({ explorer }: TabsProps): JSX.Element => {
|
||||
const { openedTabs, openedReactTabs, activeTab, activeReactTab, networkSettingsWarning } = useTabs();
|
||||
const [showRUThresholdMessageBar, setShowRUThresholdMessageBar] = useState<boolean>(
|
||||
userContext.apiType === "SQL" && !hasRUThresholdBeenConfigured(),
|
||||
userContext.apiType === "SQL" && configContext.platform !== Platform.Fabric && !hasRUThresholdBeenConfigured(),
|
||||
);
|
||||
const [
|
||||
showMongoAndCassandraProxiesNetworkSettingsWarningState,
|
||||
@@ -182,11 +182,9 @@ function TabNav({ tab, active, tabKind }: { tab?: Tab; active: boolean; tabKind?
|
||||
)}
|
||||
</span>
|
||||
<span className="tabNavText">{useObservable(tab?.tabTitle || getReactTabTitle())}</span>
|
||||
{tabKind !== ReactTabKind.Home && (
|
||||
<span className="tabIconSection">
|
||||
<CloseButton tab={tab} active={active} hovering={hovering} tabKind={tabKind} />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</span>
|
||||
</li>
|
||||
@@ -326,13 +324,17 @@ const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): J
|
||||
|
||||
const showMongoAndCassandraProxiesNetworkSettingsWarning = (): boolean => {
|
||||
const ipRules: IpRule[] = userContext.databaseAccount?.properties?.ipRules;
|
||||
if ((userContext.apiType === "Mongo" || userContext.apiType === "Cassandra") && ipRules?.length) {
|
||||
if (
|
||||
((userContext.apiType === "Mongo" && configContext.MONGO_PROXY_ENDPOINT !== MongoProxyEndpoints.Development) ||
|
||||
(userContext.apiType === "Cassandra" &&
|
||||
configContext.CASSANDRA_PROXY_ENDPOINT !== CassandraProxyEndpoints.Development)) &&
|
||||
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;
|
||||
|
||||
const ipRulesIncludeLegacyPortalBackend: boolean = legacyPortalBackendIPs.every((legacyPortalBackendIP: string) =>
|
||||
ipAddressesFromIPRules.includes(legacyPortalBackendIP),
|
||||
);
|
||||
if (!ipRulesIncludeLegacyPortalBackend) {
|
||||
return false;
|
||||
}
|
||||
@@ -346,9 +348,9 @@ const showMongoAndCassandraProxiesNetworkSettingsWarning = (): boolean => {
|
||||
? [...MongoProxyOutboundIPs[MongoProxyEndpoints.Mpac], ...MongoProxyOutboundIPs[MongoProxyEndpoints.Prod]]
|
||||
: MongoProxyOutboundIPs[configContext.MONGO_PROXY_ENDPOINT];
|
||||
|
||||
const ipRulesIncludeMongoProxy: boolean =
|
||||
ipAddressesFromIPRules.filter((ipAddressFromIPRule) => mongoProxyOutboundIPs.includes(ipAddressFromIPRule))
|
||||
?.length === mongoProxyOutboundIPs.length;
|
||||
const ipRulesIncludeMongoProxy: boolean = mongoProxyOutboundIPs.every((mongoProxyOutboundIP: string) =>
|
||||
ipAddressesFromIPRules.includes(mongoProxyOutboundIP),
|
||||
);
|
||||
|
||||
if (ipRulesIncludeMongoProxy) {
|
||||
updateConfigContext({
|
||||
@@ -370,9 +372,15 @@ const showMongoAndCassandraProxiesNetworkSettingsWarning = (): boolean => {
|
||||
]
|
||||
: CassandraProxyOutboundIPs[configContext.CASSANDRA_PROXY_ENDPOINT];
|
||||
|
||||
const ipRulesIncludeCassandraProxy: boolean =
|
||||
ipAddressesFromIPRules.filter((ipAddressFromIPRule) => cassandraProxyOutboundIPs.includes(ipAddressFromIPRule))
|
||||
?.length === cassandraProxyOutboundIPs.length;
|
||||
const ipRulesIncludeCassandraProxy: boolean = cassandraProxyOutboundIPs.every(
|
||||
(cassandraProxyOutboundIP: string) => ipAddressesFromIPRules.includes(cassandraProxyOutboundIP),
|
||||
);
|
||||
|
||||
if (ipRulesIncludeCassandraProxy) {
|
||||
updateConfigContext({
|
||||
CASSANDRA_PROXY_OUTBOUND_IPS_ALLOWLISTED: true,
|
||||
});
|
||||
}
|
||||
|
||||
return !ipRulesIncludeCassandraProxy;
|
||||
}
|
||||
|
||||
@@ -40,11 +40,10 @@ export default class TabsBase extends WaitsForTemplateViewModel {
|
||||
this.database = options.database;
|
||||
this.rid = options.rid || (this.collection && this.collection.rid) || "";
|
||||
this.tabKind = options.tabKind;
|
||||
this.tabTitle = ko.observable<string>(options.title);
|
||||
this.tabTitle = ko.observable<string>(this.getTitle(options));
|
||||
this.tabPath =
|
||||
ko.observable(options.tabPath ?? "") ||
|
||||
(this.collection &&
|
||||
ko.observable<string>(`${this.collection.databaseId}>${this.collection.id()}>${this.tabTitle()}`));
|
||||
this.collection &&
|
||||
ko.observable<string>(`${this.collection.databaseId}>${this.collection.id()}>${options.title}`);
|
||||
this.pendingNotification = ko.observable<DataModels.Notification>(undefined);
|
||||
this.onLoadStartKey = options.onLoadStartKey;
|
||||
this.closeTabButton = {
|
||||
@@ -143,6 +142,26 @@ export default class TabsBase extends WaitsForTemplateViewModel {
|
||||
return (this.collection && this.collection.container) || (this.database && this.database.container);
|
||||
}
|
||||
|
||||
public getTitle(options: ViewModels.TabOptions): string {
|
||||
const coll = this.collection?.id();
|
||||
const db = this.database?.id();
|
||||
if (coll) {
|
||||
if (coll.length > 8) {
|
||||
return coll.slice(0, 5) + "…" + options.title;
|
||||
} else {
|
||||
return coll + "." + options.title;
|
||||
}
|
||||
} else if (db) {
|
||||
if (db.length > 8) {
|
||||
return db.slice(0, 5) + "…" + options.title;
|
||||
} else {
|
||||
return db + "." + options.title;
|
||||
}
|
||||
} else {
|
||||
return options.title;
|
||||
}
|
||||
}
|
||||
|
||||
/** Renders a Javascript object to be displayed inside Monaco Editor */
|
||||
public renderObjectForEditor(value: any, replacer: any, space: string | number): string {
|
||||
return JSON.stringify(value, replacer, space);
|
||||
|
||||
@@ -58,6 +58,7 @@ export default class Collection implements ViewModels.Collection {
|
||||
public indexingPolicy: ko.Observable<DataModels.IndexingPolicy>;
|
||||
public uniqueKeyPolicy: DataModels.UniqueKeyPolicy;
|
||||
public usageSizeInKB: ko.Observable<number>;
|
||||
public computedProperties: ko.Observable<DataModels.ComputedProperties>;
|
||||
|
||||
public offer: ko.Observable<DataModels.Offer>;
|
||||
public conflictResolutionPolicy: ko.Observable<DataModels.ConflictResolutionPolicy>;
|
||||
@@ -121,6 +122,7 @@ export default class Collection implements ViewModels.Collection {
|
||||
this.schema = data.schema;
|
||||
this.requestSchema = data.requestSchema;
|
||||
this.geospatialConfig = ko.observable(data.geospatialConfig);
|
||||
this.computedProperties = ko.observable(data.computedProperties);
|
||||
|
||||
this.partitionKeyPropertyHeaders = this.partitionKey?.paths;
|
||||
this.partitionKeyProperties = this.partitionKeyPropertyHeaders?.map((partitionKeyPropertyHeader, i) => {
|
||||
@@ -306,7 +308,7 @@ export default class Collection implements ViewModels.Collection {
|
||||
collectionName: this.id(),
|
||||
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
tabTitle: this.rawDataModel.id + " - Items",
|
||||
tabTitle: "Items",
|
||||
});
|
||||
this.documentIds([]);
|
||||
|
||||
@@ -314,7 +316,7 @@ export default class Collection implements ViewModels.Collection {
|
||||
partitionKey: this.partitionKey,
|
||||
documentIds: ko.observableArray<DocumentId>([]),
|
||||
tabKind: ViewModels.CollectionTabKind.Documents,
|
||||
title: this.rawDataModel.id + " - Items",
|
||||
title: "Items",
|
||||
collection: this,
|
||||
node: this,
|
||||
tabPath: `${this.databaseId}>${this.id()}>Documents`,
|
||||
|
||||
@@ -373,11 +373,6 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
|
||||
iconSrc: NewNotebookIcon,
|
||||
onClick: () => container.onCreateDirectory(item, isGithubTree),
|
||||
},
|
||||
{
|
||||
label: "New Notebook",
|
||||
iconSrc: NewNotebookIcon,
|
||||
onClick: () => container.onNewNotebookClicked(item, isGithubTree),
|
||||
},
|
||||
{
|
||||
label: "Upload File",
|
||||
iconSrc: NewNotebookIcon,
|
||||
@@ -786,9 +781,6 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
|
||||
<AccordionItemComponent title={"DATA"} isExpanded={!gitHubNotebooksContentRoot}>
|
||||
<TreeComponent className="dataResourceTree" rootNode={dataRootNode} />
|
||||
</AccordionItemComponent>
|
||||
<AccordionItemComponent title={"NOTEBOOKS"}>
|
||||
<TreeComponent className="notebookResourceTree" rootNode={buildNotebooksTree()} />
|
||||
</AccordionItemComponent>
|
||||
</AccordionComponent>
|
||||
|
||||
{/* {buildGalleryCallout()} */}
|
||||
|
||||
@@ -800,11 +800,6 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
||||
iconSrc: NewNotebookIcon,
|
||||
onClick: () => this.container.onCreateDirectory(item),
|
||||
},
|
||||
{
|
||||
label: "New Notebook",
|
||||
iconSrc: NewNotebookIcon,
|
||||
onClick: () => this.container.onNewNotebookClicked(item),
|
||||
},
|
||||
{
|
||||
label: "Upload File",
|
||||
iconSrc: NewNotebookIcon,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { initializeIcons } from "@fluentui/react";
|
||||
import { useBoolean } from "@fluentui/react-hooks";
|
||||
import { AadAuthorizationFailure } from "Platform/Hosted/Components/AadAuthorizationFailure";
|
||||
import * as React from "react";
|
||||
import { render } from "react-dom";
|
||||
import ChevronRight from "../images/chevron-right.svg";
|
||||
@@ -32,7 +33,8 @@ const App: React.FunctionComponent = () => {
|
||||
// For showing/hiding panel
|
||||
const [isOpen, { setTrue: openPanel, setFalse: dismissPanel }] = useBoolean(false);
|
||||
const config = useConfig();
|
||||
const { isLoggedIn, armToken, graphToken, account, tenantId, logout, login, switchTenant } = useAADAuth();
|
||||
const { isLoggedIn, armToken, graphToken, account, tenantId, logout, login, switchTenant, authFailure } =
|
||||
useAADAuth();
|
||||
const [databaseAccount, setDatabaseAccount] = React.useState<DatabaseAccount>();
|
||||
const [authType, setAuthType] = React.useState<AuthType>(encryptedToken ? AuthType.EncryptedToken : undefined);
|
||||
const [connectionString, setConnectionString] = React.useState<string>();
|
||||
@@ -136,7 +138,10 @@ const App: React.FunctionComponent = () => {
|
||||
{!isLoggedIn && !encryptedTokenMetadata && (
|
||||
<ConnectExplorer {...{ login, setEncryptedToken, setAuthType, connectionString, setConnectionString }} />
|
||||
)}
|
||||
{isLoggedIn && <DirectoryPickerPanel {...{ isOpen, dismissPanel, armToken, tenantId, switchTenant }} />}
|
||||
{isLoggedIn && authFailure && <AadAuthorizationFailure {...{ authFailure }} />}
|
||||
{isLoggedIn && !authFailure && (
|
||||
<DirectoryPickerPanel {...{ isOpen, dismissPanel, armToken, tenantId, switchTenant }} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// Import this first, to ensure that the dev tools hook is copied before React is loaded.
|
||||
import "./ReactDevTools";
|
||||
|
||||
// CSS Dependencies
|
||||
import { initializeIcons, loadTheme } from "@fluentui/react";
|
||||
import { QuickstartCarousel } from "Explorer/Quickstart/QuickstartCarousel";
|
||||
|
||||
52
src/Platform/Hosted/AadAuthorizationFailure.less
Normal file
52
src/Platform/Hosted/AadAuthorizationFailure.less
Normal file
@@ -0,0 +1,52 @@
|
||||
.aadAuthFailureContainer {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
.aadAuthFailureContainer .aadAuthFailureFormContainer {
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: -ms-flex;
|
||||
display: flex;
|
||||
-webkit-flex-direction: column;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
.aadAuthFailureContainer .aadAuthFailure {
|
||||
text-align: center;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: -ms-flex;
|
||||
display: flex;
|
||||
-webkit-flex-direction: column;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
margin-bottom: 60px;
|
||||
}
|
||||
.aadAuthFailureContainer .aadAuthFailure .authFailureTitle {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #d12d2d;
|
||||
margin: 16px 8px 8px 8px;
|
||||
}
|
||||
.aadAuthFailureContainer .aadAuthFailure .authFailureMessage {
|
||||
font-size: 14px;
|
||||
color: #393939;
|
||||
margin: 16px 16px 16px 16px;
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.aadAuthFailureContainer .aadAuthFailure .authFailureLink {
|
||||
margin: 8px;
|
||||
font-size: 14px;
|
||||
color: #0058ad;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.aadAuthFailureContainer .aadAuthFailure .aadAuthFailureContent {
|
||||
margin: 8px;
|
||||
color: #393939;
|
||||
}
|
||||
29
src/Platform/Hosted/Components/AadAuthorizationFailure.tsx
Normal file
29
src/Platform/Hosted/Components/AadAuthorizationFailure.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { AadAuthFailure } from "hooks/useAADAuth";
|
||||
import * as React from "react";
|
||||
import ConnectImage from "../../../../images/HdeConnectCosmosDB.svg";
|
||||
import "../AadAuthorizationFailure.less";
|
||||
|
||||
interface Props {
|
||||
authFailure: AadAuthFailure;
|
||||
}
|
||||
|
||||
export const AadAuthorizationFailure: React.FunctionComponent<Props> = ({ authFailure }: Props) => {
|
||||
return (
|
||||
<div id="aadAuthFailure" className="aadAuthFailureContainer" style={{ display: "flex" }}>
|
||||
<div className="aadAuthFailureFormContainer">
|
||||
<div className="aadAuthFailure">
|
||||
<p className="aadAuthFailureContent">
|
||||
<img src={ConnectImage} alt="Azure Cosmos DB" />
|
||||
</p>
|
||||
<p className="authFailureTitle">Authorization Failure</p>
|
||||
<p className="authFailureMessage">{authFailure.failureMessage}</p>
|
||||
{authFailure.failureLinkTitle && (
|
||||
<p className="authFailureLink" onClick={authFailure.failureLinkAction}>
|
||||
{authFailure.failureLinkTitle}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useBoolean } from "@fluentui/react-hooks";
|
||||
import { userContext } from "UserContext";
|
||||
import { useNewPortalBackendEndpoint } from "Utils/EndpointUtils";
|
||||
import * as React from "react";
|
||||
import ConnectImage from "../../../../images/HdeConnectCosmosDB.svg";
|
||||
import ErrorImage from "../../../../images/error.svg";
|
||||
import { AuthType } from "../../../AuthType";
|
||||
import { HttpHeaders } from "../../../Common/Constants";
|
||||
import { BackendApi, HttpHeaders } from "../../../Common/Constants";
|
||||
import { configContext } from "../../../ConfigContext";
|
||||
import { GenerateTokenResponse } from "../../../Contracts/DataModels";
|
||||
import { isResourceTokenConnectionString } from "../Helpers/ResourceTokenUtils";
|
||||
@@ -18,6 +19,23 @@ interface Props {
|
||||
}
|
||||
|
||||
export const fetchEncryptedToken = async (connectionString: string): Promise<string> => {
|
||||
if (!useNewPortalBackendEndpoint(BackendApi.GenerateToken)) {
|
||||
return await fetchEncryptedToken_ToBeDeprecated(connectionString);
|
||||
}
|
||||
|
||||
const headers = new Headers();
|
||||
headers.append(HttpHeaders.connectionString, connectionString);
|
||||
const url = configContext.PORTAL_BACKEND_ENDPOINT + "/api/connectionstring/token/generatetoken";
|
||||
const response = await fetch(url, { headers, method: "POST" });
|
||||
if (!response.ok) {
|
||||
throw response;
|
||||
}
|
||||
|
||||
const encryptedTokenResponse: string = await response.json();
|
||||
return decodeURIComponent(encryptedTokenResponse);
|
||||
};
|
||||
|
||||
export const fetchEncryptedToken_ToBeDeprecated = async (connectionString: string): Promise<string> => {
|
||||
const headers = new Headers();
|
||||
headers.append(HttpHeaders.connectionString, connectionString);
|
||||
const url = configContext.BACKEND_ENDPOINT + "/api/guest/tokens/generateToken";
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
if (window.parent !== window) {
|
||||
try {
|
||||
(window as any).__REACT_DEVTOOLS_GLOBAL_HOOK__ = (window.parent as any).__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
||||
} catch {
|
||||
// No-op. We can throw here if the parent is not the same origin (such as in the Azure portal).
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,8 +51,24 @@ interface FabricContext {
|
||||
connectionId: string;
|
||||
databaseConnectionInfo: FabricDatabaseConnectionInfo | undefined;
|
||||
isReadOnly: boolean;
|
||||
isVisible: boolean;
|
||||
}
|
||||
|
||||
export type AdminFeedbackControlPolicy =
|
||||
| "connectedExperiences"
|
||||
| "policyAllowFeedback"
|
||||
| "policyAllowSurvey"
|
||||
| "policyAllowScreenshot"
|
||||
| "policyAllowContact"
|
||||
| "policyAllowContent"
|
||||
| "policyEmailCollectionDefault"
|
||||
| "policyScreenshotDefault"
|
||||
| "policyContentSamplesDefault";
|
||||
|
||||
export type AdminFeedbackPolicySettings = {
|
||||
[key in AdminFeedbackControlPolicy]: boolean;
|
||||
};
|
||||
|
||||
interface UserContext {
|
||||
readonly fabricContext?: FabricContext;
|
||||
readonly authType?: AuthType;
|
||||
@@ -84,6 +100,7 @@ interface UserContext {
|
||||
collectionCreationDefaults: CollectionCreationDefaults;
|
||||
sampleDataConnectionInfo?: ParsedResourceTokenConnectionString;
|
||||
readonly vcoreMongoConnectionParams?: VCoreMongoConnectionParams;
|
||||
readonly feedbackPolicies?: AdminFeedbackPolicySettings;
|
||||
}
|
||||
|
||||
export type ApiType = "SQL" | "Mongo" | "Gremlin" | "Tables" | "Cassandra" | "Postgres" | "VCoreMongo";
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import * as msal from "@azure/msal-browser";
|
||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||
import { AuthType } from "../AuthType";
|
||||
import * as Constants from "../Common/Constants";
|
||||
import * as Logger from "../Common/Logger";
|
||||
import { configContext } from "../ConfigContext";
|
||||
import * as ViewModels from "../Contracts/ViewModels";
|
||||
import { traceFailure } from "../Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "../UserContext";
|
||||
|
||||
export function getAuthorizationHeader(): ViewModels.AuthorizationTokenHeaderMetadata {
|
||||
@@ -43,8 +45,8 @@ export function decryptJWTToken(token: string) {
|
||||
return JSON.parse(tokenPayload);
|
||||
}
|
||||
|
||||
export function getMsalInstance() {
|
||||
const config: msal.Configuration = {
|
||||
export async function getMsalInstance() {
|
||||
const msalConfig: msal.Configuration = {
|
||||
cache: {
|
||||
cacheLocation: "localStorage",
|
||||
},
|
||||
@@ -55,8 +57,46 @@ export function getMsalInstance() {
|
||||
};
|
||||
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
config.auth.redirectUri = "https://dataexplorer-dev.azurewebsites.net";
|
||||
msalConfig.auth.redirectUri = "https://dataexplorer-dev.azurewebsites.net";
|
||||
}
|
||||
const msalInstance = new msal.PublicClientApplication(config);
|
||||
|
||||
const msalInstance = new msal.PublicClientApplication(msalConfig);
|
||||
return msalInstance;
|
||||
}
|
||||
|
||||
export async function acquireTokenWithMsal(msalInstance: msal.IPublicClientApplication, request: msal.SilentRequest) {
|
||||
const tokenRequest = {
|
||||
account: msalInstance.getActiveAccount() || null,
|
||||
...request,
|
||||
};
|
||||
|
||||
try {
|
||||
// attempt silent acquisition first
|
||||
return (await msalInstance.acquireTokenSilent(tokenRequest)).accessToken;
|
||||
} catch (silentError) {
|
||||
if (silentError instanceof msal.InteractionRequiredAuthError) {
|
||||
try {
|
||||
// The error indicates that we need to acquire the token interactively.
|
||||
// This will display a pop-up to re-establish authorization. If user does not
|
||||
// have pop-ups enabled in their browser, this will fail.
|
||||
return (await msalInstance.acquireTokenPopup(tokenRequest)).accessToken;
|
||||
} catch (interactiveError) {
|
||||
traceFailure(Action.SignInAad, {
|
||||
request: JSON.stringify(tokenRequest),
|
||||
acquireTokenType: "interactive",
|
||||
errorMessage: JSON.stringify(interactiveError),
|
||||
});
|
||||
|
||||
throw interactiveError;
|
||||
}
|
||||
} else {
|
||||
traceFailure(Action.SignInAad, {
|
||||
request: JSON.stringify(tokenRequest),
|
||||
acquireTokenType: "silent",
|
||||
errorMessage: JSON.stringify(silentError),
|
||||
});
|
||||
|
||||
throw silentError;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import { CassandraProxyEndpoints, JunoEndpoints, MongoProxyEndpoints } from "Common/Constants";
|
||||
import {
|
||||
BackendApi,
|
||||
CassandraProxyEndpoints,
|
||||
JunoEndpoints,
|
||||
MongoProxyEndpoints,
|
||||
PortalBackendEndpoints,
|
||||
} from "Common/Constants";
|
||||
import { configContext } from "ConfigContext";
|
||||
import * as Logger from "../Common/Logger";
|
||||
|
||||
export function validateEndpoint(
|
||||
@@ -98,6 +105,14 @@ export const allowedCassandraProxyEndpoints: ReadonlyArray<string> = [
|
||||
CassandraProxyEndpoints.Mooncake,
|
||||
];
|
||||
|
||||
export const allowedCassandraProxyEndpoints_ToBeDeprecated: ReadonlyArray<string> = [
|
||||
"https://main.documentdb.ext.azure.com",
|
||||
"https://main.documentdb.ext.azure.cn",
|
||||
"https://main.documentdb.ext.azure.us",
|
||||
"https://main.cosmos.ext.azure",
|
||||
"https://localhost:12901",
|
||||
];
|
||||
|
||||
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"],
|
||||
@@ -129,3 +144,23 @@ export const allowedJunoOrigins: ReadonlyArray<string> = [
|
||||
];
|
||||
|
||||
export const allowedNotebookServerUrls: ReadonlyArray<string> = [];
|
||||
|
||||
//
|
||||
// Temporary function to determine if a portal backend API is supported by the
|
||||
// new backend in this environment.
|
||||
//
|
||||
// TODO: Remove this function once new backend migration is completed for all environments.
|
||||
//
|
||||
export function useNewPortalBackendEndpoint(backendApi: string): boolean {
|
||||
// This maps backend APIs to the environments supported by the new backend.
|
||||
const newBackendApiEnvironmentMap: { [key: string]: string[] } = {
|
||||
[BackendApi.GenerateToken]: [PortalBackendEndpoints.Development],
|
||||
[BackendApi.PortalSettings]: [PortalBackendEndpoints.Development, PortalBackendEndpoints.Mpac],
|
||||
};
|
||||
|
||||
if (!newBackendApiEnvironmentMap[backendApi] || !configContext.PORTAL_BACKEND_ENDPOINT) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return newBackendApiEnvironmentMap[backendApi].includes(configContext.PORTAL_BACKEND_ENDPOINT);
|
||||
}
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
Run "npm run generateARMClients" to regenerate
|
||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2023-09-15-preview/cosmos-db.json
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json
|
||||
*/
|
||||
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
import { armRequest } from "../../request";
|
||||
import * as Types from "./types";
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
const apiVersion = "2023-09-15-preview";
|
||||
const apiVersion = "2024-02-15-preview";
|
||||
|
||||
/* Lists the Cassandra keyspaces under an existing Azure Cosmos DB database account. */
|
||||
export async function listCassandraKeyspaces(
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
Run "npm run generateARMClients" to regenerate
|
||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2023-09-15-preview/cosmos-db.json
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json
|
||||
*/
|
||||
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
import { armRequest } from "../../request";
|
||||
import * as Types from "./types";
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
const apiVersion = "2023-09-15-preview";
|
||||
const apiVersion = "2024-02-15-preview";
|
||||
|
||||
/* Retrieves the metrics determined by the given filter for the given database account and collection. */
|
||||
export async function listMetrics(
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
Run "npm run generateARMClients" to regenerate
|
||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2023-09-15-preview/cosmos-db.json
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json
|
||||
*/
|
||||
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
import { armRequest } from "../../request";
|
||||
import * as Types from "./types";
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
const apiVersion = "2023-09-15-preview";
|
||||
const apiVersion = "2024-02-15-preview";
|
||||
|
||||
/* Retrieves the metrics determined by the given filter for the given collection, split by partition. */
|
||||
export async function listMetrics(
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
Run "npm run generateARMClients" to regenerate
|
||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2023-09-15-preview/cosmos-db.json
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json
|
||||
*/
|
||||
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
import { armRequest } from "../../request";
|
||||
import * as Types from "./types";
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
const apiVersion = "2023-09-15-preview";
|
||||
const apiVersion = "2024-02-15-preview";
|
||||
|
||||
/* Retrieves the metrics determined by the given filter for the given collection and region, split by partition. */
|
||||
export async function listMetrics(
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
Run "npm run generateARMClients" to regenerate
|
||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2023-09-15-preview/cosmos-db.json
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json
|
||||
*/
|
||||
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
import { armRequest } from "../../request";
|
||||
import * as Types from "./types";
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
const apiVersion = "2023-09-15-preview";
|
||||
const apiVersion = "2024-02-15-preview";
|
||||
|
||||
/* Retrieves the metrics determined by the given filter for the given database account, collection and region. */
|
||||
export async function listMetrics(
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
Run "npm run generateARMClients" to regenerate
|
||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2023-09-15-preview/cosmos-db.json
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json
|
||||
*/
|
||||
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
import { armRequest } from "../../request";
|
||||
import * as Types from "./types";
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
const apiVersion = "2023-09-15-preview";
|
||||
const apiVersion = "2024-02-15-preview";
|
||||
|
||||
/* Retrieves the metrics determined by the given filter for the given database account and database. */
|
||||
export async function listMetrics(
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
Run "npm run generateARMClients" to regenerate
|
||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2023-09-15-preview/cosmos-db.json
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json
|
||||
*/
|
||||
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
import { armRequest } from "../../request";
|
||||
import * as Types from "./types";
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
const apiVersion = "2023-09-15-preview";
|
||||
const apiVersion = "2024-02-15-preview";
|
||||
|
||||
/* Retrieves the metrics determined by the given filter for the given database account and region. */
|
||||
export async function listMetrics(
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
Run "npm run generateARMClients" to regenerate
|
||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2023-09-15-preview/cosmos-db.json
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json
|
||||
*/
|
||||
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
import { armRequest } from "../../request";
|
||||
import * as Types from "./types";
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
const apiVersion = "2023-09-15-preview";
|
||||
const apiVersion = "2024-02-15-preview";
|
||||
|
||||
/* Retrieves the properties of an existing Azure Cosmos DB database account. */
|
||||
export async function get(
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
Run "npm run generateARMClients" to regenerate
|
||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2023-09-15-preview/cosmos-db.json
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json
|
||||
*/
|
||||
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
import { armRequest } from "../../request";
|
||||
import * as Types from "./types";
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
const apiVersion = "2023-09-15-preview";
|
||||
const apiVersion = "2024-02-15-preview";
|
||||
|
||||
/* Lists the graphs under an existing Azure Cosmos DB database account. */
|
||||
export async function listGraphs(
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
Run "npm run generateARMClients" to regenerate
|
||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2023-09-15-preview/cosmos-db.json
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json
|
||||
*/
|
||||
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
import { armRequest } from "../../request";
|
||||
import * as Types from "./types";
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
const apiVersion = "2023-09-15-preview";
|
||||
const apiVersion = "2024-02-15-preview";
|
||||
|
||||
/* Lists the Gremlin databases under an existing Azure Cosmos DB database account. */
|
||||
export async function listGremlinDatabases(
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
Run "npm run generateARMClients" to regenerate
|
||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2023-09-15-preview/cosmos-db.json
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json
|
||||
*/
|
||||
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
import { armRequest } from "../../request";
|
||||
import * as Types from "./types";
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
const apiVersion = "2023-09-15-preview";
|
||||
const apiVersion = "2024-02-15-preview";
|
||||
|
||||
/* List Cosmos DB locations and their properties */
|
||||
export async function list(subscriptionId: string): Promise<Types.LocationListResult | Types.CloudError> {
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
Run "npm run generateARMClients" to regenerate
|
||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2023-09-15-preview/cosmos-db.json
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json
|
||||
*/
|
||||
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
import { armRequest } from "../../request";
|
||||
import * as Types from "./types";
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
const apiVersion = "2023-09-15-preview";
|
||||
const apiVersion = "2024-02-15-preview";
|
||||
|
||||
/* Lists the MongoDB databases under an existing Azure Cosmos DB database account. */
|
||||
export async function listMongoDBDatabases(
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
Run "npm run generateARMClients" to regenerate
|
||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2023-09-15-preview/cosmos-db.json
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json
|
||||
*/
|
||||
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
import { armRequest } from "../../request";
|
||||
import * as Types from "./types";
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
const apiVersion = "2023-09-15-preview";
|
||||
const apiVersion = "2024-02-15-preview";
|
||||
|
||||
/* Lists all of the available Cosmos DB Resource Provider operations. */
|
||||
export async function list(): Promise<Types.OperationListResult> {
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
Run "npm run generateARMClients" to regenerate
|
||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2023-09-15-preview/cosmos-db.json
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json
|
||||
*/
|
||||
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
import { armRequest } from "../../request";
|
||||
import * as Types from "./types";
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
const apiVersion = "2023-09-15-preview";
|
||||
const apiVersion = "2024-02-15-preview";
|
||||
|
||||
/* Retrieves the metrics determined by the given filter for the given partition key range id. */
|
||||
export async function listMetrics(
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
Run "npm run generateARMClients" to regenerate
|
||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2023-09-15-preview/cosmos-db.json
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json
|
||||
*/
|
||||
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
import { armRequest } from "../../request";
|
||||
import * as Types from "./types";
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
const apiVersion = "2023-09-15-preview";
|
||||
const apiVersion = "2024-02-15-preview";
|
||||
|
||||
/* Retrieves the metrics determined by the given filter for the given partition key range id and region. */
|
||||
export async function listMetrics(
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
Run "npm run generateARMClients" to regenerate
|
||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2023-09-15-preview/cosmos-db.json
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json
|
||||
*/
|
||||
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
import { armRequest } from "../../request";
|
||||
import * as Types from "./types";
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
const apiVersion = "2023-09-15-preview";
|
||||
const apiVersion = "2024-02-15-preview";
|
||||
|
||||
/* Retrieves the metrics determined by the given filter for the given database account. This url is only for PBS and Replication Latency data */
|
||||
export async function listMetrics(
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
Run "npm run generateARMClients" to regenerate
|
||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2023-09-15-preview/cosmos-db.json
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json
|
||||
*/
|
||||
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
import { armRequest } from "../../request";
|
||||
import * as Types from "./types";
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
const apiVersion = "2023-09-15-preview";
|
||||
const apiVersion = "2024-02-15-preview";
|
||||
|
||||
/* Retrieves the metrics determined by the given filter for the given account, source and target region. This url is only for PBS and Replication Latency data */
|
||||
export async function listMetrics(
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
Run "npm run generateARMClients" to regenerate
|
||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2023-09-15-preview/cosmos-db.json
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json
|
||||
*/
|
||||
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
import { armRequest } from "../../request";
|
||||
import * as Types from "./types";
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
const apiVersion = "2023-09-15-preview";
|
||||
const apiVersion = "2024-02-15-preview";
|
||||
|
||||
/* Retrieves the metrics determined by the given filter for the given account target region. This url is only for PBS and Replication Latency data */
|
||||
export async function listMetrics(
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
Run "npm run generateARMClients" to regenerate
|
||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2023-09-15-preview/cosmos-db.json
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json
|
||||
*/
|
||||
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
import { armRequest } from "../../request";
|
||||
import * as Types from "./types";
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
const apiVersion = "2023-09-15-preview";
|
||||
const apiVersion = "2024-02-15-preview";
|
||||
|
||||
/* Lists the SQL databases under an existing Azure Cosmos DB database account. */
|
||||
export async function listSqlDatabases(
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
Run "npm run generateARMClients" to regenerate
|
||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2023-09-15-preview/cosmos-db.json
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json
|
||||
*/
|
||||
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
import { armRequest } from "../../request";
|
||||
import * as Types from "./types";
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
const apiVersion = "2023-09-15-preview";
|
||||
const apiVersion = "2024-02-15-preview";
|
||||
|
||||
/* Lists the Tables under an existing Azure Cosmos DB database account. */
|
||||
export async function listTables(
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
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-09-15-preview/cosmos-db.json
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json
|
||||
*/
|
||||
|
||||
/* The List operation response, that contains the client encryption keys and their properties. */
|
||||
@@ -566,12 +566,14 @@ export interface DatabaseAccountGetProperties {
|
||||
minimalTlsVersion?: MinimalTlsVersion;
|
||||
|
||||
/* Indicates the status of the Customer Managed Key feature on the account. In case there are errors, the property provides troubleshooting guidance. */
|
||||
customerManagedKeyStatus?: CustomerManagedKeyStatus;
|
||||
|
||||
customerManagedKeyStatus?: string;
|
||||
/* Flag to indicate enabling/disabling of Priority Based Execution Preview feature on the account */
|
||||
enablePriorityBasedExecution?: boolean;
|
||||
/* Enum to indicate default Priority Level of request for Priority Based Execution. */
|
||||
defaultPriorityLevel?: DefaultPriorityLevel;
|
||||
|
||||
/* Flag to indicate enabling/disabling of Per-Region Per-partition autoscale Preview feature on the account */
|
||||
enablePerRegionPerPartitionAutoscale?: boolean;
|
||||
}
|
||||
|
||||
/* Properties to create and update Azure Cosmos DB database accounts. */
|
||||
@@ -663,12 +665,14 @@ export interface DatabaseAccountCreateUpdateProperties {
|
||||
minimalTlsVersion?: MinimalTlsVersion;
|
||||
|
||||
/* Indicates the status of the Customer Managed Key feature on the account. In case there are errors, the property provides troubleshooting guidance. */
|
||||
customerManagedKeyStatus?: CustomerManagedKeyStatus;
|
||||
|
||||
customerManagedKeyStatus?: string;
|
||||
/* Flag to indicate enabling/disabling of Priority Based Execution Preview feature on the account */
|
||||
enablePriorityBasedExecution?: boolean;
|
||||
/* Enum to indicate default Priority Level of request for Priority Based Execution. */
|
||||
defaultPriorityLevel?: DefaultPriorityLevel;
|
||||
|
||||
/* Flag to indicate enabling/disabling of Per-Region Per-partition autoscale Preview feature on the account */
|
||||
enablePerRegionPerPartitionAutoscale?: boolean;
|
||||
}
|
||||
|
||||
/* Parameters to create and update Cosmos DB database accounts. */
|
||||
@@ -763,12 +767,14 @@ export interface DatabaseAccountUpdateProperties {
|
||||
minimalTlsVersion?: MinimalTlsVersion;
|
||||
|
||||
/* Indicates the status of the Customer Managed Key feature on the account. In case there are errors, the property provides troubleshooting guidance. */
|
||||
customerManagedKeyStatus?: CustomerManagedKeyStatus;
|
||||
|
||||
customerManagedKeyStatus?: string;
|
||||
/* Flag to indicate enabling/disabling of Priority Based Execution Preview feature on the account */
|
||||
enablePriorityBasedExecution?: boolean;
|
||||
/* Enum to indicate default Priority Level of request for Priority Based Execution. */
|
||||
defaultPriorityLevel?: DefaultPriorityLevel;
|
||||
|
||||
/* Flag to indicate enabling/disabling of Per-Region Per-partition autoscale Preview feature on the account */
|
||||
enablePerRegionPerPartitionAutoscale?: boolean;
|
||||
}
|
||||
|
||||
/* Parameters for patching Azure Cosmos DB database account properties. */
|
||||
@@ -1256,6 +1262,9 @@ export interface SqlContainerResource {
|
||||
|
||||
/* The configuration for defining Materialized Views. This must be specified only for creating a Materialized View container. */
|
||||
materializedViewDefinition?: MaterializedViewDefinition;
|
||||
|
||||
/* List of computed properties */
|
||||
computedProperties?: ComputedProperty[];
|
||||
}
|
||||
|
||||
/* Cosmos DB indexing policy */
|
||||
@@ -1325,6 +1334,14 @@ export interface SpatialSpec {
|
||||
/* Indicates the spatial type of index. */
|
||||
export type SpatialType = "Point" | "LineString" | "Polygon" | "MultiPolygon";
|
||||
|
||||
/* The definition of a computed property */
|
||||
export interface ComputedProperty {
|
||||
/* The name of a computed property, for example - "cp_lowerName" */
|
||||
name?: string;
|
||||
/* The query that evaluates the value for computed property, for example - "SELECT VALUE LOWER(c.name) FROM c" */
|
||||
query?: string;
|
||||
}
|
||||
|
||||
/* The configuration of the partition key to be used for partitioning data into multiple partitions */
|
||||
export interface ContainerPartitionKey {
|
||||
/* List of paths using which data within the container can be partitioned */
|
||||
@@ -1929,6 +1946,8 @@ export interface RestoreParametersBase {
|
||||
restoreSource?: string;
|
||||
/* Time to which the account has to be restored (ISO-8601 format). */
|
||||
restoreTimestampInUtc?: string;
|
||||
/* Specifies whether the restored account will have Time-To-Live disabled upon the successful restore. */
|
||||
restoreWithTtlDisabled?: boolean;
|
||||
}
|
||||
|
||||
/* Parameters to indicate the information about the restore. */
|
||||
@@ -2072,19 +2091,5 @@ export type ContinuousTier = "Continuous7Days" | "Continuous30Days";
|
||||
/* Indicates the minimum allowed Tls version. The default is Tls 1.0, except for Cassandra and Mongo API's, which only work with Tls 1.2. */
|
||||
export type MinimalTlsVersion = "Tls" | "Tls11" | "Tls12";
|
||||
|
||||
/* Indicates the status of the Customer Managed Key feature on the account. In case there are errors, the property provides troubleshooting guidance. */
|
||||
export type CustomerManagedKeyStatus =
|
||||
| "Access to your account is currently revoked because the Azure Cosmos DB service is unable to obtain the AAD authentication token for the account's default identity; for more details about this error and how to restore access to your account please visit https://learn.microsoft.com/en-us/azure/cosmos-db/cmk-troubleshooting-guide#azure-active-directory-token-acquisition-error (4000)."
|
||||
| "Access to your account is currently revoked because the Azure Cosmos DB account's key vault key URI does not follow the expected format; for more details about this error and how to restore access to your account please visit https://learn.microsoft.com/en-us/azure/cosmos-db/cmk-troubleshooting-guide#improper-syntax-detected-on-the-key-vault-uri-property (4006)."
|
||||
| "Access to your account is currently revoked because the current default identity no longer has permission to the associated Key Vault key; for more details about this error and how to restore access to your account please visit https://learn.microsoft.com/en-us/azure/cosmos-db/cmk-troubleshooting-guide#default-identity-is-unauthorized-to-access-the-azure-key-vault-key (4002)."
|
||||
| "Access to your account is currently revoked because the Azure Key Vault DNS name specified by the account's keyvaultkeyuri property could not be resolved; for more details about this error and how to restore access to your account please visit https://learn.microsoft.com/en-us/azure/cosmos-db/cmk-troubleshooting-guide#unable-to-resolve-the-key-vaults-dns (4009)."
|
||||
| "Access to your account is currently revoked because the correspondent key is not found on the specified Key Vault; for more details about this error and how to restore access to your account please visit https://learn.microsoft.com/en-us/azure/cosmos-db/cmk-troubleshooting-guide#azure-key-vault-resource-not-found (4003)."
|
||||
| "Access to your account is currently revoked because the Azure Cosmos DB service is unable to wrap or unwrap the key; for more details about this error and how to restore access to your account please visit https://learn.microsoft.com/en-us/azure/cosmos-db/cmk-troubleshooting-guide#internal-unwrapping-procedure-error (4005)."
|
||||
| "Access to your account is currently revoked because the Azure Cosmos DB account has an undefined default identity; for more details about this error and how to restore access to your account please visit https://learn.microsoft.com/en-us/azure/cosmos-db/cmk-troubleshooting-guide#invalid-azure-cosmos-db-default-identity (4015)."
|
||||
| "Access to your account is currently revoked because the access rules are blocking outbound requests to the Azure Key Vault service; for more details about this error and how to restore access to your account please visit https://learn.microsoft.com/en-us/azure/cosmos-db/cmk-troubleshooting-guide (4016)."
|
||||
| "Access to your account is currently revoked because the correspondent Azure Key Vault was not found; for more details about this error and how to restore access to your account please visit https://learn.microsoft.com/en-us/azure/cosmos-db/cmk-troubleshooting-guide#azure-key-vault-resource-not-found (4017)."
|
||||
| "Access to your account is currently revoked; for more details about this error and how to restore access to your account please visit https://learn.microsoft.com/en-us/azure/cosmos-db/cmk-troubleshooting-guide"
|
||||
| "Access to the configured customer managed key confirmed.";
|
||||
|
||||
/* Enum to indicate default priorityLevel of requests */
|
||||
export type DefaultPriorityLevel = "High" | "Low";
|
||||
|
||||
@@ -2,9 +2,9 @@ import * as msal from "@azure/msal-browser";
|
||||
import { useBoolean } from "@fluentui/react-hooks";
|
||||
import * as React from "react";
|
||||
import { configContext } from "../ConfigContext";
|
||||
import { getMsalInstance } from "../Utils/AuthorizationUtils";
|
||||
import { acquireTokenWithMsal, getMsalInstance } from "../Utils/AuthorizationUtils";
|
||||
|
||||
const msalInstance = getMsalInstance();
|
||||
const msalInstance = await getMsalInstance();
|
||||
|
||||
const cachedAccount = msalInstance.getAllAccounts()?.[0];
|
||||
const cachedTenantId = localStorage.getItem("cachedTenantId");
|
||||
@@ -18,6 +18,13 @@ interface ReturnType {
|
||||
tenantId: string;
|
||||
account: msal.AccountInfo;
|
||||
switchTenant: (tenantId: string) => void;
|
||||
authFailure: AadAuthFailure;
|
||||
}
|
||||
|
||||
export interface AadAuthFailure {
|
||||
failureMessage: string;
|
||||
failureLinkTitle?: string;
|
||||
failureLinkAction?: () => void;
|
||||
}
|
||||
|
||||
export function useAADAuth(): ReturnType {
|
||||
@@ -28,6 +35,7 @@ export function useAADAuth(): ReturnType {
|
||||
const [tenantId, setTenantId] = React.useState<string>(cachedTenantId);
|
||||
const [graphToken, setGraphToken] = React.useState<string>();
|
||||
const [armToken, setArmToken] = React.useState<string>();
|
||||
const [authFailure, setAuthFailure] = React.useState<AadAuthFailure>(undefined);
|
||||
|
||||
msalInstance.setActiveAccount(account);
|
||||
const login = React.useCallback(async () => {
|
||||
@@ -61,24 +69,60 @@ export function useAADAuth(): ReturnType {
|
||||
[account, tenantId],
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (account && tenantId) {
|
||||
Promise.all([
|
||||
msalInstance.acquireTokenSilent({
|
||||
authority: `${configContext.AAD_ENDPOINT}${tenantId}`,
|
||||
scopes: [`${configContext.GRAPH_ENDPOINT}/.default`],
|
||||
}),
|
||||
msalInstance.acquireTokenSilent({
|
||||
const acquireTokens = React.useCallback(async () => {
|
||||
if (!(account && tenantId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const armToken = await acquireTokenWithMsal(msalInstance, {
|
||||
authority: `${configContext.AAD_ENDPOINT}${tenantId}`,
|
||||
scopes: [`${configContext.ARM_ENDPOINT}/.default`],
|
||||
}),
|
||||
]).then(([graphTokenResponse, armTokenResponse]) => {
|
||||
setGraphToken(graphTokenResponse.accessToken);
|
||||
setArmToken(armTokenResponse.accessToken);
|
||||
});
|
||||
|
||||
setArmToken(armToken);
|
||||
setAuthFailure(null);
|
||||
} catch (error) {
|
||||
if (error instanceof msal.AuthError && error.errorCode === msal.BrowserAuthErrorMessage.popUpWindowError.code) {
|
||||
// This error can occur when acquireTokenWithMsal() has attempted to acquire token interactively
|
||||
// and user has popups disabled in browser. This fails as the popup is not the result of a explicit user
|
||||
// action. In this case, we display the failure and a link to repeat the operation. Clicking on the
|
||||
// link is a user action so it will work even if popups have been disabled.
|
||||
// See: https://github.com/AzureAD/microsoft-authentication-library-for-js/issues/76#issuecomment-324787539
|
||||
setAuthFailure({
|
||||
failureMessage:
|
||||
"We were unable to establish authorization for this account, due to pop-ups being disabled in the browser.\nPlease click below to retry authorization without requiring popups being enabled.",
|
||||
failureLinkTitle: "Retry Authorization",
|
||||
failureLinkAction: acquireTokens,
|
||||
});
|
||||
} else {
|
||||
const errorJson = JSON.stringify(error);
|
||||
setAuthFailure({
|
||||
failureMessage: `We were unable to establish authorization for this account, due to the following error: \n${errorJson}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const graphToken = await acquireTokenWithMsal(msalInstance, {
|
||||
authority: `${configContext.AAD_ENDPOINT}${tenantId}`,
|
||||
scopes: [`${configContext.GRAPH_ENDPOINT}/.default`],
|
||||
});
|
||||
|
||||
setGraphToken(graphToken);
|
||||
} catch (error) {
|
||||
// Graph token is used only for retrieving user photo at the moment, so
|
||||
// it's not critical if this fails.
|
||||
console.warn("Error acquiring graph token: " + error);
|
||||
}
|
||||
}, [account, tenantId]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (account && tenantId && !authFailure) {
|
||||
acquireTokens();
|
||||
}
|
||||
}, [account, tenantId, acquireTokens, authFailure]);
|
||||
|
||||
return {
|
||||
account,
|
||||
tenantId,
|
||||
@@ -88,5 +132,6 @@ export function useAADAuth(): ReturnType {
|
||||
login,
|
||||
logout,
|
||||
switchTenant,
|
||||
authFailure,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@ import { createUri } from "Common/UrlUtility";
|
||||
import { DATA_EXPLORER_RPC_VERSION } from "Contracts/DataExplorerMessagesContract";
|
||||
import { FABRIC_RPC_VERSION, FabricMessageV2 } from "Contracts/FabricMessagesContract";
|
||||
import Explorer from "Explorer/Explorer";
|
||||
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
||||
import { useSelectedNode } from "Explorer/useSelectedNode";
|
||||
import { scheduleRefreshDatabaseResourceToken } from "Platform/Fabric/FabricUtil";
|
||||
import { getNetworkSettingsWarningMessage } from "Utils/NetworkUtility";
|
||||
import { logConsoleError } from "Utils/NotificationConsoleUtils";
|
||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
||||
import { useEffect, useState } from "react";
|
||||
@@ -35,7 +35,7 @@ import {
|
||||
import { extractFeatures } from "../Platform/Hosted/extractFeatures";
|
||||
import { DefaultExperienceUtility } from "../Shared/DefaultExperienceUtility";
|
||||
import { Node, PortalEnv, updateUserContext, userContext } from "../UserContext";
|
||||
import { getAuthorizationHeader, getMsalInstance } from "../Utils/AuthorizationUtils";
|
||||
import { acquireTokenWithMsal, getAuthorizationHeader, getMsalInstance } from "../Utils/AuthorizationUtils";
|
||||
import { isInvalidParentFrameOrigin, shouldProcessMessage } from "../Utils/MessageValidation";
|
||||
import { listKeys } from "../Utils/arm/generatedClients/cosmos/databaseAccounts";
|
||||
import { DatabaseAccountListKeysResult } from "../Utils/arm/generatedClients/cosmos/types";
|
||||
@@ -89,6 +89,7 @@ async function configureFabric(): Promise<Explorer> {
|
||||
// These are the versions of Fabric that Data Explorer supports.
|
||||
const SUPPORTED_FABRIC_VERSIONS = [FABRIC_RPC_VERSION];
|
||||
|
||||
let firstContainerOpened = false;
|
||||
let explorer: Explorer;
|
||||
return new Promise<Explorer>((resolve) => {
|
||||
window.addEventListener(
|
||||
@@ -120,7 +121,10 @@ async function configureFabric(): Promise<Explorer> {
|
||||
await scheduleRefreshDatabaseResourceToken(true);
|
||||
resolve(explorer);
|
||||
await explorer.refreshAllDatabases();
|
||||
if (userContext.fabricContext.isVisible && !firstContainerOpened) {
|
||||
firstContainerOpened = true;
|
||||
openFirstContainer(explorer, userContext.fabricContext.databaseConnectionInfo.databaseId);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "newContainer":
|
||||
@@ -131,8 +135,16 @@ async function configureFabric(): Promise<Explorer> {
|
||||
handleCachedDataMessage(data);
|
||||
break;
|
||||
}
|
||||
case "setToolbarStatus": {
|
||||
useCommandBar.getState().setIsHidden(data.message.visible === false);
|
||||
case "explorerVisible": {
|
||||
userContext.fabricContext.isVisible = data.message.visible;
|
||||
if (
|
||||
userContext.fabricContext.isVisible &&
|
||||
!firstContainerOpened &&
|
||||
userContext?.fabricContext?.databaseConnectionInfo?.databaseId !== undefined
|
||||
) {
|
||||
firstContainerOpened = true;
|
||||
openFirstContainer(explorer, userContext.fabricContext.databaseConnectionInfo.databaseId);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
@@ -243,16 +255,19 @@ async function configureHostedWithAAD(config: AAD): Promise<Explorer> {
|
||||
let keys: DatabaseAccountListKeysResult = {};
|
||||
if (account.properties?.documentEndpoint) {
|
||||
const hrefEndpoint = new URL(account.properties.documentEndpoint).href.replace(/\/$/, "/.default");
|
||||
const msalInstance = getMsalInstance();
|
||||
const msalInstance = await getMsalInstance();
|
||||
const cachedAccount = msalInstance.getAllAccounts()?.[0];
|
||||
msalInstance.setActiveAccount(cachedAccount);
|
||||
const cachedTenantId = localStorage.getItem("cachedTenantId");
|
||||
const aadTokenResponse = await msalInstance.acquireTokenSilent({
|
||||
try {
|
||||
aadToken = await acquireTokenWithMsal(msalInstance, {
|
||||
forceRefresh: true,
|
||||
scopes: [hrefEndpoint],
|
||||
authority: `${configContext.AAD_ENDPOINT}${cachedTenantId}`,
|
||||
});
|
||||
aadToken = aadTokenResponse.accessToken;
|
||||
} catch (authError) {
|
||||
logConsoleError("Failed to acquire authorization token: " + authError);
|
||||
}
|
||||
}
|
||||
try {
|
||||
if (!account.properties.disableLocalAuth) {
|
||||
@@ -323,12 +338,13 @@ function configureHostedWithResourceToken(config: ResourceToken): Explorer {
|
||||
return explorer;
|
||||
}
|
||||
|
||||
function createExplorerFabric(params: { connectionId: string }): Explorer {
|
||||
function createExplorerFabric(params: { connectionId: string; isVisible: boolean }): Explorer {
|
||||
updateUserContext({
|
||||
fabricContext: {
|
||||
connectionId: params.connectionId,
|
||||
databaseConnectionInfo: undefined,
|
||||
isReadOnly: true,
|
||||
isVisible: params.isVisible ?? true,
|
||||
},
|
||||
authType: AuthType.ConnectionString,
|
||||
databaseAccount: {
|
||||
@@ -481,6 +497,7 @@ function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) {
|
||||
ARM_ENDPOINT: normalizeArmEndpoint(inputs.csmEndpoint || configContext.ARM_ENDPOINT),
|
||||
MONGO_PROXY_ENDPOINT: inputs.mongoProxyEndpoint,
|
||||
CASSANDRA_PROXY_ENDPOINT: inputs.cassandraProxyEndpoint,
|
||||
PORTAL_BACKEND_ENDPOINT: inputs.portalBackendEndpoint,
|
||||
});
|
||||
|
||||
updateUserContext({
|
||||
@@ -495,6 +512,7 @@ function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) {
|
||||
hasWriteAccess: inputs.hasWriteAccess ?? true,
|
||||
collectionCreationDefaults: inputs.defaultCollectionThroughput,
|
||||
isTryCosmosDBSubscription: inputs.isTryCosmosDBSubscription,
|
||||
feedbackPolicies: inputs.feedbackPolicies,
|
||||
});
|
||||
|
||||
if (inputs.isPostgresAccount) {
|
||||
|
||||
@@ -105,7 +105,7 @@ export const useTabs: UseStore<TabsState> = create((set, get) => ({
|
||||
return true;
|
||||
});
|
||||
if (updatedTabs.length === 0 && configContext.platform !== Platform.Fabric) {
|
||||
set({ activeTab: undefined, activeReactTab: ReactTabKind.Home });
|
||||
set({ activeTab: undefined, activeReactTab: undefined });
|
||||
}
|
||||
|
||||
if (tab.tabId === activeTab.tabId && tabIndex !== -1) {
|
||||
@@ -143,7 +143,7 @@ export const useTabs: UseStore<TabsState> = create((set, get) => ({
|
||||
});
|
||||
|
||||
if (get().openedTabs.length === 0 && configContext.platform !== Platform.Fabric) {
|
||||
set({ activeTab: undefined, activeReactTab: ReactTabKind.Home });
|
||||
set({ activeTab: undefined, activeReactTab: undefined });
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import { jest } from "@jest/globals";
|
||||
import "expect-playwright";
|
||||
import { generateUniqueName } from "../utils/shared";
|
||||
import { generateUniqueName, getAzureCLICredentialsToken } from "../utils/shared";
|
||||
import { waitForExplorer } from "../utils/waitForExplorer";
|
||||
jest.setTimeout(120000);
|
||||
|
||||
test("Cassandra keyspace and table CRUD", async () => {
|
||||
const keyspaceId = generateUniqueName("keyspace");
|
||||
const tableId = generateUniqueName("table");
|
||||
|
||||
// We can't retrieve AZ CLI credentials from the browser so we get them here.
|
||||
const token = await getAzureCLICredentialsToken();
|
||||
page.setDefaultTimeout(50000);
|
||||
|
||||
await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-cassandra-runner");
|
||||
await page.goto(`https://localhost:1234/testExplorer.html?accountName=portal-cassandra-runner&token=${token}`);
|
||||
await page.waitForSelector("iframe");
|
||||
const explorer = await waitForExplorer();
|
||||
|
||||
@@ -20,11 +23,11 @@ test("Cassandra keyspace and table CRUD", async () => {
|
||||
await explorer.fill('[aria-label="addCollection-table Id Create table"]', tableId);
|
||||
await explorer.click("#sidePanelOkButton");
|
||||
await explorer.click(`.nodeItem >> text=${keyspaceId}`);
|
||||
await explorer.click(`[data-test="${tableId}"] [aria-label="More"]`);
|
||||
await explorer.click(`[data-test="${tableId}"] [aria-label="More options"]`);
|
||||
await explorer.click('button[role="menuitem"]:has-text("Delete Table")');
|
||||
await explorer.fill('text=* Confirm by typing the table id >> input[type="text"]', tableId);
|
||||
await explorer.click('[aria-label="OK"]');
|
||||
await explorer.click(`[data-test="${keyspaceId}"] [aria-label="More"]`);
|
||||
await explorer.click(`[data-test="${keyspaceId}"] [aria-label="More options"]`);
|
||||
await explorer.click('button[role="menuitem"]:has-text("Delete Keyspace")');
|
||||
await explorer.click('text=* Confirm by typing the database id >> input[type="text"]');
|
||||
await explorer.fill('text=* Confirm by typing the database id >> input[type="text"]', keyspaceId);
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import { jest } from "@jest/globals";
|
||||
import "expect-playwright";
|
||||
import { generateDatabaseNameWithTimestamp, generateUniqueName } from "../utils/shared";
|
||||
import { generateDatabaseNameWithTimestamp, generateUniqueName, getAzureCLICredentialsToken } from "../utils/shared";
|
||||
import { waitForExplorer } from "../utils/waitForExplorer";
|
||||
jest.setTimeout(240000);
|
||||
|
||||
test("Graph CRUD", async () => {
|
||||
const databaseId = generateDatabaseNameWithTimestamp();
|
||||
const containerId = generateUniqueName("container");
|
||||
|
||||
// We can't retrieve AZ CLI credentials from the browser so we get them here.
|
||||
const token = await getAzureCLICredentialsToken();
|
||||
page.setDefaultTimeout(50000);
|
||||
|
||||
await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-gremlin-runner");
|
||||
await page.goto(`https://localhost:1234/testExplorer.html?accountName=portal-gremlin-runner&token=${token}`);
|
||||
const explorer = await waitForExplorer();
|
||||
|
||||
// Create new database and graph
|
||||
@@ -21,11 +24,11 @@ test("Graph CRUD", async () => {
|
||||
await explorer.click(`.nodeItem >> text=${databaseId}`);
|
||||
await explorer.click(`.nodeItem >> text=${containerId}`);
|
||||
// Delete database and graph
|
||||
await explorer.click(`[data-test="${containerId}"] [aria-label="More"]`);
|
||||
await explorer.click(`[data-test="${containerId}"] [aria-label="More options"]`);
|
||||
await explorer.click('button[role="menuitem"]:has-text("Delete Graph")');
|
||||
await explorer.fill('text=* Confirm by typing the graph id >> input[type="text"]', containerId);
|
||||
await explorer.click('[aria-label="OK"]');
|
||||
await explorer.click(`[data-test="${databaseId}"] [aria-label="More"]`);
|
||||
await explorer.click(`[data-test="${databaseId}"] [aria-label="More options"]`);
|
||||
await explorer.click('button[role="menuitem"]:has-text("Delete Database")');
|
||||
await explorer.click('text=* Confirm by typing the database id >> input[type="text"]');
|
||||
await explorer.fill('text=* Confirm by typing the database id >> input[type="text"]', databaseId);
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import { jest } from "@jest/globals";
|
||||
import "expect-playwright";
|
||||
import { generateDatabaseNameWithTimestamp, generateUniqueName } from "../utils/shared";
|
||||
import { generateDatabaseNameWithTimestamp, generateUniqueName, getAzureCLICredentialsToken } from "../utils/shared";
|
||||
import { waitForExplorer } from "../utils/waitForExplorer";
|
||||
jest.setTimeout(240000);
|
||||
|
||||
test("Mongo CRUD", async () => {
|
||||
const databaseId = generateDatabaseNameWithTimestamp();
|
||||
const containerId = generateUniqueName("container");
|
||||
|
||||
// We can't retrieve AZ CLI credentials from the browser so we get them here.
|
||||
const token = await getAzureCLICredentialsToken();
|
||||
page.setDefaultTimeout(50000);
|
||||
|
||||
await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-mongo-runner");
|
||||
await page.goto(`https://localhost:1234/testExplorer.html?accountName=portal-mongo-runner&token=${token}`);
|
||||
const explorer = await waitForExplorer();
|
||||
|
||||
// Create new database and collection
|
||||
@@ -32,11 +35,11 @@ test("Mongo CRUD", async () => {
|
||||
await explorer.click('[aria-label="Delete index Button"]');
|
||||
await explorer.click('[data-test="Save"]');
|
||||
// Delete database and collection
|
||||
await explorer.click(`[data-test="${containerId}"] [aria-label="More"]`);
|
||||
await explorer.click(`[data-test="${containerId}"] [aria-label="More options"]`);
|
||||
await explorer.click('button[role="menuitem"]:has-text("Delete Collection")');
|
||||
await explorer.fill('text=* Confirm by typing the collection id >> input[type="text"]', containerId);
|
||||
await explorer.click('[aria-label="OK"]');
|
||||
await explorer.click(`[data-test="${databaseId}"] [aria-label="More"]`);
|
||||
await explorer.click(`[data-test="${databaseId}"] [aria-label="More options"]`);
|
||||
await explorer.click('button[role="menuitem"]:has-text("Delete Database")');
|
||||
await explorer.click('text=* Confirm by typing the database id >> input[type="text"]');
|
||||
await explorer.fill('text=* Confirm by typing the database id >> input[type="text"]', databaseId);
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import { jest } from "@jest/globals";
|
||||
import "expect-playwright";
|
||||
import { generateDatabaseNameWithTimestamp, generateUniqueName } from "../utils/shared";
|
||||
import { generateDatabaseNameWithTimestamp, generateUniqueName, getAzureCLICredentialsToken } from "../utils/shared";
|
||||
import { waitForExplorer } from "../utils/waitForExplorer";
|
||||
jest.setTimeout(240000);
|
||||
|
||||
test("Mongo CRUD", async () => {
|
||||
const databaseId = generateDatabaseNameWithTimestamp();
|
||||
const containerId = generateUniqueName("container");
|
||||
|
||||
// We can't retrieve AZ CLI credentials from the browser so we get them here.
|
||||
const token = await getAzureCLICredentialsToken();
|
||||
page.setDefaultTimeout(50000);
|
||||
|
||||
await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-mongo32-runner");
|
||||
await page.goto(`https://localhost:1234/testExplorer.html?accountName=portal-mongo32-runner&token=${token}`);
|
||||
const explorer = await waitForExplorer();
|
||||
|
||||
// Create new database and collection
|
||||
@@ -21,11 +24,11 @@ test("Mongo CRUD", async () => {
|
||||
explorer.click(`.nodeItem >> text=${databaseId}`);
|
||||
explorer.click(`.nodeItem >> text=${containerId}`);
|
||||
// Delete database and collection
|
||||
explorer.click(`[data-test="${containerId}"] [aria-label="More"]`);
|
||||
explorer.click(`[data-test="${containerId}"] [aria-label="More options"]`);
|
||||
explorer.click('button[role="menuitem"]:has-text("Delete Collection")');
|
||||
await explorer.fill('text=* Confirm by typing the collection id >> input[type="text"]', containerId);
|
||||
await explorer.click('[aria-label="OK"]');
|
||||
await explorer.click(`[data-test="${databaseId}"] [aria-label="More"]`);
|
||||
await explorer.click(`[data-test="${databaseId}"] [aria-label="More options"]`);
|
||||
await explorer.click('button[role="menuitem"]:has-text("Delete Database")');
|
||||
await explorer.click('text=* Confirm by typing the database id >> input[type="text"]');
|
||||
await explorer.fill('text=* Confirm by typing the database id >> input[type="text"]', databaseId);
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { getAzureCLICredentialsToken } from "../utils/shared";
|
||||
|
||||
test("Self Serve", async () => {
|
||||
await page.goto("https://localhost:1234/testExplorer.html?iframeSrc=selfServe.html");
|
||||
// We can't retrieve AZ CLI credentials from the browser so we get them here.
|
||||
const token = await getAzureCLICredentialsToken();
|
||||
|
||||
await page.goto(`https://localhost:1234/testExplorer.html?iframeSrc=selfServe.html&token=${token}`);
|
||||
const handle = await page.waitForSelector("iframe");
|
||||
const frame = await handle.contentFrame();
|
||||
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import { jest } from "@jest/globals";
|
||||
import "expect-playwright";
|
||||
import { generateUniqueName } from "../utils/shared";
|
||||
import { generateUniqueName, getAzureCLICredentialsToken } from "../utils/shared";
|
||||
import { waitForExplorer } from "../utils/waitForExplorer";
|
||||
jest.setTimeout(120000);
|
||||
|
||||
test("SQL CRUD", async () => {
|
||||
const databaseId = generateUniqueName("db");
|
||||
const containerId = generateUniqueName("container");
|
||||
|
||||
// We can't retrieve AZ CLI credentials from the browser so we get them here.
|
||||
const token = await getAzureCLICredentialsToken();
|
||||
page.setDefaultTimeout(50000);
|
||||
|
||||
await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-sql-runner-west-us");
|
||||
await page.goto(`https://localhost:1234/testExplorer.html?accountName=portal-sql-runner-west-us&token=${token}`);
|
||||
const explorer = await waitForExplorer();
|
||||
|
||||
await explorer.click('[data-test="New Container"]');
|
||||
@@ -18,11 +21,11 @@ test("SQL CRUD", async () => {
|
||||
await explorer.fill('[aria-label="Partition key"]', "/pk");
|
||||
await explorer.click("#sidePanelOkButton");
|
||||
await explorer.click(`.nodeItem >> text=${databaseId}`);
|
||||
await explorer.click(`[data-test="${containerId}"] [aria-label="More"]`);
|
||||
await explorer.click(`[data-test="${containerId}"] [aria-label="More options"]`);
|
||||
await explorer.click('button[role="menuitem"]:has-text("Delete Container")');
|
||||
await explorer.fill('text=* Confirm by typing the container id >> input[type="text"]', containerId);
|
||||
await explorer.click('[aria-label="OK"]');
|
||||
await explorer.click(`[data-test="${databaseId}"] [aria-label="More"]`);
|
||||
await explorer.click(`[data-test="${databaseId}"] [aria-label="More options"]`);
|
||||
await explorer.click('button[role="menuitem"]:has-text("Delete Database")');
|
||||
await explorer.click('text=* Confirm by typing the database id >> input[type="text"]');
|
||||
await explorer.fill('text=* Confirm by typing the database id >> input[type="text"]', databaseId);
|
||||
|
||||
@@ -1,19 +1,15 @@
|
||||
import { CosmosDBManagementClient } from "@azure/arm-cosmosdb";
|
||||
import { CosmosClient, PermissionMode } from "@azure/cosmos";
|
||||
import * as msRestNodeAuth from "@azure/ms-rest-nodeauth";
|
||||
import { jest } from "@jest/globals";
|
||||
import "expect-playwright";
|
||||
import { generateUniqueName } from "../utils/shared";
|
||||
import { generateUniqueName, getAzureCLICredentials } from "../utils/shared";
|
||||
jest.setTimeout(120000);
|
||||
|
||||
const clientId = "fd8753b0-0707-4e32-84e9-2532af865fb4";
|
||||
const secret = process.env["NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET"];
|
||||
const tenantId = "72f988bf-86f1-41af-91ab-2d7cd011db47";
|
||||
const subscriptionId = "69e02f2d-f059-4409-9eac-97e8a276ae2c";
|
||||
const subscriptionId = process.env["AZURE_SUBSCRIPTION_ID"] ?? "";
|
||||
const resourceGroupName = "runners";
|
||||
|
||||
test("Resource token", async () => {
|
||||
const credentials = await msRestNodeAuth.loginWithServicePrincipalSecret(clientId, secret, tenantId);
|
||||
const credentials = await getAzureCLICredentials();
|
||||
const armClient = new CosmosDBManagementClient(credentials, subscriptionId);
|
||||
const account = await armClient.databaseAccounts.get(resourceGroupName, "portal-sql-runner-west-us");
|
||||
const keys = await armClient.databaseAccounts.listKeys(resourceGroupName, "portal-sql-runner-west-us");
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user