mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-01-06 11:11:23 +00:00
Compare commits
45 Commits
users/boge
...
accessibil
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9bb07f5098 | ||
|
|
cb15ef6f22 | ||
|
|
14e5efcebf | ||
|
|
5c3f18f5f8 | ||
|
|
6ebc48ad28 | ||
|
|
298197b1b8 | ||
|
|
81a5b7cb6d | ||
|
|
b023250e67 | ||
|
|
92246144f7 | ||
|
|
a08415e7bc | ||
|
|
b94ce28e96 | ||
|
|
f8f7ea34bd | ||
|
|
cbd5e6bf76 | ||
|
|
618c5ec0fe | ||
|
|
afc82845b5 | ||
|
|
f4bcee5461 | ||
|
|
17207624a9 | ||
|
|
d36e511b18 | ||
|
|
c1a28793ba | ||
|
|
acf5acfdb4 | ||
|
|
7b81767ded | ||
|
|
c12eced120 | ||
|
|
2b15a4d43d | ||
|
|
c220a8b070 | ||
|
|
a5a5a95973 | ||
|
|
e3fab9b5bf | ||
|
|
98000a27f0 | ||
|
|
af664326ea | ||
|
|
a44ed1f45c | ||
|
|
e0cb3da6aa | ||
|
|
6c9673975a | ||
|
|
d35e2a325e | ||
|
|
00a816c488 | ||
|
|
953bef404b | ||
|
|
dfcb771939 | ||
|
|
6925fa8e4e | ||
|
|
7f6338b68b | ||
|
|
db50f42832 | ||
|
|
f533eeb0fc | ||
|
|
3c5d899e47 | ||
|
|
b44778b00a | ||
|
|
1464745659 | ||
|
|
18cc2a4195 | ||
|
|
86f2bc171f | ||
|
|
cabedf4a29 |
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:
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -16,5 +16,4 @@ Contracts/*
|
||||
.env
|
||||
failure.png
|
||||
screenshots/*
|
||||
GettingStarted-ignore*.ipynb
|
||||
.vscode/
|
||||
GettingStarted-ignore*.ipynb
|
||||
@@ -76,6 +76,10 @@ module.exports = {
|
||||
"^dnd-core$": "dnd-core/dist/cjs",
|
||||
"^react-dnd$": "react-dnd/dist/cjs",
|
||||
"^react-dnd-html5-backend$": "react-dnd-html5-backend/dist/cjs",
|
||||
"d3-force": "<rootDir>/node_modules/d3-force/dist/d3-force.min.js",
|
||||
"d3-quadtree": "<rootDir>/node_modules/d3-quadtree/dist/d3-quadtree.min.js",
|
||||
"d3-scale-chromatic": "<rootDir>/node_modules/d3-scale-chromatic/dist/d3-scale-chromatic.min.js",
|
||||
"d3-zoom": "<rootDir>/node_modules/d3-zoom/dist/d3-zoom.min.js",
|
||||
},
|
||||
|
||||
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
|
||||
@@ -130,7 +134,6 @@ module.exports = {
|
||||
|
||||
// The test environment that will be used for testing
|
||||
// testEnvironment: "jest-environment-jsdom",
|
||||
|
||||
modulePaths: ["node_modules", "<rootDir>/src"],
|
||||
|
||||
// Options that will be passed to the testEnvironment
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
1902
package-lock.json
generated
1902
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
21
package.json
21
package.json
@@ -7,8 +7,8 @@
|
||||
"@azure/arm-cosmosdb": "9.1.0",
|
||||
"@azure/cosmos": "4.0.1-beta.2",
|
||||
"@azure/cosmos-language-service": "0.0.5",
|
||||
"@azure/identity": "1.2.1",
|
||||
"@azure/ms-rest-nodeauth": "3.0.7",
|
||||
"@azure/identity": "1.5.2",
|
||||
"@azure/ms-rest-nodeauth": "3.1.1",
|
||||
"@azure/msal-browser": "2.14.2",
|
||||
"@babel/plugin-proposal-class-properties": "7.12.1",
|
||||
"@babel/plugin-proposal-decorators": "7.12.12",
|
||||
@@ -46,6 +46,7 @@
|
||||
"@types/lodash": "4.14.171",
|
||||
"@types/mkdirp": "1.0.1",
|
||||
"@types/node-fetch": "2.5.7",
|
||||
"@xmldom/xmldom": "0.7.13",
|
||||
"applicationinsights": "1.8.0",
|
||||
"bootstrap": "3.4.1",
|
||||
"canvas": "file:./canvas",
|
||||
@@ -54,7 +55,7 @@
|
||||
"copy-webpack-plugin": "11.0.0",
|
||||
"crossroads": "0.12.2",
|
||||
"css-element-queries": "1.1.1",
|
||||
"d3": "6.1.1",
|
||||
"d3": "7.8.5",
|
||||
"datatables.net-colreorder-dt": "1.7.0",
|
||||
"datatables.net-dt": "1.13.8",
|
||||
"date-fns": "1.29.0",
|
||||
@@ -69,12 +70,14 @@
|
||||
"i18next-browser-languagedetector": "6.0.1",
|
||||
"i18next-http-backend": "1.0.23",
|
||||
"iframe-resizer-react": "1.1.0",
|
||||
"immer": "9.0.6",
|
||||
"immutable": "4.0.0-rc.12",
|
||||
"is-ci": "2.0.0",
|
||||
"jquery": "3.7.1",
|
||||
"jquery-typeahead": "2.11.1",
|
||||
"jquery-ui-dist": "1.13.2",
|
||||
"knockout": "3.5.1",
|
||||
"loader-utils": "2.0.3",
|
||||
"mkdirp": "1.0.4",
|
||||
"monaco-editor": "0.44.0",
|
||||
"ms": "2.1.3",
|
||||
@@ -98,10 +101,12 @@
|
||||
"reflect-metadata": "0.1.13",
|
||||
"rx-jupyter": "5.5.12",
|
||||
"sanitize-html": "2.3.3",
|
||||
"shell-quote": "1.7.3",
|
||||
"styled-components": "5.0.1",
|
||||
"swr": "0.4.0",
|
||||
"terser-webpack-plugin": "5.3.9",
|
||||
"underscore": "1.9.1",
|
||||
"tinykeys": "2.1.0",
|
||||
"underscore": "1.12.1",
|
||||
"utility-types": "3.10.0",
|
||||
"zustand": "3.5.0"
|
||||
},
|
||||
@@ -170,25 +175,25 @@
|
||||
"less-vars-loader": "1.1.0",
|
||||
"mini-css-extract-plugin": "2.1.0",
|
||||
"monaco-editor-webpack-plugin": "7.1.0",
|
||||
"node-fetch": "2.6.1",
|
||||
"node-fetch": "2.6.7",
|
||||
"playwright": "1.13.0",
|
||||
"prettier": "3.0.3",
|
||||
"process": "0.11.10",
|
||||
"querystring-es3": "0.2.1",
|
||||
"raw-loader": "0.5.1",
|
||||
"react-dev-utils": "11.0.4",
|
||||
"react-dev-utils": "12.0.1",
|
||||
"rimraf": "3.0.0",
|
||||
"sinon": "3.2.1",
|
||||
"style-loader": "0.23.0",
|
||||
"ts-loader": "9.2.4",
|
||||
"typedoc": "0.21.5",
|
||||
"typedoc": "0.22.15",
|
||||
"typescript": "4.3.5",
|
||||
"url-loader": "4.1.1",
|
||||
"wait-on": "4.0.2",
|
||||
"webpack": "5.88.2",
|
||||
"webpack-bundle-analyzer": "4.9.1",
|
||||
"webpack-cli": "5.1.4",
|
||||
"webpack-dev-server": "4.15.1"
|
||||
"webpack-dev-server": "4.15.2"
|
||||
},
|
||||
"scripts": {
|
||||
"postinstall": "patch-package",
|
||||
|
||||
@@ -124,8 +124,9 @@ export enum MongoBackendEndpointType {
|
||||
remote,
|
||||
}
|
||||
|
||||
export enum BackendApi {
|
||||
GenerateToken,
|
||||
export class BackendApi {
|
||||
public static readonly GenerateToken: string = "GenerateToken";
|
||||
public static readonly PortalSettings: string = "PortalSettings";
|
||||
}
|
||||
|
||||
export class PortalBackendEndpoints {
|
||||
@@ -137,7 +138,7 @@ export class PortalBackendEndpoints {
|
||||
}
|
||||
|
||||
export class MongoProxyEndpoints {
|
||||
public static readonly Development: string = "https://localhost:7238";
|
||||
public static readonly Local: 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";
|
||||
|
||||
@@ -85,8 +85,6 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
|
||||
return decodeURIComponent(result.PrimaryReadWriteToken);
|
||||
};
|
||||
|
||||
// TODO
|
||||
// Need to create separate plugins or change endpoint logic to return the correct write or read endpoint.
|
||||
export const requestPlugin: Cosmos.Plugin<any> = async (requestContext, diagnosticNode, next) => {
|
||||
requestContext.endpoint = new URL(configContext.PROXY_PATH, window.location.href).href;
|
||||
requestContext.headers["x-ms-proxy-target"] = endpoint();
|
||||
@@ -136,32 +134,9 @@ enum SDKSupportedCapabilities {
|
||||
PartitionMerge = 1 << 0,
|
||||
}
|
||||
|
||||
// Client Management
|
||||
|
||||
let _client: Cosmos.CosmosClient;
|
||||
let _readClients: Map<string, Cosmos.CosmosClient> = new Map();
|
||||
|
||||
export enum ClientOperationType {
|
||||
READ,
|
||||
WRITE,
|
||||
}
|
||||
|
||||
export function client(clientOperationType: ClientOperationType): Cosmos.CosmosClient {
|
||||
switch (clientOperationType) {
|
||||
case ClientOperationType.READ:
|
||||
return readClients();
|
||||
case ClientOperationType.WRITE:
|
||||
return writeClient();
|
||||
default:
|
||||
throw new Error("Invalid operation type");
|
||||
}
|
||||
}
|
||||
|
||||
export function writeClient(): Cosmos.CosmosClient {
|
||||
console.log(`Called primary client`);
|
||||
const currentUserContextDocumentEndpoint = userContext?.databaseAccount?.properties?.documentEndpoint;
|
||||
console.log(`Current selected endpoint in userContext: ${currentUserContextDocumentEndpoint}`);
|
||||
|
||||
export function client(): Cosmos.CosmosClient {
|
||||
if (_client) return _client;
|
||||
|
||||
let _defaultHeaders: Cosmos.CosmosHeaders = {};
|
||||
@@ -208,61 +183,3 @@ export function writeClient(): Cosmos.CosmosClient {
|
||||
_client = new Cosmos.CosmosClient(options);
|
||||
return _client;
|
||||
}
|
||||
|
||||
export function readClients(): Cosmos.CosmosClient {
|
||||
console.log(`Called read only client`);
|
||||
const currentUserContextDocumentEndpoint = userContext?.databaseAccount?.properties?.documentEndpoint;
|
||||
console.log(`Current selected read endpoint in userContext: ${currentUserContextDocumentEndpoint}`);
|
||||
3;
|
||||
|
||||
const selectedEndpoint = endpoint() || "https://cosmos.azure.com";
|
||||
|
||||
if (_readClients.has(selectedEndpoint)) {
|
||||
return _readClients.get(selectedEndpoint);
|
||||
}
|
||||
|
||||
let _defaultHeaders: Cosmos.CosmosHeaders = {};
|
||||
_defaultHeaders["x-ms-cosmos-sdk-supportedcapabilities"] =
|
||||
SDKSupportedCapabilities.None | SDKSupportedCapabilities.PartitionMerge;
|
||||
|
||||
if (
|
||||
userContext.authType === AuthType.ConnectionString ||
|
||||
userContext.authType === AuthType.EncryptedToken ||
|
||||
userContext.authType === AuthType.ResourceToken
|
||||
) {
|
||||
// Default to low priority. Needed for non-AAD-auth scenarios
|
||||
// where we cannot use RP API, and thus, cannot detect whether priority
|
||||
// based execution is enabled.
|
||||
// The header will be ignored if priority based execution is disabled on the account.
|
||||
_defaultHeaders["x-ms-cosmos-priority-level"] = PriorityLevel.Default;
|
||||
}
|
||||
|
||||
const options: Cosmos.CosmosClientOptions = {
|
||||
endpoint: selectedEndpoint, // CosmosClient gets upset if we pass a bad URL. This should never actually get called
|
||||
key: userContext.masterKey,
|
||||
tokenProvider,
|
||||
userAgentSuffix: "Azure Portal",
|
||||
defaultHeaders: _defaultHeaders,
|
||||
connectionPolicy: {
|
||||
retryOptions: {
|
||||
maxRetryAttemptCount: LocalStorageUtility.getEntryNumber(StorageKey.RetryAttempts),
|
||||
fixedRetryIntervalInMilliseconds: LocalStorageUtility.getEntryNumber(StorageKey.RetryInterval),
|
||||
maxWaitTimeInSeconds: LocalStorageUtility.getEntryNumber(StorageKey.MaxWaitTimeInSeconds),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
if (configContext.PROXY_PATH !== undefined) {
|
||||
(options as any).plugins = [{ on: "request", plugin: requestPlugin }];
|
||||
}
|
||||
|
||||
if (PriorityBasedExecutionUtils.isFeatureEnabled()) {
|
||||
const plugins = (options as any).plugins || [];
|
||||
plugins.push({ on: "request", plugin: PriorityBasedExecutionUtils.requestPlugin });
|
||||
(options as any).plugins = plugins;
|
||||
}
|
||||
|
||||
_readClients.set(selectedEndpoint, new Cosmos.CosmosClient(options));
|
||||
|
||||
return _readClients.get(selectedEndpoint);
|
||||
}
|
||||
|
||||
@@ -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}`, {
|
||||
@@ -672,6 +672,28 @@ export function getEndpoint(endpoint: string): string {
|
||||
return url;
|
||||
}
|
||||
|
||||
export function useMongoProxyEndpoint(api: string): boolean {
|
||||
const activeMongoProxyEndpoints: string[] = [
|
||||
MongoProxyEndpoints.Local,
|
||||
MongoProxyEndpoints.Mpac,
|
||||
MongoProxyEndpoints.Prod,
|
||||
MongoProxyEndpoints.Fairfax,
|
||||
];
|
||||
let canAccessMongoProxy: boolean = userContext.databaseAccount.properties.publicNetworkAccess === "Enabled";
|
||||
if (
|
||||
configContext.MONGO_PROXY_ENDPOINT !== MongoProxyEndpoints.Local &&
|
||||
userContext.databaseAccount.properties.ipRules?.length > 0
|
||||
) {
|
||||
canAccessMongoProxy = canAccessMongoProxy && configContext.MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED;
|
||||
}
|
||||
|
||||
return (
|
||||
canAccessMongoProxy &&
|
||||
configContext.NEW_MONGO_APIS?.includes(api) &&
|
||||
activeMongoProxyEndpoints.includes(configContext.MONGO_PROXY_ENDPOINT)
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: This function throws most of the time except on Forbidden which is a bit strange
|
||||
// It causes problems for TypeScript understanding the types
|
||||
async function errorHandling(response: Response, action: string, params: unknown): Promise<void> {
|
||||
@@ -688,17 +710,3 @@ async function errorHandling(response: Response, action: string, params: unknown
|
||||
export function getARMCreateCollectionEndpoint(params: DataModels.MongoParameters): string {
|
||||
return `subscriptions/${params.sid}/resourceGroups/${params.rg}/providers/Microsoft.DocumentDB/databaseAccounts/${userContext.databaseAccount.name}/mongodbDatabases/${params.db}/collections/${params.coll}`;
|
||||
}
|
||||
|
||||
function useMongoProxyEndpoint(api: string): boolean {
|
||||
const activeMongoProxyEndpoints: string[] = [MongoProxyEndpoints.Development];
|
||||
let canAccessMongoProxy: boolean = userContext.databaseAccount.properties.publicNetworkAccess === "Enabled";
|
||||
if (userContext.databaseAccount.properties.ipRules?.length > 0) {
|
||||
canAccessMongoProxy = canAccessMongoProxy && configContext.MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED;
|
||||
}
|
||||
|
||||
return (
|
||||
canAccessMongoProxy &&
|
||||
configContext.NEW_MONGO_APIS?.includes(api) &&
|
||||
activeMongoProxyEndpoints.includes(configContext.MONGO_PROXY_ENDPOINT)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -142,7 +142,7 @@ export const TableEntity: FunctionComponent<TableEntityProps> = ({
|
||||
<Image
|
||||
{...imageProps}
|
||||
src={EditIcon}
|
||||
alt="editEntity"
|
||||
alt={`Edit ${entityProperty} entity`}
|
||||
onClick={onEditEntity}
|
||||
tabIndex={0}
|
||||
onKeyPress={handleKeyPress}
|
||||
@@ -156,7 +156,7 @@ export const TableEntity: FunctionComponent<TableEntityProps> = ({
|
||||
<Image
|
||||
{...imageProps}
|
||||
src={DeleteIcon}
|
||||
alt="delete entity"
|
||||
alt={`Delete ${entityProperty} entity`}
|
||||
id="deleteEntity"
|
||||
onClick={onDeleteEntity}
|
||||
tabIndex={0}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { JSONObject, OperationResponse } from "@azure/cosmos";
|
||||
import { CollectionBase } from "../../Contracts/ViewModels";
|
||||
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { ClientOperationType, client } from "../CosmosClient";
|
||||
import { client } from "../CosmosClient";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
|
||||
export const bulkCreateDocument = async (
|
||||
@@ -13,7 +13,7 @@ export const bulkCreateDocument = async (
|
||||
);
|
||||
|
||||
try {
|
||||
const response = await client(ClientOperationType.WRITE)
|
||||
const response = await client()
|
||||
.database(collection.databaseId)
|
||||
.container(collection.id())
|
||||
.items.bulk(
|
||||
|
||||
@@ -6,14 +6,14 @@ import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstan
|
||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { getCollectionName } from "../../Utils/APITypeUtils";
|
||||
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { createUpdateCassandraTable } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
|
||||
import { createUpdateGremlinGraph } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
|
||||
import { createUpdateMongoDBCollection } from "../../Utils/arm/generatedClients/cosmos/mongoDBResources";
|
||||
import { createUpdateSqlContainer } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
|
||||
import { createUpdateTable } from "../../Utils/arm/generatedClients/cosmos/tableResources";
|
||||
import * as ARMTypes from "../../Utils/arm/generatedClients/cosmos/types";
|
||||
import { ClientOperationType, client } from "../CosmosClient";
|
||||
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { client } from "../CosmosClient";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
import { createMongoCollectionWithProxy } from "../MongoProxyClient";
|
||||
import { createDatabase } from "./createDatabase";
|
||||
@@ -284,9 +284,7 @@ const createCollectionWithSDK = async (params: DataModels.CreateCollectionParams
|
||||
}
|
||||
}
|
||||
|
||||
const databaseResponse: DatabaseResponse = await client(ClientOperationType.WRITE).databases.createIfNotExists(
|
||||
createDatabaseBody,
|
||||
);
|
||||
const databaseResponse: DatabaseResponse = await client().databases.createIfNotExists(createDatabaseBody);
|
||||
const collectionResponse: ContainerResponse = await databaseResponse?.database.containers.create(
|
||||
createCollectionBody,
|
||||
collectionOptions,
|
||||
|
||||
@@ -4,7 +4,6 @@ import * as DataModels from "../../Contracts/DataModels";
|
||||
import { useDatabases } from "../../Explorer/useDatabases";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { getDatabaseName } from "../../Utils/APITypeUtils";
|
||||
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { createUpdateCassandraKeyspace } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
|
||||
import { createUpdateGremlinDatabase } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
|
||||
import { createUpdateMongoDBDatabase } from "../../Utils/arm/generatedClients/cosmos/mongoDBResources";
|
||||
@@ -16,7 +15,8 @@ import {
|
||||
MongoDBDatabaseCreateUpdateParameters,
|
||||
SqlDatabaseCreateUpdateParameters,
|
||||
} from "../../Utils/arm/generatedClients/cosmos/types";
|
||||
import { ClientOperationType, client } from "../CosmosClient";
|
||||
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { client } from "../CosmosClient";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
|
||||
export async function createDatabase(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> {
|
||||
@@ -153,7 +153,7 @@ async function createDatabaseWithSDK(params: DataModels.CreateDatabaseParams): P
|
||||
}
|
||||
}
|
||||
|
||||
const response: DatabaseResponse = await client(ClientOperationType.WRITE).databases.create(createBody);
|
||||
const response: DatabaseResponse = await client().databases.create(createBody);
|
||||
return response.resource;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { CollectionBase } from "../../Contracts/ViewModels";
|
||||
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { ClientOperationType, client } from "../CosmosClient";
|
||||
import { client } from "../CosmosClient";
|
||||
import { getEntityName } from "../DocumentUtility";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
|
||||
@@ -9,7 +9,7 @@ export const createDocument = async (collection: CollectionBase, newDocument: un
|
||||
const clearMessage = logConsoleProgress(`Creating new ${entityName} for container ${collection.id()}`);
|
||||
|
||||
try {
|
||||
const response = await client(ClientOperationType.WRITE)
|
||||
const response = await client()
|
||||
.database(collection.databaseId)
|
||||
.container(collection.id())
|
||||
.items.create(newDocument);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Resource, StoredProcedureDefinition } from "@azure/cosmos";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import {
|
||||
createUpdateSqlStoredProcedure,
|
||||
getSqlStoredProcedure,
|
||||
@@ -10,7 +9,8 @@ import {
|
||||
SqlStoredProcedureCreateUpdateParameters,
|
||||
SqlStoredProcedureResource,
|
||||
} from "../../Utils/arm/generatedClients/cosmos/types";
|
||||
import { ClientOperationType, client } from "../CosmosClient";
|
||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { client } from "../CosmosClient";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
|
||||
export async function createStoredProcedure(
|
||||
@@ -63,7 +63,7 @@ export async function createStoredProcedure(
|
||||
return rpResponse && (rpResponse.properties?.resource as StoredProcedureDefinition & Resource);
|
||||
}
|
||||
|
||||
const response = await client(ClientOperationType.WRITE)
|
||||
const response = await client()
|
||||
.database(databaseId)
|
||||
.container(collectionId)
|
||||
.scripts.storedProcedures.create(storedProcedure);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { TriggerDefinition } from "@azure/cosmos";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { createUpdateSqlTrigger, getSqlTrigger } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
|
||||
import { SqlTriggerCreateUpdateParameters, SqlTriggerResource } from "../../Utils/arm/generatedClients/cosmos/types";
|
||||
import { ClientOperationType, client } from "../CosmosClient";
|
||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { client } from "../CosmosClient";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
|
||||
export async function createTrigger(
|
||||
@@ -55,7 +55,7 @@ export async function createTrigger(
|
||||
return rpResponse && rpResponse.properties?.resource;
|
||||
}
|
||||
|
||||
const response = await client(ClientOperationType.WRITE)
|
||||
const response = await client()
|
||||
.database(databaseId)
|
||||
.container(collectionId)
|
||||
.scripts.triggers.create(trigger as unknown as TriggerDefinition); // TODO: TypeScript does not like the SQL SDK trigger type
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Resource, UserDefinedFunctionDefinition } from "@azure/cosmos";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import {
|
||||
createUpdateSqlUserDefinedFunction,
|
||||
getSqlUserDefinedFunction,
|
||||
@@ -10,7 +9,8 @@ import {
|
||||
SqlUserDefinedFunctionCreateUpdateParameters,
|
||||
SqlUserDefinedFunctionResource,
|
||||
} from "../../Utils/arm/generatedClients/cosmos/types";
|
||||
import { ClientOperationType, client } from "../CosmosClient";
|
||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { client } from "../CosmosClient";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
|
||||
export async function createUserDefinedFunction(
|
||||
@@ -63,7 +63,7 @@ export async function createUserDefinedFunction(
|
||||
return rpResponse && (rpResponse.properties?.resource as UserDefinedFunctionDefinition & Resource);
|
||||
}
|
||||
|
||||
const response = await client(ClientOperationType.WRITE)
|
||||
const response = await client()
|
||||
.database(databaseId)
|
||||
.container(collectionId)
|
||||
.scripts.userDefinedFunctions.create(userDefinedFunction);
|
||||
|
||||
@@ -6,7 +6,7 @@ import { deleteMongoDBCollection } from "../../Utils/arm/generatedClients/cosmos
|
||||
import { deleteSqlContainer } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
|
||||
import { deleteTable } from "../../Utils/arm/generatedClients/cosmos/tableResources";
|
||||
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { client, ClientOperationType } from "../CosmosClient";
|
||||
import { client } from "../CosmosClient";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
|
||||
export async function deleteCollection(databaseId: string, collectionId: string): Promise<void> {
|
||||
@@ -15,7 +15,7 @@ export async function deleteCollection(databaseId: string, collectionId: string)
|
||||
if (userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations) {
|
||||
await deleteCollectionWithARM(databaseId, collectionId);
|
||||
} else {
|
||||
await client(ClientOperationType.WRITE).database(databaseId).container(collectionId).delete();
|
||||
await client().database(databaseId).container(collectionId).delete();
|
||||
}
|
||||
logConsoleInfo(`Successfully deleted container ${collectionId}`);
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { RequestOptions } from "@azure/cosmos";
|
||||
import { CollectionBase } from "../../Contracts/ViewModels";
|
||||
import ConflictId from "../../Explorer/Tree/ConflictId";
|
||||
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { ClientOperationType, client } from "../CosmosClient";
|
||||
import { CollectionBase } from "../../Contracts/ViewModels";
|
||||
import { RequestOptions } from "@azure/cosmos";
|
||||
import { client } from "../CosmosClient";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
import { logConsoleProgress, logConsoleInfo } from "../../Utils/NotificationConsoleUtils";
|
||||
|
||||
export const deleteConflict = async (collection: CollectionBase, conflictId: ConflictId): Promise<void> => {
|
||||
const clearMessage = logConsoleProgress(`Deleting conflict ${conflictId.id()}`);
|
||||
@@ -13,7 +13,7 @@ export const deleteConflict = async (collection: CollectionBase, conflictId: Con
|
||||
partitionKey: getPartitionKeyHeaderForConflict(conflictId),
|
||||
};
|
||||
|
||||
await client(ClientOperationType.WRITE)
|
||||
await client()
|
||||
.database(collection.databaseId)
|
||||
.container(collection.id())
|
||||
.conflict(conflictId.id())
|
||||
|
||||
@@ -5,7 +5,7 @@ import { deleteGremlinDatabase } from "../../Utils/arm/generatedClients/cosmos/g
|
||||
import { deleteMongoDBDatabase } from "../../Utils/arm/generatedClients/cosmos/mongoDBResources";
|
||||
import { deleteSqlDatabase } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
|
||||
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { client, ClientOperationType } from "../CosmosClient";
|
||||
import { client } from "../CosmosClient";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
|
||||
export async function deleteDatabase(databaseId: string): Promise<void> {
|
||||
@@ -15,7 +15,7 @@ export async function deleteDatabase(databaseId: string): Promise<void> {
|
||||
if (userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations) {
|
||||
await deleteDatabaseWithARM(databaseId);
|
||||
} else {
|
||||
await client(ClientOperationType.WRITE).database(databaseId).delete();
|
||||
await client().database(databaseId).delete();
|
||||
}
|
||||
logConsoleInfo(`Successfully deleted database ${databaseId}`);
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { CollectionBase } from "../../Contracts/ViewModels";
|
||||
import DocumentId from "../../Explorer/Tree/DocumentId";
|
||||
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { ClientOperationType, client } from "../CosmosClient";
|
||||
import { client } from "../CosmosClient";
|
||||
import { getEntityName } from "../DocumentUtility";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
import { getPartitionKeyValue } from "./getPartitionKeyValue";
|
||||
@@ -11,7 +11,7 @@ export const deleteDocument = async (collection: CollectionBase, documentId: Doc
|
||||
const clearMessage = logConsoleProgress(`Deleting ${entityName} ${documentId.id()}`);
|
||||
|
||||
try {
|
||||
await client(ClientOperationType.WRITE)
|
||||
await client()
|
||||
.database(collection.databaseId)
|
||||
.container(collection.id())
|
||||
.item(documentId.id(), getPartitionKeyValue(documentId))
|
||||
|
||||
@@ -2,7 +2,7 @@ import { AuthType } from "../../AuthType";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { deleteSqlStoredProcedure } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
|
||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { client, ClientOperationType } from "../CosmosClient";
|
||||
import { client } from "../CosmosClient";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
|
||||
export async function deleteStoredProcedure(
|
||||
@@ -26,11 +26,7 @@ export async function deleteStoredProcedure(
|
||||
storedProcedureId,
|
||||
);
|
||||
} else {
|
||||
await client(ClientOperationType.WRITE)
|
||||
.database(databaseId)
|
||||
.container(collectionId)
|
||||
.scripts.storedProcedure(storedProcedureId)
|
||||
.delete();
|
||||
await client().database(databaseId).container(collectionId).scripts.storedProcedure(storedProcedureId).delete();
|
||||
}
|
||||
} catch (error) {
|
||||
handleError(error, "DeleteStoredProcedure", `Error while deleting stored procedure ${storedProcedureId}`);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { AuthType } from "../../AuthType";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { deleteSqlTrigger } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
|
||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { client, ClientOperationType } from "../CosmosClient";
|
||||
import { client } from "../CosmosClient";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
|
||||
export async function deleteTrigger(databaseId: string, collectionId: string, triggerId: string): Promise<void> {
|
||||
@@ -22,11 +22,7 @@ export async function deleteTrigger(databaseId: string, collectionId: string, tr
|
||||
triggerId,
|
||||
);
|
||||
} else {
|
||||
await client(ClientOperationType.WRITE)
|
||||
.database(databaseId)
|
||||
.container(collectionId)
|
||||
.scripts.trigger(triggerId)
|
||||
.delete();
|
||||
await client().database(databaseId).container(collectionId).scripts.trigger(triggerId).delete();
|
||||
}
|
||||
} catch (error) {
|
||||
handleError(error, "DeleteTrigger", `Error while deleting trigger ${triggerId}`);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { AuthType } from "../../AuthType";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { deleteSqlUserDefinedFunction } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
|
||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { client, ClientOperationType } from "../CosmosClient";
|
||||
import { client } from "../CosmosClient";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
|
||||
export async function deleteUserDefinedFunction(databaseId: string, collectionId: string, id: string): Promise<void> {
|
||||
@@ -22,11 +22,7 @@ export async function deleteUserDefinedFunction(databaseId: string, collectionId
|
||||
id,
|
||||
);
|
||||
} else {
|
||||
await client(ClientOperationType.WRITE)
|
||||
.database(databaseId)
|
||||
.container(collectionId)
|
||||
.scripts.userDefinedFunction(id)
|
||||
.delete();
|
||||
await client().database(databaseId).container(collectionId).scripts.userDefinedFunction(id).delete();
|
||||
}
|
||||
} catch (error) {
|
||||
handleError(error, "DeleteUserDefinedFunction", `Error while deleting user defined function ${id}`);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Collection } from "../../Contracts/ViewModels";
|
||||
import StoredProcedure from "../../Explorer/Tree/StoredProcedure";
|
||||
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { ClientDefaults, HttpHeaders } from "../Constants";
|
||||
import { ClientOperationType, client } from "../CosmosClient";
|
||||
import { client } from "../CosmosClient";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
import { logConsoleProgress, logConsoleInfo } from "../../Utils/NotificationConsoleUtils";
|
||||
import StoredProcedure from "../../Explorer/Tree/StoredProcedure";
|
||||
|
||||
export interface ExecuteSprocResult {
|
||||
result: StoredProcedure;
|
||||
@@ -22,7 +22,7 @@ export const executeStoredProcedure = async (
|
||||
}, ClientDefaults.requestTimeoutMs);
|
||||
|
||||
try {
|
||||
const response = await client(ClientOperationType.WRITE)
|
||||
const response = await client()
|
||||
.database(collection.databaseId)
|
||||
.container(collection.id())
|
||||
.scripts.storedProcedure(storedProcedure.id())
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { client } from "../CosmosClient";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import * as Constants from "../Constants";
|
||||
import { ClientOperationType, client } from "../CosmosClient";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { userContext } from "../../UserContext";
|
||||
|
||||
export async function getIndexTransformationProgress(databaseId: string, collectionId: string): Promise<number> {
|
||||
if (userContext.authType !== AuthType.AAD) {
|
||||
@@ -12,10 +12,7 @@ export async function getIndexTransformationProgress(databaseId: string, collect
|
||||
let indexTransformationPercentage: number;
|
||||
const clearMessage = logConsoleProgress(`Reading container ${collectionId}`);
|
||||
try {
|
||||
const response = await client(ClientOperationType.READ)
|
||||
.database(databaseId)
|
||||
.container(collectionId)
|
||||
.read({ populateQuotaInfo: true });
|
||||
const response = await client().database(databaseId).container(collectionId).read({ populateQuotaInfo: true });
|
||||
|
||||
indexTransformationPercentage = parseInt(
|
||||
response.headers[Constants.HttpHeaders.collectionIndexTransformationProgress] as string,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ConflictDefinition, FeedOptions, QueryIterator, Resource } from "@azure/cosmos";
|
||||
import { ClientOperationType, client } from "../CosmosClient";
|
||||
import { client } from "../CosmosClient";
|
||||
|
||||
export const queryConflicts = (
|
||||
databaseId: string,
|
||||
@@ -7,5 +7,5 @@ export const queryConflicts = (
|
||||
query: string,
|
||||
options: FeedOptions,
|
||||
): QueryIterator<ConflictDefinition & Resource> => {
|
||||
return client(ClientOperationType.READ).database(databaseId).container(containerId).conflicts.query(query, options);
|
||||
return client().database(databaseId).container(containerId).conflicts.query(query, options);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { FeedOptions, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
||||
import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility";
|
||||
import { Queries } from "../Constants";
|
||||
import { ClientOperationType, client } from "../CosmosClient";
|
||||
import { client } from "../CosmosClient";
|
||||
|
||||
export const queryDocuments = (
|
||||
databaseId: string,
|
||||
@@ -10,7 +10,7 @@ export const queryDocuments = (
|
||||
options: FeedOptions,
|
||||
): QueryIterator<ItemDefinition & Resource> => {
|
||||
options = getCommonQueryOptions(options);
|
||||
return client(ClientOperationType.READ).database(databaseId).container(containerId).items.query(query, options);
|
||||
return client().database(databaseId).container(containerId).items.query(query, options);
|
||||
};
|
||||
|
||||
export const getCommonQueryOptions = (options: FeedOptions): FeedOptions => {
|
||||
|
||||
@@ -3,11 +3,11 @@ import { sampleDataClient } from "Common/SampleDataClient";
|
||||
import { userContext } from "UserContext";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { ClientOperationType, client } from "../CosmosClient";
|
||||
import { client } from "../CosmosClient";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
|
||||
export async function readCollection(databaseId: string, collectionId: string): Promise<DataModels.Collection> {
|
||||
const cosmosClient = client(ClientOperationType.READ);
|
||||
const cosmosClient = client();
|
||||
return await readCollectionInternal(cosmosClient, databaseId, collectionId);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import { listGremlinGraphs } from "../../Utils/arm/generatedClients/cosmos/greml
|
||||
import { listMongoDBCollections } from "../../Utils/arm/generatedClients/cosmos/mongoDBResources";
|
||||
import { listSqlContainers } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
|
||||
import { listTables } from "../../Utils/arm/generatedClients/cosmos/tableResources";
|
||||
import { ClientOperationType, client } from "../CosmosClient";
|
||||
import { client } from "../CosmosClient";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
|
||||
export async function readCollections(databaseId: string): Promise<DataModels.Collection[]> {
|
||||
@@ -31,7 +31,7 @@ export async function readCollections(databaseId: string): Promise<DataModels.Co
|
||||
const tokenCollectionId = resourceIdObj[3];
|
||||
|
||||
if (tokenDatabaseId === databaseId) {
|
||||
promises.push(client(ClientOperationType.READ).database(databaseId).container(tokenCollectionId).read());
|
||||
promises.push(client().database(databaseId).container(tokenCollectionId).read());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ export async function readCollections(databaseId: string): Promise<DataModels.Co
|
||||
return await readCollectionsWithARM(databaseId);
|
||||
}
|
||||
|
||||
const sdkResponse = await client(ClientOperationType.READ).database(databaseId).containers.readAll().fetchAll();
|
||||
const sdkResponse = await client().database(databaseId).containers.readAll().fetchAll();
|
||||
return sdkResponse.resources as DataModels.Collection[];
|
||||
} catch (error) {
|
||||
handleError(error, "ReadCollections", `Error while querying containers for database ${databaseId}`);
|
||||
@@ -77,7 +77,7 @@ export async function readCollectionsWithPagination(
|
||||
): Promise<DataModels.CollectionsWithPagination> {
|
||||
const clearMessage = logConsoleProgress(`Querying containers for database ${databaseId}`);
|
||||
try {
|
||||
const sdkResponse = await client(ClientOperationType.READ)
|
||||
const sdkResponse = await client()
|
||||
.database(databaseId)
|
||||
.containers.query(
|
||||
{ query: "SELECT * FROM c" },
|
||||
|
||||
@@ -7,7 +7,7 @@ import { listCassandraKeyspaces } from "../../Utils/arm/generatedClients/cosmos/
|
||||
import { listGremlinDatabases } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
|
||||
import { listMongoDBDatabases } from "../../Utils/arm/generatedClients/cosmos/mongoDBResources";
|
||||
import { listSqlDatabases } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
|
||||
import { ClientOperationType, client } from "../CosmosClient";
|
||||
import { client } from "../CosmosClient";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
|
||||
export async function readDatabases(): Promise<DataModels.Database[]> {
|
||||
@@ -56,7 +56,7 @@ export async function readDatabases(): Promise<DataModels.Database[]> {
|
||||
) {
|
||||
databases = await readDatabasesWithARM();
|
||||
} else {
|
||||
const sdkResponse = await client(ClientOperationType.READ).databases.readAll().fetchAll();
|
||||
const sdkResponse = await client().databases.readAll().fetchAll();
|
||||
databases = sdkResponse.resources as DataModels.Database[];
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { CollectionBase } from "../../Contracts/ViewModels";
|
||||
import DocumentId from "../../Explorer/Tree/DocumentId";
|
||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { HttpHeaders } from "../Constants";
|
||||
import { ClientOperationType, client } from "../CosmosClient";
|
||||
import { client } from "../CosmosClient";
|
||||
import { getEntityName } from "../DocumentUtility";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
import { getPartitionKeyValue } from "./getPartitionKeyValue";
|
||||
@@ -19,7 +19,7 @@ export const readDocument = async (collection: CollectionBase, documentId: Docum
|
||||
[HttpHeaders.partitionKey]: documentId.partitionKeyValue,
|
||||
}
|
||||
: {};
|
||||
const response = await client(ClientOperationType.READ)
|
||||
const response = await client()
|
||||
.database(collection.databaseId)
|
||||
.container(collection.id())
|
||||
.item(documentId.id(), getPartitionKeyValue(documentId))
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { RequestOptions } from "@azure/cosmos";
|
||||
import { Offer } from "../../Contracts/DataModels";
|
||||
import { HttpHeaders } from "../Constants";
|
||||
import { ClientOperationType, client } from "../CosmosClient";
|
||||
import { client } from "../CosmosClient";
|
||||
import { parseSDKOfferResponse } from "../OfferUtility";
|
||||
import { readOffers } from "./readOffers";
|
||||
|
||||
@@ -21,7 +21,7 @@ export const readOfferWithSDK = async (offerId: string, resourceId: string): Pro
|
||||
[HttpHeaders.populateCollectionThroughputInfo]: true,
|
||||
},
|
||||
};
|
||||
const response = await client(ClientOperationType.READ).offer(offerId).read(options);
|
||||
const response = await client().offer(offerId).read(options);
|
||||
|
||||
return parseSDKOfferResponse(response);
|
||||
};
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { SDKOfferDefinition } from "../../Contracts/DataModels";
|
||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { ClientOperationType, client } from "../CosmosClient";
|
||||
import { getErrorMessage, handleError } from "../ErrorHandlingUtils";
|
||||
import { client } from "../CosmosClient";
|
||||
import { handleError, getErrorMessage } from "../ErrorHandlingUtils";
|
||||
|
||||
export const readOffers = async (): Promise<SDKOfferDefinition[]> => {
|
||||
const clearMessage = logConsoleProgress(`Querying offers`);
|
||||
|
||||
try {
|
||||
const response = await client(ClientOperationType.READ).offers.readAll().fetchAll();
|
||||
const response = await client().offers.readAll().fetchAll();
|
||||
return response?.resources;
|
||||
} catch (error) {
|
||||
// This should be removed when we can correctly identify if an account is serverless when connected using connection string too.
|
||||
|
||||
@@ -4,7 +4,7 @@ import { AuthType } from "../../AuthType";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { listSqlStoredProcedures } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
|
||||
import { ClientOperationType, client } from "../CosmosClient";
|
||||
import { client } from "../CosmosClient";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
|
||||
export async function readStoredProcedures(
|
||||
@@ -34,7 +34,7 @@ export async function readStoredProcedures(
|
||||
throw new Error(cloudError?.error?.message);
|
||||
}
|
||||
|
||||
const response = await client(ClientOperationType.READ)
|
||||
const response = await client()
|
||||
.database(databaseId)
|
||||
.container(collectionId)
|
||||
.scripts.storedProcedures.readAll()
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { TriggerDefinition } from "@azure/cosmos";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { listSqlTriggers } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
|
||||
import { SqlTriggerResource } from "../../Utils/arm/generatedClients/cosmos/types";
|
||||
import { ClientOperationType, client } from "../CosmosClient";
|
||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { client } from "../CosmosClient";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
|
||||
export async function readTriggers(
|
||||
@@ -28,11 +28,7 @@ export async function readTriggers(
|
||||
return rpResponse?.value?.map((trigger) => trigger.properties?.resource);
|
||||
}
|
||||
|
||||
const response = await client(ClientOperationType.READ)
|
||||
.database(databaseId)
|
||||
.container(collectionId)
|
||||
.scripts.triggers.readAll()
|
||||
.fetchAll();
|
||||
const response = await client().database(databaseId).container(collectionId).scripts.triggers.readAll().fetchAll();
|
||||
return response?.resources;
|
||||
} catch (error) {
|
||||
handleError(error, "ReadTriggers", `Failed to query triggers for container ${collectionId}`);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Resource, UserDefinedFunctionDefinition } from "@azure/cosmos";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { listSqlUserDefinedFunctions } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
|
||||
import { ClientOperationType, client } from "../CosmosClient";
|
||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { client } from "../CosmosClient";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
|
||||
export async function readUserDefinedFunctions(
|
||||
@@ -24,7 +24,7 @@ export async function readUserDefinedFunctions(
|
||||
return rpResponse?.value?.map((udf) => udf.properties?.resource as UserDefinedFunctionDefinition & Resource);
|
||||
}
|
||||
|
||||
const response = await client(ClientOperationType.READ)
|
||||
const response = await client()
|
||||
.database(databaseId)
|
||||
.container(collectionId)
|
||||
.scripts.userDefinedFunctions.readAll()
|
||||
|
||||
@@ -2,7 +2,6 @@ import { ContainerDefinition, RequestOptions } from "@azure/cosmos";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { Collection } from "../../Contracts/DataModels";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import {
|
||||
createUpdateCassandraTable,
|
||||
getCassandraTable,
|
||||
@@ -20,7 +19,8 @@ import {
|
||||
SqlContainerCreateUpdateParameters,
|
||||
SqlContainerResource,
|
||||
} from "../../Utils/arm/generatedClients/cosmos/types";
|
||||
import { ClientOperationType, client } from "../CosmosClient";
|
||||
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { client } from "../CosmosClient";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
|
||||
export async function updateCollection(
|
||||
@@ -40,7 +40,7 @@ export async function updateCollection(
|
||||
) {
|
||||
collection = await updateCollectionWithARM(databaseId, collectionId, newCollection);
|
||||
} else {
|
||||
const sdkResponse = await client(ClientOperationType.WRITE)
|
||||
const sdkResponse = await client()
|
||||
.database(databaseId)
|
||||
.container(collectionId)
|
||||
.replace(newCollection as ContainerDefinition, options);
|
||||
|
||||
@@ -3,7 +3,7 @@ import { HttpHeaders } from "Common/Constants";
|
||||
import { CollectionBase } from "../../Contracts/ViewModels";
|
||||
import DocumentId from "../../Explorer/Tree/DocumentId";
|
||||
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { ClientOperationType, client } from "../CosmosClient";
|
||||
import { client } from "../CosmosClient";
|
||||
import { getEntityName } from "../DocumentUtility";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
import { getPartitionKeyValue } from "./getPartitionKeyValue";
|
||||
@@ -23,7 +23,7 @@ export const updateDocument = async (
|
||||
[HttpHeaders.partitionKey]: documentId.partitionKeyValue,
|
||||
}
|
||||
: {};
|
||||
const response = await client(ClientOperationType.WRITE)
|
||||
const response = await client()
|
||||
.database(collection.databaseId)
|
||||
.container(collection.id())
|
||||
.item(documentId.id(), getPartitionKeyValue(documentId))
|
||||
|
||||
@@ -2,7 +2,6 @@ import { OfferDefinition, RequestOptions } from "@azure/cosmos";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { Offer, SDKOfferDefinition, UpdateOfferParams } from "../../Contracts/DataModels";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import {
|
||||
migrateCassandraKeyspaceToAutoscale,
|
||||
migrateCassandraKeyspaceToManualThroughput,
|
||||
@@ -41,8 +40,9 @@ import {
|
||||
updateTableThroughput,
|
||||
} from "../../Utils/arm/generatedClients/cosmos/tableResources";
|
||||
import { ThroughputSettingsUpdateParameters } from "../../Utils/arm/generatedClients/cosmos/types";
|
||||
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { HttpHeaders } from "../Constants";
|
||||
import { ClientOperationType, client } from "../CosmosClient";
|
||||
import { client } from "../CosmosClient";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
import { parseSDKOfferResponse } from "../OfferUtility";
|
||||
import { readCollectionOffer } from "./readCollectionOffer";
|
||||
@@ -401,7 +401,7 @@ const updateOfferWithSDK = async (params: UpdateOfferParams): Promise<Offer> =>
|
||||
newOffer.content.offerAutopilotSettings = { maxThroughput: 0 };
|
||||
}
|
||||
|
||||
const sdkResponse = await client(ClientOperationType.WRITE)
|
||||
const sdkResponse = await client()
|
||||
.offer(params.currentOffer.id)
|
||||
// TODO Remove casting when SDK types are fixed (https://github.com/Azure/azure-sdk-for-js/issues/10660)
|
||||
.replace(newOffer as unknown as OfferDefinition, options);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Resource, StoredProcedureDefinition } from "@azure/cosmos";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import {
|
||||
createUpdateSqlStoredProcedure,
|
||||
getSqlStoredProcedure,
|
||||
@@ -10,7 +9,8 @@ import {
|
||||
SqlStoredProcedureCreateUpdateParameters,
|
||||
SqlStoredProcedureResource,
|
||||
} from "../../Utils/arm/generatedClients/cosmos/types";
|
||||
import { ClientOperationType, client } from "../CosmosClient";
|
||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { client } from "../CosmosClient";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
|
||||
export async function updateStoredProcedure(
|
||||
@@ -54,7 +54,7 @@ export async function updateStoredProcedure(
|
||||
throw new Error(`Failed to update stored procedure: ${storedProcedure.id} does not exist.`);
|
||||
}
|
||||
|
||||
const response = await client(ClientOperationType.WRITE)
|
||||
const response = await client()
|
||||
.database(databaseId)
|
||||
.container(collectionId)
|
||||
.scripts.storedProcedure(storedProcedure.id)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { TriggerDefinition } from "@azure/cosmos";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { createUpdateSqlTrigger, getSqlTrigger } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
|
||||
import { SqlTriggerCreateUpdateParameters, SqlTriggerResource } from "../../Utils/arm/generatedClients/cosmos/types";
|
||||
import { ClientOperationType, client } from "../CosmosClient";
|
||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { client } from "../CosmosClient";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
|
||||
export async function updateTrigger(
|
||||
@@ -47,7 +47,7 @@ export async function updateTrigger(
|
||||
throw new Error(`Failed to update trigger: ${trigger.id} does not exist.`);
|
||||
}
|
||||
|
||||
const response = await client(ClientOperationType.WRITE)
|
||||
const response = await client()
|
||||
.database(databaseId)
|
||||
.container(collectionId)
|
||||
.scripts.trigger(trigger.id)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Resource, UserDefinedFunctionDefinition } from "@azure/cosmos";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import {
|
||||
createUpdateSqlUserDefinedFunction,
|
||||
getSqlUserDefinedFunction,
|
||||
@@ -10,7 +9,8 @@ import {
|
||||
SqlUserDefinedFunctionCreateUpdateParameters,
|
||||
SqlUserDefinedFunctionResource,
|
||||
} from "../../Utils/arm/generatedClients/cosmos/types";
|
||||
import { ClientOperationType, client } from "../CosmosClient";
|
||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { client } from "../CosmosClient";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
|
||||
export async function updateUserDefinedFunction(
|
||||
@@ -53,7 +53,7 @@ export async function updateUserDefinedFunction(
|
||||
throw new Error(`Failed to update user defined function: ${userDefinedFunction.id} does not exist.`);
|
||||
}
|
||||
|
||||
const response = await client(ClientOperationType.WRITE)
|
||||
const response = await client()
|
||||
.database(databaseId)
|
||||
.container(collectionId)
|
||||
.scripts.userDefinedFunction(userDefinedFunction.id)
|
||||
|
||||
@@ -83,6 +83,7 @@ let configContext: Readonly<ConfigContext> = {
|
||||
`^https:\\/\\/.*\\.analysis-df\\.net$`,
|
||||
`^https:\\/\\/.*\\.analysis-df\\.windows\\.net$`,
|
||||
`^https:\\/\\/.*\\.azure-test\\.net$`,
|
||||
`^https:\\/\\/cosmos-explorer-preview\\.azurewebsites\\.net`,
|
||||
], // Webpack injects this at build time
|
||||
gitSha: process.env.GIT_SHA,
|
||||
hostedExplorerURL: "https://cosmos.azure.com/",
|
||||
@@ -99,24 +100,20 @@ let configContext: Readonly<ConfigContext> = {
|
||||
JUNO_ENDPOINT: JunoEndpoints.Prod,
|
||||
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
||||
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Prod,
|
||||
NEW_BACKEND_APIS: [BackendApi.GenerateToken],
|
||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||
NEW_MONGO_APIS: [
|
||||
// "resourcelist",
|
||||
// "queryDocuments",
|
||||
// "createDocument",
|
||||
// "readDocument",
|
||||
// "updateDocument",
|
||||
// "deleteDocument",
|
||||
// "createCollectionWithProxy",
|
||||
"legacyMongoShell",
|
||||
],
|
||||
MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED: false,
|
||||
CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Prod,
|
||||
NEW_CASSANDRA_APIS: [
|
||||
// "postQuery",
|
||||
// "createOrDelete",
|
||||
// "getKeys",
|
||||
// "getSchema",
|
||||
],
|
||||
NEW_CASSANDRA_APIS: ["postQuery", "createOrDelete", "getKeys", "getSchema"],
|
||||
CASSANDRA_PROXY_OUTBOUND_IPS_ALLOWLISTED: false,
|
||||
isTerminalEnabled: false,
|
||||
isPhoenixEnabled: false,
|
||||
|
||||
@@ -47,6 +47,7 @@ export enum MessageTypes {
|
||||
GetAllResourceTokens, // Data Explorer -> Fabric
|
||||
Ready, // Data Explorer -> Fabric
|
||||
OpenCESCVAFeedbackBlade,
|
||||
ActivateTab,
|
||||
}
|
||||
|
||||
export interface AuthorizationToken {
|
||||
|
||||
@@ -387,6 +387,7 @@ export interface DataExplorerInputsFrame {
|
||||
dnsSuffix?: string;
|
||||
serverId?: string;
|
||||
extensionEndpoint?: string;
|
||||
portalBackendEndpoint?: string;
|
||||
mongoProxyEndpoint?: string;
|
||||
cassandraProxyEndpoint?: string;
|
||||
subscriptionType?: SubscriptionType;
|
||||
|
||||
@@ -132,13 +132,16 @@ export const createCollectionContextMenuButton = (
|
||||
if (configContext.platform !== Platform.Fabric) {
|
||||
items.push({
|
||||
iconSrc: DeleteCollectionIcon,
|
||||
onClick: () => {
|
||||
onClick: (lastFocusedElement?: React.RefObject<HTMLElement>) => {
|
||||
useSelectedNode.getState().setSelectedNode(selectedCollection);
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
"Delete " + getCollectionName(),
|
||||
<DeleteCollectionConfirmationPane refreshDatabases={() => container.refreshAllDatabases()} />,
|
||||
<DeleteCollectionConfirmationPane
|
||||
lastFocusedElement={lastFocusedElement}
|
||||
refreshDatabases={() => container.refreshAllDatabases()}
|
||||
/>,
|
||||
);
|
||||
},
|
||||
label: `Delete ${getCollectionName()}`,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/**
|
||||
* React component for Command button component.
|
||||
*/
|
||||
import { KeyboardAction } from "KeyboardShortcuts";
|
||||
import * as React from "react";
|
||||
import CollapseChevronDownIcon from "../../../../images/QueryBuilder/CollapseChevronDown_16x.png";
|
||||
import { KeyCodes } from "../../../Common/Constants";
|
||||
@@ -30,7 +31,7 @@ export interface CommandButtonComponentProps {
|
||||
/**
|
||||
* Click handler for command button click
|
||||
*/
|
||||
onCommandClick: (e: React.SyntheticEvent) => void;
|
||||
onCommandClick: (e: React.SyntheticEvent | KeyboardEvent) => void;
|
||||
|
||||
/**
|
||||
* Label for the button
|
||||
@@ -107,10 +108,17 @@ export interface CommandButtonComponentProps {
|
||||
* Vertical bar to divide buttons
|
||||
*/
|
||||
isDivider?: boolean;
|
||||
|
||||
/**
|
||||
* Aria-label for the button
|
||||
*/
|
||||
ariaLabel: string;
|
||||
|
||||
/**
|
||||
* If specified, a keyboard action that should trigger this button's onCommandClick handler when activated.
|
||||
* If not specified, the button will not be triggerable by keyboard shortcuts.
|
||||
*/
|
||||
keyboardAction?: KeyboardAction;
|
||||
}
|
||||
|
||||
export class CommandButtonComponent extends React.Component<CommandButtonComponentProps> {
|
||||
|
||||
@@ -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());
|
||||
});
|
||||
|
||||
@@ -30,7 +30,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"conflictResolutionPolicy": [Function],
|
||||
"container": Explorer {
|
||||
"_isInitializingNotebooks": false,
|
||||
"_resetNotebookWorkspace": [Function],
|
||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||
"isTabsContentExpanded": [Function],
|
||||
"onRefreshDatabasesKeyPress": [Function],
|
||||
@@ -108,7 +107,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"conflictResolutionPolicy": [Function],
|
||||
"container": Explorer {
|
||||
"_isInitializingNotebooks": false,
|
||||
"_resetNotebookWorkspace": [Function],
|
||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||
"isTabsContentExpanded": [Function],
|
||||
"onRefreshDatabasesKeyPress": [Function],
|
||||
@@ -225,7 +223,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"conflictResolutionPolicy": [Function],
|
||||
"container": Explorer {
|
||||
"_isInitializingNotebooks": false,
|
||||
"_resetNotebookWorkspace": [Function],
|
||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||
"isTabsContentExpanded": [Function],
|
||||
"onRefreshDatabasesKeyPress": [Function],
|
||||
@@ -272,7 +269,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
explorer={
|
||||
Explorer {
|
||||
"_isInitializingNotebooks": false,
|
||||
"_resetNotebookWorkspace": [Function],
|
||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||
"isTabsContentExpanded": [Function],
|
||||
"onRefreshDatabasesKeyPress": [Function],
|
||||
|
||||
@@ -24,7 +24,7 @@ import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcesso
|
||||
|
||||
export interface TreeNodeMenuItem {
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
onClick: (value?: React.RefObject<any>) => void;
|
||||
iconSrc?: string;
|
||||
isDisabled?: boolean;
|
||||
styleClass?: string;
|
||||
@@ -242,8 +242,9 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={this.contextMenuRef} onContextMenu={this.onRightClick} onKeyPress={this.onMoreButtonKeyPress}>
|
||||
<div onContextMenu={this.onRightClick} onKeyPress={this.onMoreButtonKeyPress}>
|
||||
<IconButton
|
||||
elementRef={this.contextMenuRef}
|
||||
name="More"
|
||||
title="More"
|
||||
className="treeMenuEllipsis"
|
||||
@@ -283,7 +284,7 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
|
||||
disabled: menuItem.isDisabled,
|
||||
className: menuItem.styleClass,
|
||||
onClick: () => {
|
||||
menuItem.onClick();
|
||||
menuItem.onClick(this.contextMenuRef);
|
||||
TelemetryProcessor.trace(Action.ClickResourceTreeNodeContextMenuItem, ActionModifiers.Mark, {
|
||||
label: menuItem.label,
|
||||
});
|
||||
|
||||
@@ -174,6 +174,11 @@ exports[`TreeNodeComponent renders a simple node (sorted children, expanded) 1`]
|
||||
<CustomizedIconButton
|
||||
ariaLabel="More options"
|
||||
className="treeMenuEllipsis"
|
||||
elementRef={
|
||||
Object {
|
||||
"current": null,
|
||||
}
|
||||
}
|
||||
menuIconProps={
|
||||
Object {
|
||||
"iconName": "More",
|
||||
@@ -399,6 +404,11 @@ exports[`TreeNodeComponent renders sorted children, expanded, leaves and parents
|
||||
<CustomizedIconButton
|
||||
ariaLabel="More options"
|
||||
className="treeMenuEllipsis"
|
||||
elementRef={
|
||||
Object {
|
||||
"current": null,
|
||||
}
|
||||
}
|
||||
menuIconProps={
|
||||
Object {
|
||||
"iconName": "More",
|
||||
|
||||
@@ -38,7 +38,6 @@ import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils";
|
||||
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
|
||||
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils";
|
||||
import { update } from "../Utils/arm/generatedClients/cosmos/databaseAccounts";
|
||||
import { listByDatabaseAccount } from "../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces";
|
||||
import { useSidePanel } from "../hooks/useSidePanel";
|
||||
import { useTabs } from "../hooks/useTabs";
|
||||
import "./ComponentRegisterer";
|
||||
@@ -56,7 +55,6 @@ import { AddCollectionPanel } from "./Panes/AddCollectionPanel";
|
||||
import { CassandraAddCollectionPane } from "./Panes/CassandraAddCollectionPane/CassandraAddCollectionPane";
|
||||
import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane/ExecuteSprocParamsPane";
|
||||
import { StringInputPane } from "./Panes/StringInputPane/StringInputPane";
|
||||
import { UploadFilePane } from "./Panes/UploadFilePane/UploadFilePane";
|
||||
import { UploadItemsPane } from "./Panes/UploadItemsPane/UploadItemsPane";
|
||||
import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient";
|
||||
import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab";
|
||||
@@ -510,104 +508,6 @@ export default class Explorer {
|
||||
.then((memoryUsageInfo) => useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo));
|
||||
}
|
||||
|
||||
public resetNotebookWorkspace(): void {
|
||||
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookClient) {
|
||||
handleError(
|
||||
"Attempt to reset notebook workspace, but notebook is not enabled",
|
||||
"Explorer/resetNotebookWorkspace",
|
||||
);
|
||||
return;
|
||||
}
|
||||
const dialogContent = useNotebook.getState().isPhoenixNotebooks
|
||||
? "Notebooks saved in the temporary workspace will be deleted. Do you want to proceed?"
|
||||
: "This lets you keep your notebook files and the workspace will be restored to default. Proceed anyway?";
|
||||
|
||||
const resetConfirmationDialogProps: DialogProps = {
|
||||
isModal: true,
|
||||
title: "Reset Workspace",
|
||||
subText: dialogContent,
|
||||
primaryButtonText: "OK",
|
||||
secondaryButtonText: "Cancel",
|
||||
onPrimaryButtonClick: this._resetNotebookWorkspace,
|
||||
onSecondaryButtonClick: () => useDialog.getState().closeDialog(),
|
||||
};
|
||||
useDialog.getState().openDialog(resetConfirmationDialogProps);
|
||||
}
|
||||
|
||||
private async _containsDefaultNotebookWorkspace(databaseAccount: DataModels.DatabaseAccount): Promise<boolean> {
|
||||
if (!databaseAccount) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const { value: workspaces } = await listByDatabaseAccount(
|
||||
userContext.subscriptionId,
|
||||
userContext.resourceGroup,
|
||||
userContext.databaseAccount.name,
|
||||
);
|
||||
return workspaces && workspaces.length > 0 && workspaces.some((workspace) => workspace.name === "default");
|
||||
} catch (error) {
|
||||
Logger.logError(getErrorMessage(error), "Explorer/_containsDefaultNotebookWorkspace");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private _resetNotebookWorkspace = async () => {
|
||||
useDialog.getState().closeDialog();
|
||||
const clearInProgressMessage = logConsoleProgress("Resetting notebook workspace");
|
||||
let connectionStatus: ContainerConnectionInfo;
|
||||
try {
|
||||
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
||||
if (!notebookServerInfo || !notebookServerInfo.notebookServerEndpoint) {
|
||||
const error = "No server endpoint detected";
|
||||
Logger.logError(error, "NotebookContainerClient/resetWorkspace");
|
||||
logConsoleError(error);
|
||||
return;
|
||||
}
|
||||
TelemetryProcessor.traceStart(Action.PhoenixResetWorkspace, {
|
||||
dataExplorerArea: Areas.Notebook,
|
||||
});
|
||||
if (useNotebook.getState().isPhoenixNotebooks) {
|
||||
useTabs.getState().closeAllNotebookTabs(true);
|
||||
connectionStatus = {
|
||||
status: ConnectionStatusType.Connecting,
|
||||
};
|
||||
useNotebook.getState().setConnectionInfo(connectionStatus);
|
||||
}
|
||||
const connectionInfo = await this.notebookManager?.notebookClient.resetWorkspace();
|
||||
if (connectionInfo?.status !== HttpStatusCodes.OK) {
|
||||
throw new Error(`Reset Workspace: Received status code- ${connectionInfo?.status}`);
|
||||
}
|
||||
if (!connectionInfo?.data?.phoenixServiceUrl) {
|
||||
throw new Error(`Reset Workspace: PhoenixServiceUrl is invalid!`);
|
||||
}
|
||||
if (useNotebook.getState().isPhoenixNotebooks) {
|
||||
await this.setNotebookInfo(true, connectionInfo, connectionStatus);
|
||||
useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed);
|
||||
}
|
||||
logConsoleInfo("Successfully reset notebook workspace");
|
||||
TelemetryProcessor.traceSuccess(Action.PhoenixResetWorkspace, {
|
||||
dataExplorerArea: Areas.Notebook,
|
||||
});
|
||||
} catch (error) {
|
||||
logConsoleError(`Failed to reset notebook workspace: ${error}`);
|
||||
TelemetryProcessor.traceFailure(Action.PhoenixResetWorkspace, {
|
||||
dataExplorerArea: Areas.Notebook,
|
||||
error: getErrorMessage(error),
|
||||
errorStack: getErrorStack(error),
|
||||
});
|
||||
if (useNotebook.getState().isPhoenixNotebooks) {
|
||||
connectionStatus = {
|
||||
status: ConnectionStatusType.Failed,
|
||||
};
|
||||
useNotebook.getState().resetContainerConnection(connectionStatus);
|
||||
useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed);
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
clearInProgressMessage();
|
||||
}
|
||||
};
|
||||
|
||||
private getDeltaDatabases(
|
||||
updatedDatabaseList: DataModels.Database[],
|
||||
databases: ViewModels.Database[],
|
||||
@@ -1010,92 +910,6 @@ export default class Explorer {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* This creates a new notebook file, then opens the notebook
|
||||
*/
|
||||
public async onNewNotebookClicked(parent?: NotebookContentItem, isGithubTree?: boolean): Promise<void> {
|
||||
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
|
||||
const error = "Attempt to create new notebook, but notebook is not enabled";
|
||||
handleError(error, "Explorer/onNewNotebookClicked");
|
||||
throw new Error(error);
|
||||
}
|
||||
if (useNotebook.getState().isPhoenixNotebooks) {
|
||||
if (isGithubTree) {
|
||||
await this.allocateContainer(PoolIdType.DefaultPoolId);
|
||||
parent = parent || this.resourceTree.myNotebooksContentRoot;
|
||||
this.createNewNoteBook(parent, isGithubTree);
|
||||
} else {
|
||||
useDialog.getState().showOkCancelModalDialog(
|
||||
Notebook.newNotebookModalTitle,
|
||||
undefined,
|
||||
"Create",
|
||||
async () => {
|
||||
await this.allocateContainer(PoolIdType.DefaultPoolId);
|
||||
parent = parent || this.resourceTree.myNotebooksContentRoot;
|
||||
this.createNewNoteBook(parent, isGithubTree);
|
||||
},
|
||||
"Cancel",
|
||||
undefined,
|
||||
this.getNewNoteWarningText(),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
parent = parent || this.resourceTree.myNotebooksContentRoot;
|
||||
this.createNewNoteBook(parent, isGithubTree);
|
||||
}
|
||||
}
|
||||
|
||||
private getNewNoteWarningText(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<p>{Notebook.newNotebookModalContent1}</p>
|
||||
<br />
|
||||
<p>
|
||||
{Notebook.newNotebookModalContent2}
|
||||
<Link href={Notebook.cosmosNotebookHomePageUrl} target="_blank">
|
||||
{Notebook.learnMore}
|
||||
</Link>
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
private createNewNoteBook(parent?: NotebookContentItem, isGithubTree?: boolean): void {
|
||||
const clearInProgressMessage = logConsoleProgress(`Creating new notebook in ${parent.path}`);
|
||||
const startKey: number = TelemetryProcessor.traceStart(Action.CreateNewNotebook, {
|
||||
dataExplorerArea: Constants.Areas.Notebook,
|
||||
});
|
||||
|
||||
this.notebookManager?.notebookContentClient
|
||||
.createNewNotebookFile(parent, isGithubTree)
|
||||
.then((newFile: NotebookContentItem) => {
|
||||
logConsoleInfo(`Successfully created: ${newFile.name}`);
|
||||
TelemetryProcessor.traceSuccess(
|
||||
Action.CreateNewNotebook,
|
||||
{
|
||||
dataExplorerArea: Constants.Areas.Notebook,
|
||||
},
|
||||
startKey,
|
||||
);
|
||||
return this.openNotebook(newFile);
|
||||
})
|
||||
.then(() => this.resourceTree.triggerRender())
|
||||
.catch((error) => {
|
||||
const errorMessage = `Failed to create a new notebook: ${getErrorMessage(error)}`;
|
||||
logConsoleError(errorMessage);
|
||||
TelemetryProcessor.traceFailure(
|
||||
Action.CreateNewNotebook,
|
||||
{
|
||||
dataExplorerArea: Constants.Areas.Notebook,
|
||||
error: errorMessage,
|
||||
errorStack: getErrorStack(error),
|
||||
},
|
||||
startKey,
|
||||
);
|
||||
})
|
||||
.finally(clearInProgressMessage);
|
||||
}
|
||||
|
||||
// TODO: Delete this function when ResourceTreeAdapter is removed.
|
||||
public async refreshContentItem(item: NotebookContentItem): Promise<void> {
|
||||
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
|
||||
@@ -1130,10 +944,6 @@ export default class Explorer {
|
||||
let title: string;
|
||||
|
||||
switch (kind) {
|
||||
case ViewModels.TerminalKind.Default:
|
||||
title = "Terminal";
|
||||
break;
|
||||
|
||||
case ViewModels.TerminalKind.Mongo:
|
||||
title = "Mongo Shell";
|
||||
break;
|
||||
@@ -1287,36 +1097,6 @@ export default class Explorer {
|
||||
.openSidePanel("Input parameters", <ExecuteSprocParamsPane storedProcedure={storedProcedure} />);
|
||||
}
|
||||
|
||||
public openUploadFilePanel(parent?: NotebookContentItem): void {
|
||||
if (useNotebook.getState().isPhoenixNotebooks) {
|
||||
useDialog.getState().showOkCancelModalDialog(
|
||||
Notebook.newNotebookUploadModalTitle,
|
||||
undefined,
|
||||
"Upload",
|
||||
async () => {
|
||||
await this.allocateContainer(PoolIdType.DefaultPoolId);
|
||||
parent = parent || this.resourceTree.myNotebooksContentRoot;
|
||||
this.uploadFilePanel(parent);
|
||||
},
|
||||
"Cancel",
|
||||
undefined,
|
||||
this.getNewNoteWarningText(),
|
||||
);
|
||||
} else {
|
||||
parent = parent || this.resourceTree.myNotebooksContentRoot;
|
||||
this.uploadFilePanel(parent);
|
||||
}
|
||||
}
|
||||
|
||||
private uploadFilePanel(parent?: NotebookContentItem): void {
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
"Upload file to notebook server",
|
||||
<UploadFilePane uploadFile={(name: string, content: string) => this.uploadFile(name, content, parent)} />,
|
||||
);
|
||||
}
|
||||
|
||||
public getDownloadModalConent(fileName: string): JSX.Element {
|
||||
if (useNotebook.getState().isPhoenixNotebooks) {
|
||||
return (
|
||||
|
||||
@@ -349,7 +349,7 @@ export class NodePropertiesComponent extends React.Component<
|
||||
onActivated={this.setIsDeleteConfirm.bind(this, true)}
|
||||
aria-label="Delete this vertex"
|
||||
>
|
||||
<img src={DeleteIcon} alt="Delete" />
|
||||
<img src={DeleteIcon} alt="Delete" role="button" />
|
||||
</AccessibleElement>
|
||||
);
|
||||
} else {
|
||||
@@ -406,7 +406,7 @@ export class NodePropertiesComponent extends React.Component<
|
||||
aria-label="Edit properties"
|
||||
onActivated={expandClickHandler}
|
||||
>
|
||||
<img src={EditIcon} alt="Edit" />
|
||||
<img src={EditIcon} alt="Edit" role="button" />
|
||||
</AccessibleElement>
|
||||
)}
|
||||
|
||||
|
||||
@@ -184,12 +184,18 @@ export const NewVertexComponent: FunctionComponent<INewVertexComponentProps> = (
|
||||
className="rightPaneTrashIcon rightPaneBtns"
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
aria-label={`Delete ${data.key}`}
|
||||
onClick={(event: React.MouseEvent<HTMLDivElement>) => removeNewVertexProperty(event, index)}
|
||||
onKeyPress={(event: React.KeyboardEvent<HTMLDivElement>) =>
|
||||
removeNewVertexPropertyKeyPress(event, index)
|
||||
}
|
||||
>
|
||||
<img className="refreshcol rightPaneTrashIconImg" src={DeleteIcon} alt="Remove property" />
|
||||
<img
|
||||
aria-label="hidden"
|
||||
className="refreshcol rightPaneTrashIconImg"
|
||||
src={DeleteIcon}
|
||||
alt="Remove property"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react";
|
||||
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
||||
import { KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
|
||||
import { userContext } from "UserContext";
|
||||
import * as React from "react";
|
||||
import create, { UseStore } from "zustand";
|
||||
@@ -40,6 +41,7 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
||||
const buttons = useCommandBar((state) => state.contextButtons);
|
||||
const isHidden = useCommandBar((state) => state.isHidden);
|
||||
const backgroundColor = StyleConstants.BaseLight;
|
||||
const setKeyboardHandlers = useKeyboardActionGroup(KeyboardActionGroup.COMMAND_BAR);
|
||||
|
||||
if (userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo") {
|
||||
const buttons =
|
||||
@@ -105,6 +107,10 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
||||
},
|
||||
};
|
||||
|
||||
const allButtons = staticButtons.concat(contextButtons).concat(controlButtons);
|
||||
const keyboardHandlers = CommandBarUtil.createKeyboardHandlers(allButtons);
|
||||
setKeyboardHandlers(keyboardHandlers);
|
||||
|
||||
return (
|
||||
<div className="commandBarContainer" style={{ display: isHidden ? "none" : "initial" }}>
|
||||
<FluentCommandBar
|
||||
|
||||
@@ -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 { KeyboardAction } from "KeyboardShortcuts";
|
||||
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
||||
import * as React from "react";
|
||||
import AddCollectionIcon from "../../../../images/AddCollection.svg";
|
||||
@@ -7,14 +8,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";
|
||||
@@ -22,8 +19,7 @@ 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 { updateUserContext, userContext } from "../../../UserContext";
|
||||
import { userContext } from "../../../UserContext";
|
||||
import { getCollectionName, getDatabaseName } from "../../../Utils/APITypeUtils";
|
||||
import { isRunningOnNationalCloud } from "../../../Utils/CloudUtils";
|
||||
import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||
@@ -33,7 +29,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";
|
||||
@@ -63,6 +58,7 @@ export function createStaticCommandBarButtons(
|
||||
buttons.push(homeBtn);
|
||||
|
||||
const newCollectionBtn = createNewCollectionGroup(container);
|
||||
newCollectionBtn.keyboardAction = KeyboardAction.NEW_COLLECTION; // Just for the root button, not the child version we create below.
|
||||
buttons.push(newCollectionBtn);
|
||||
if (userContext.apiType !== "Tables" && userContext.apiType !== "Cassandra") {
|
||||
const addSynapseLink = createOpenSynapseLinkDialogButton(container);
|
||||
@@ -80,57 +76,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";
|
||||
|
||||
@@ -151,6 +96,7 @@ export function createStaticCommandBarButtons(
|
||||
const newStoredProcedureBtn: CommandButtonComponentProps = {
|
||||
iconSrc: AddStoredProcedureIcon,
|
||||
iconAlt: label,
|
||||
keyboardAction: KeyboardAction.NEW_SPROC,
|
||||
onCommandClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
||||
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection);
|
||||
@@ -168,14 +114,6 @@ export function createStaticCommandBarButtons(
|
||||
}
|
||||
}
|
||||
|
||||
// Attempting to add region selection button here.
|
||||
addDivider();
|
||||
const [selectedRegion, setSelectedRegion] = React.useState(
|
||||
userContext.databaseAccount.properties.locations[0].locationName,
|
||||
);
|
||||
const newReadRegionSelectionBtn = createReadRegionSelectionGroup(container, selectedRegion, setSelectedRegion);
|
||||
buttons.push(newReadRegionSelectionBtn);
|
||||
|
||||
return buttons;
|
||||
}
|
||||
|
||||
@@ -342,6 +280,7 @@ function createNewDatabase(container: Explorer): CommandButtonComponentProps {
|
||||
return {
|
||||
iconSrc: AddDatabaseIcon,
|
||||
iconAlt: label,
|
||||
keyboardAction: KeyboardAction.NEW_DATABASE,
|
||||
onCommandClick: async () => {
|
||||
const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit;
|
||||
if (throughputCap && throughputCap !== -1) {
|
||||
@@ -362,6 +301,7 @@ function createNewSQLQueryButton(selectedNodeState: SelectedNodeState): CommandB
|
||||
id: "newQueryBtn",
|
||||
iconSrc: AddSqlQueryIcon,
|
||||
iconAlt: label,
|
||||
keyboardAction: KeyboardAction.NEW_QUERY,
|
||||
onCommandClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
||||
selectedCollection && selectedCollection.onNewQueryClick(selectedCollection);
|
||||
@@ -377,6 +317,7 @@ function createNewSQLQueryButton(selectedNodeState: SelectedNodeState): CommandB
|
||||
id: "newQueryBtn",
|
||||
iconSrc: AddSqlQueryIcon,
|
||||
iconAlt: label,
|
||||
keyboardAction: KeyboardAction.NEW_QUERY,
|
||||
onCommandClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
||||
selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection);
|
||||
@@ -402,6 +343,7 @@ export function createScriptCommandButtons(selectedNodeState: SelectedNodeState)
|
||||
const newStoredProcedureBtn: CommandButtonComponentProps = {
|
||||
iconSrc: AddStoredProcedureIcon,
|
||||
iconAlt: label,
|
||||
keyboardAction: KeyboardAction.NEW_SPROC,
|
||||
onCommandClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
||||
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection);
|
||||
@@ -421,6 +363,7 @@ export function createScriptCommandButtons(selectedNodeState: SelectedNodeState)
|
||||
const newUserDefinedFunctionBtn: CommandButtonComponentProps = {
|
||||
iconSrc: AddUdfIcon,
|
||||
iconAlt: label,
|
||||
keyboardAction: KeyboardAction.NEW_UDF,
|
||||
onCommandClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
||||
selectedCollection && selectedCollection.onNewUserDefinedFunctionClick(selectedCollection);
|
||||
@@ -440,6 +383,7 @@ export function createScriptCommandButtons(selectedNodeState: SelectedNodeState)
|
||||
const newTriggerBtn: CommandButtonComponentProps = {
|
||||
iconSrc: AddTriggerIcon,
|
||||
iconAlt: label,
|
||||
keyboardAction: KeyboardAction.NEW_TRIGGER,
|
||||
onCommandClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
||||
selectedCollection && selectedCollection.onNewTriggerClick(selectedCollection);
|
||||
@@ -457,45 +401,12 @@ 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 {
|
||||
iconSrc: BrowseQueriesIcon,
|
||||
iconAlt: label,
|
||||
keyboardAction: KeyboardAction.OPEN_QUERY,
|
||||
onCommandClick: () =>
|
||||
useSidePanel.getState().openSidePanel("Open Saved Queries", <BrowseQueriesPane explorer={container} />),
|
||||
commandButtonLabel: label,
|
||||
@@ -510,6 +421,7 @@ function createOpenQueryFromDiskButton(): CommandButtonComponentProps {
|
||||
return {
|
||||
iconSrc: OpenQueryFromDiskIcon,
|
||||
iconAlt: label,
|
||||
keyboardAction: KeyboardAction.OPEN_QUERY_FROM_DISK,
|
||||
onCommandClick: () => useSidePanel.getState().openSidePanel("Load Query", <LoadQueryPane />),
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
@@ -518,19 +430,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,
|
||||
@@ -570,142 +469,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 createReadRegionSelectionGroup(container: Explorer): CommandButtonComponentProps {
|
||||
// const label = "Select a Read Region";
|
||||
// return {
|
||||
// iconAlt: label,
|
||||
// commandButtonLabel: label,
|
||||
// hasPopup: false,
|
||||
// disabled: false,
|
||||
// ariaLabel: label,
|
||||
// onCommandClick: async () => {
|
||||
// console.log(
|
||||
// `CURRENT DOCUMENT ENDPOINT: ${JSON.stringify(userContext.databaseAccount.properties.documentEndpoint)}`,
|
||||
// );
|
||||
// userContext.databaseAccount.properties.readLocations.forEach((readLocation) => {
|
||||
// console.log(`CURRENT READ ENDPOINT(S): ${JSON.stringify(readLocation)}`);
|
||||
// });
|
||||
// const updatedDatabaseAccount = {
|
||||
// ...userContext.databaseAccount,
|
||||
// properties: {
|
||||
// ...userContext.databaseAccount.properties,
|
||||
// documentEndpoint: "https://test-craig-nosql-periodic-eastus.documents.azure.com:443/",
|
||||
// },
|
||||
// };
|
||||
// updateUserContext({
|
||||
// databaseAccount: updatedDatabaseAccount,
|
||||
// });
|
||||
// },
|
||||
// };
|
||||
// }
|
||||
|
||||
function createReadRegionSelectionGroup(
|
||||
container: Explorer,
|
||||
selectedRegion: string,
|
||||
setSelectedRegion: (region: string) => void,
|
||||
): CommandButtonComponentProps {
|
||||
const children = userContext.databaseAccount.properties.readLocations.map((readLocation) => ({
|
||||
commandButtonLabel: readLocation.locationName,
|
||||
onCommandClick: async () => {
|
||||
const updatedDatabaseAccount = {
|
||||
...userContext.databaseAccount,
|
||||
properties: {
|
||||
...userContext.databaseAccount.properties,
|
||||
documentEndpoint: readLocation.documentEndpoint,
|
||||
},
|
||||
};
|
||||
updateUserContext({
|
||||
databaseAccount: updatedDatabaseAccount,
|
||||
});
|
||||
setSelectedRegion(readLocation.locationName);
|
||||
},
|
||||
hasPopup: false,
|
||||
ariaLabel: `Select ${readLocation.locationName}`,
|
||||
}));
|
||||
const label = selectedRegion || "Select a Read Region";
|
||||
return {
|
||||
iconAlt: label,
|
||||
commandButtonLabel: label,
|
||||
onCommandClick: () => {},
|
||||
hasPopup: true,
|
||||
ariaLabel: label,
|
||||
isDropdown: true,
|
||||
children,
|
||||
dropdownWidth: 100,
|
||||
};
|
||||
}
|
||||
|
||||
// function createAccountRegionSelectionButton(container: Explorer): JSX.Element {
|
||||
// const [selectedEndpoint, setSelectedEndpoint] = useState(userContext.databaseAccount.properties.documentEndpoint);
|
||||
|
||||
// const handleChange = (event: React.ChangeEvent<{ value: unknown }>) => {
|
||||
// const endpoint = event.target.value as string;
|
||||
// setSelectedEndpoint(endpoint);
|
||||
|
||||
// const updatedDatabaseAccount = {
|
||||
// ...userContext.databaseAccount,
|
||||
// properties: {
|
||||
// ...userContext.databaseAccount.properties,
|
||||
// documentEndpoint: endpoint,
|
||||
// },
|
||||
// };
|
||||
// updateUserContext({
|
||||
// databaseAccount: updatedDatabaseAccount,
|
||||
// });
|
||||
// };
|
||||
|
||||
// return (
|
||||
// <Select
|
||||
// value={selectedEndpoint}
|
||||
// onChange={handleChange}
|
||||
// displayEmpty
|
||||
// >
|
||||
// {userContext.databaseAccount.properties.readLocations.map((readLocation) => (
|
||||
// <MenuItem value={readLocation}>{readLocation}</MenuItem>
|
||||
// ))}
|
||||
// </Select>
|
||||
// );
|
||||
// }
|
||||
|
||||
function createStaticCommandBarButtonsForResourceToken(
|
||||
container: Explorer,
|
||||
selectedNodeState: SelectedNodeState,
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
IDropdownStyles,
|
||||
} from "@fluentui/react";
|
||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||
import { KeyboardHandlerMap } from "KeyboardShortcuts";
|
||||
import * as React from "react";
|
||||
import _ from "underscore";
|
||||
import ChevronDownIcon from "../../../../images/Chevron_down.svg";
|
||||
@@ -233,3 +234,28 @@ export const createConnectionStatus = (container: Explorer, poolId: PoolIdType,
|
||||
onRender: () => <ConnectionStatus container={container} poolId={poolId} />,
|
||||
};
|
||||
};
|
||||
|
||||
export function createKeyboardHandlers(allButtons: CommandButtonComponentProps[]): KeyboardHandlerMap {
|
||||
const handlers: KeyboardHandlerMap = {};
|
||||
|
||||
function createHandlers(buttons: CommandButtonComponentProps[]) {
|
||||
buttons.forEach((button) => {
|
||||
if (!button.disabled && button.keyboardAction) {
|
||||
handlers[button.keyboardAction] = (e) => {
|
||||
button.onCommandClick(e);
|
||||
|
||||
// If the handler is bound, it means the button is visible and enabled, so we should prevent the default action
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
if (button.children && button.children.length > 0) {
|
||||
createHandlers(button.children);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createHandlers(allButtons);
|
||||
|
||||
return handlers;
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -52,7 +52,9 @@ describe("Delete Collection Confirmation Pane", () => {
|
||||
|
||||
describe("shouldRecordFeedback()", () => {
|
||||
it("should return true if last collection and database does not have shared throughput else false", () => {
|
||||
const wrapper = shallow(<DeleteCollectionConfirmationPane refreshDatabases={() => undefined} />);
|
||||
const wrapper = shallow(
|
||||
<DeleteCollectionConfirmationPane refreshDatabases={() => undefined} lastFocusedElement={undefined} />,
|
||||
);
|
||||
expect(wrapper.exists(".deleteCollectionFeedback")).toBe(false);
|
||||
|
||||
const database = { id: ko.observable("testDB") } as Database;
|
||||
@@ -109,7 +111,9 @@ describe("Delete Collection Confirmation Pane", () => {
|
||||
});
|
||||
|
||||
it("should call delete collection", () => {
|
||||
const wrapper = mount(<DeleteCollectionConfirmationPane refreshDatabases={() => undefined} />);
|
||||
const wrapper = mount(
|
||||
<DeleteCollectionConfirmationPane refreshDatabases={() => undefined} lastFocusedElement={undefined} />,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
|
||||
expect(wrapper.exists("#confirmCollectionId")).toBe(true);
|
||||
@@ -126,7 +130,9 @@ describe("Delete Collection Confirmation Pane", () => {
|
||||
});
|
||||
|
||||
it("should record feedback", async () => {
|
||||
const wrapper = mount(<DeleteCollectionConfirmationPane refreshDatabases={() => undefined} />);
|
||||
const wrapper = mount(
|
||||
<DeleteCollectionConfirmationPane refreshDatabases={() => undefined} lastFocusedElement={undefined} />,
|
||||
);
|
||||
expect(wrapper.exists("#confirmCollectionId")).toBe(true);
|
||||
wrapper
|
||||
.find("#confirmCollectionId")
|
||||
|
||||
@@ -12,17 +12,19 @@ import { getCollectionName } from "Utils/APITypeUtils";
|
||||
import * as NotificationConsoleUtils from "Utils/NotificationConsoleUtils";
|
||||
import { useSidePanel } from "hooks/useSidePanel";
|
||||
import { useTabs } from "hooks/useTabs";
|
||||
import React, { FunctionComponent, useState } from "react";
|
||||
import React, { FunctionComponent, useEffect, useState } from "react";
|
||||
import { useDatabases } from "../../useDatabases";
|
||||
import { useSelectedNode } from "../../useSelectedNode";
|
||||
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
|
||||
|
||||
export interface DeleteCollectionConfirmationPaneProps {
|
||||
refreshDatabases: () => Promise<void>;
|
||||
lastFocusedElement: React.MutableRefObject<HTMLElement>;
|
||||
}
|
||||
|
||||
export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectionConfirmationPaneProps> = ({
|
||||
refreshDatabases,
|
||||
lastFocusedElement,
|
||||
}: DeleteCollectionConfirmationPaneProps) => {
|
||||
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
|
||||
const [deleteCollectionFeedback, setDeleteCollectionFeedback] = useState<string>("");
|
||||
@@ -35,6 +37,7 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
|
||||
|
||||
const collectionName = getCollectionName().toLocaleLowerCase();
|
||||
const paneTitle = "Delete " + collectionName;
|
||||
const lastItemElement = lastFocusedElement?.current;
|
||||
|
||||
const onSubmit = async (): Promise<void> => {
|
||||
const collection = useSelectedNode.getState().findSelectedCollection();
|
||||
@@ -111,6 +114,13 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
|
||||
};
|
||||
const confirmContainer = `Confirm by typing the ${collectionName.toLowerCase()} id`;
|
||||
const reasonInfo = `Help us improve Azure Cosmos DB! What is the reason why you are deleting this ${collectionName}?`;
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (lastItemElement) {
|
||||
lastItemElement.focus();
|
||||
}
|
||||
};
|
||||
}, [lastItemElement]);
|
||||
return (
|
||||
<RightPaneForm {...props}>
|
||||
<div className="panelFormWrapper">
|
||||
|
||||
@@ -18,7 +18,6 @@ exports[`GitHub Repos Panel should render Default properly 1`] = `
|
||||
Object {
|
||||
"container": Explorer {
|
||||
"_isInitializingNotebooks": false,
|
||||
"_resetNotebookWorkspace": [Function],
|
||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||
"isTabsContentExpanded": [Function],
|
||||
"onRefreshDatabasesKeyPress": [Function],
|
||||
|
||||
@@ -630,7 +630,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
Enable sample database
|
||||
<InfoTooltip>
|
||||
This is a sample database and collection with synthetic product data you can use to explore using
|
||||
NoSQL queries and Copilot. This will appear as another database in the Data Explorer UI, and is
|
||||
NoSQL queries and Query Advisor. This will appear as another database in the Data Explorer UI, and is
|
||||
created by, and maintained by Microsoft at no cost to you.
|
||||
</InfoTooltip>
|
||||
</div>
|
||||
@@ -640,7 +640,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
label: { padding: 0 },
|
||||
}}
|
||||
className="padding"
|
||||
ariaLabel="Enable sample db for Copilot"
|
||||
ariaLabel="Enable sample db for Query Advisor"
|
||||
checked={copilotSampleDBEnabled}
|
||||
onChange={handleSampleDatabaseChange}
|
||||
/>
|
||||
|
||||
@@ -8,7 +8,6 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
|
||||
explorer={
|
||||
Explorer {
|
||||
"_isInitializingNotebooks": false,
|
||||
"_resetNotebookWorkspace": [Function],
|
||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||
"isTabsContentExpanded": [Function],
|
||||
"onRefreshDatabasesKeyPress": [Function],
|
||||
|
||||
@@ -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();
|
||||
@@ -261,6 +261,7 @@ export const AddTableEntityPanel: FunctionComponent<AddTableEntityPanelProps> =
|
||||
<TextField
|
||||
multiline
|
||||
rows={5}
|
||||
ariaLabel={entityAttributeProperty}
|
||||
value={entityAttributeValue}
|
||||
onChange={(event, newInput?: string) => {
|
||||
entityChange(newInput, selectedRow, "value");
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
import { Upload } from "Common/Upload/Upload";
|
||||
import { useSidePanel } from "hooks/useSidePanel";
|
||||
import React, { ChangeEvent, FunctionComponent, useState } from "react";
|
||||
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "Utils/NotificationConsoleUtils";
|
||||
import { NotebookContentItem } from "../../Notebook/NotebookContentItem";
|
||||
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
|
||||
|
||||
export interface UploadFilePanelProps {
|
||||
uploadFile: (name: string, content: string) => Promise<NotebookContentItem>;
|
||||
}
|
||||
|
||||
export const UploadFilePane: FunctionComponent<UploadFilePanelProps> = ({ uploadFile }: UploadFilePanelProps) => {
|
||||
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
|
||||
const extensions: string = undefined; //ex. ".ipynb"
|
||||
const errorMessage = "Could not upload file";
|
||||
const inProgressMessage = "Uploading file to notebook server";
|
||||
const successMessage = "Successfully uploaded file to notebook server";
|
||||
|
||||
const [files, setFiles] = useState<FileList>();
|
||||
const [formErrors, setFormErrors] = useState<string>("");
|
||||
const [isExecuting, setIsExecuting] = useState<boolean>(false);
|
||||
|
||||
const submit = () => {
|
||||
setFormErrors("");
|
||||
if (!files || files.length === 0) {
|
||||
setFormErrors("No file specified. Please input a file.");
|
||||
logConsoleError(`${errorMessage} -- No file specified. Please input a file.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const file: File = files.item(0);
|
||||
|
||||
const clearMessage = logConsoleProgress(`${inProgressMessage}: ${file.name}`);
|
||||
|
||||
setIsExecuting(true);
|
||||
|
||||
onSubmit(files.item(0))
|
||||
.then(
|
||||
() => {
|
||||
logConsoleInfo(`${successMessage} ${file.name}`);
|
||||
closeSidePanel();
|
||||
},
|
||||
(error: string) => {
|
||||
setFormErrors(errorMessage);
|
||||
logConsoleError(`${errorMessage} ${file.name}: ${error}`);
|
||||
},
|
||||
)
|
||||
.finally(() => {
|
||||
setIsExecuting(false);
|
||||
clearMessage();
|
||||
});
|
||||
};
|
||||
|
||||
const updateSelectedFiles = (event: ChangeEvent<HTMLInputElement>): void => {
|
||||
setFiles(event.target.files);
|
||||
};
|
||||
|
||||
const onSubmit = async (file: File): Promise<NotebookContentItem> => {
|
||||
const readFileAsText = (inputFile: File): Promise<string> => {
|
||||
const reader = new FileReader();
|
||||
return new Promise((resolve, reject) => {
|
||||
reader.onerror = () => {
|
||||
reader.abort();
|
||||
reject(`Problem parsing file: ${inputFile}`);
|
||||
};
|
||||
reader.onload = () => {
|
||||
resolve(reader.result as string);
|
||||
};
|
||||
reader.readAsText(inputFile);
|
||||
});
|
||||
};
|
||||
|
||||
const fileContent = await readFileAsText(file);
|
||||
return uploadFile(file.name, fileContent);
|
||||
};
|
||||
|
||||
const props: RightPaneFormProps = {
|
||||
formError: formErrors,
|
||||
isExecuting: isExecuting,
|
||||
submitButtonText: "Upload",
|
||||
onSubmit: submit,
|
||||
};
|
||||
|
||||
return (
|
||||
<RightPaneForm {...props}>
|
||||
<div className="paneMainContent">
|
||||
<Upload label="Select file to upload" accept={extensions} onUpload={updateSelectedFiles} />
|
||||
</div>
|
||||
</RightPaneForm>
|
||||
);
|
||||
};
|
||||
@@ -385,7 +385,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
||||
hasSmallHeadline={true}
|
||||
headline="Write a prompt"
|
||||
>
|
||||
Write a prompt here and Copilot will generate the query for you. You can also choose from our{" "}
|
||||
Write a prompt here and Query Advisor will generate the query for you. You can also choose from our{" "}
|
||||
<Link
|
||||
onClick={() => {
|
||||
setShowSamplePrompts(true);
|
||||
|
||||
@@ -57,12 +57,12 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
|
||||
|
||||
const toggleCopilotButton = {
|
||||
iconSrc: QueryCommandIcon,
|
||||
iconAlt: "Copilot",
|
||||
iconAlt: "Query Advisor",
|
||||
onCommandClick: () => {
|
||||
toggleCopilot(true);
|
||||
},
|
||||
commandButtonLabel: "Copilot",
|
||||
ariaLabel: "Copilot",
|
||||
commandButtonLabel: "Query Advisor",
|
||||
ariaLabel: "Query Advisor",
|
||||
hasPopup: false,
|
||||
disabled: copilotActive,
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
|
||||
|
||||
@@ -23,7 +23,6 @@ exports[`Query copilot tab snapshot test should render with initial input 1`] =
|
||||
explorer={
|
||||
Explorer {
|
||||
"_isInitializingNotebooks": false,
|
||||
"_resetNotebookWorkspace": [Function],
|
||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||
"isTabsContentExpanded": [Function],
|
||||
"onRefreshDatabasesKeyPress": [Function],
|
||||
|
||||
@@ -25,7 +25,6 @@ import * as React from "react";
|
||||
import ConnectIcon from "../../../images/Connect_color.svg";
|
||||
import ContainersIcon from "../../../images/Containers.svg";
|
||||
import LinkIcon from "../../../images/Link_blue.svg";
|
||||
import NotebookColorIcon from "../../../images/Notebooks.svg";
|
||||
import PowerShellIcon from "../../../images/PowerShell.svg";
|
||||
import CopilotIcon from "../../../images/QueryCopilotNewLogo.svg";
|
||||
import QuickStartIcon from "../../../images/Quickstart_Lightning.svg";
|
||||
@@ -151,9 +150,9 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
{useQueryCopilot.getState().copilotEnabled && (
|
||||
<SplashScreenButton
|
||||
imgSrc={CopilotIcon}
|
||||
title={"Query faster with Copilot"}
|
||||
title={"Query faster with Query Advisor"}
|
||||
description={
|
||||
"Copilot is your AI buddy that helps you write Azure Cosmos DB queries like a pro. Try it using our sample data set now!"
|
||||
"Query Advisor is your AI buddy that helps you write Azure Cosmos DB queries like a pro. Try it using our sample data set now!"
|
||||
}
|
||||
onClick={() => {
|
||||
const copilotVersion = userContext.features.copilotVersion;
|
||||
@@ -410,14 +409,6 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
},
|
||||
};
|
||||
heroes.push(launchQuickstartBtn);
|
||||
} else if (useNotebook.getState().isPhoenixNotebooks) {
|
||||
const newNotebookBtn = {
|
||||
iconSrc: NotebookColorIcon,
|
||||
title: "New notebook",
|
||||
description: "Visualize your data stored in Azure Cosmos DB",
|
||||
onClick: () => this.container.onNewNotebookClicked(),
|
||||
};
|
||||
heroes.push(newNotebookBtn);
|
||||
}
|
||||
|
||||
heroes.push(this.getShellCard());
|
||||
@@ -689,11 +680,20 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
title: "Learn the Fundamentals",
|
||||
description: "Watch Azure Cosmos DB Live TV show introductory and how to videos.",
|
||||
};
|
||||
let items: item[];
|
||||
|
||||
const commonItems: item[] = [
|
||||
{
|
||||
link: "https://learn.microsoft.com/azure/cosmos-db/data-explorer-shortcuts",
|
||||
title: "Data Explorer keyboard shortcuts",
|
||||
description: "Learn keyboard shortcuts to navigate Data Explorer.",
|
||||
},
|
||||
];
|
||||
|
||||
let apiItems: item[];
|
||||
switch (userContext.apiType) {
|
||||
case "SQL":
|
||||
case "Postgres":
|
||||
items = [
|
||||
apiItems = [
|
||||
{
|
||||
link: "https://aka.ms/msl-sdk-connect",
|
||||
title: "Get Started using an SDK",
|
||||
@@ -708,7 +708,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
];
|
||||
break;
|
||||
case "Mongo":
|
||||
items = [
|
||||
apiItems = [
|
||||
{
|
||||
link: "https://aka.ms/mongonodejs",
|
||||
title: "Build an app with Node.js",
|
||||
@@ -723,7 +723,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
];
|
||||
break;
|
||||
case "Cassandra":
|
||||
items = [
|
||||
apiItems = [
|
||||
{
|
||||
link: "https://aka.ms/cassandracontainer",
|
||||
title: "Create a Container",
|
||||
@@ -738,7 +738,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
];
|
||||
break;
|
||||
case "Gremlin":
|
||||
items = [
|
||||
apiItems = [
|
||||
{
|
||||
link: "https://aka.ms/graphquickstart",
|
||||
title: "Get Started ",
|
||||
@@ -753,7 +753,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
];
|
||||
break;
|
||||
case "Tables":
|
||||
items = [
|
||||
apiItems = [
|
||||
{
|
||||
link: "https://aka.ms/tabledotnet",
|
||||
title: "Build a .NET App",
|
||||
@@ -770,6 +770,9 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
const items = [...commonItems, ...apiItems];
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
{items.map((item, i) => (
|
||||
|
||||
@@ -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";
|
||||
@@ -19,7 +20,6 @@ import Explorer from "../Explorer";
|
||||
import * as TableConstants from "./Constants";
|
||||
import * as Entities from "./Entities";
|
||||
import * as TableEntityProcessor from "./TableEntityProcessor";
|
||||
import { CassandraProxyAPIs } from "../../Common/Constants";
|
||||
|
||||
export interface CassandraTableKeys {
|
||||
partitionKeys: CassandraTableKey[];
|
||||
@@ -172,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);
|
||||
@@ -406,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);
|
||||
@@ -444,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);
|
||||
@@ -458,7 +465,7 @@ export class CassandraAPIDataClient extends TableDataClient {
|
||||
}
|
||||
|
||||
public getTableKeys(collection: ViewModels.Collection): Q.Promise<CassandraTableKeys> {
|
||||
if (!this.useCassandraProxyEndpoint("getTableKeys")) {
|
||||
if (!this.useCassandraProxyEndpoint("getKeys")) {
|
||||
return this.getTableKeys_ToBeDeprecated(collection);
|
||||
}
|
||||
|
||||
@@ -493,8 +500,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);
|
||||
@@ -533,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);
|
||||
@@ -578,8 +587,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);
|
||||
@@ -618,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);
|
||||
@@ -732,17 +743,23 @@ export class CassandraAPIDataClient extends TableDataClient {
|
||||
}
|
||||
|
||||
private useCassandraProxyEndpoint(api: string): boolean {
|
||||
const activeCassandraProxyEndpoints: string[] = [
|
||||
CassandraProxyEndpoints.Development,
|
||||
CassandraProxyEndpoints.Mpac,
|
||||
CassandraProxyEndpoints.Prod,
|
||||
];
|
||||
let canAccessCassandraProxy: boolean = userContext.databaseAccount.properties.publicNetworkAccess === "Enabled";
|
||||
if (userContext.databaseAccount.properties.ipRules?.length > 0) {
|
||||
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) &&
|
||||
[Constants.CassandraProxyEndpoints.Development, Constants.CassandraProxyEndpoints.Mpac].includes(
|
||||
configContext.CASSANDRA_PROXY_ENDPOINT,
|
||||
)
|
||||
activeCassandraProxyEndpoints.includes(configContext.CASSANDRA_PROXY_ENDPOINT)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,7 +80,8 @@
|
||||
placeholder:isPreferredApiMongoDB?'Type a query predicate (e.g., {´a´:´foo´}), or choose one from the drop down list, or leave empty to query all documents.':'Type a query predicate (e.g., WHERE c.id=´1´), or choose one from the drop down list, or leave empty to query all documents.'
|
||||
},
|
||||
css: { placeholderVisible: filterContent().length === 0 },
|
||||
textInput: filterContent"
|
||||
textInput: filterContent,
|
||||
event: { keydown: onFilterKeyDown }"
|
||||
/>
|
||||
|
||||
<datalist
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ItemDefinition, PartitionKey, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
||||
import { Platform, configContext } from "ConfigContext";
|
||||
import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
||||
import { KeyboardAction, KeyboardActionGroup, KeyboardHandlerSetter, useKeyboardActionGroup } from "KeyboardShortcuts";
|
||||
import { QueryConstants } from "Shared/Constants";
|
||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||
import * as ko from "knockout";
|
||||
@@ -85,9 +86,11 @@ export default class DocumentsTab extends TabsBase {
|
||||
private _isQueryCopilotSampleContainer: boolean;
|
||||
private queryAbortController: AbortController;
|
||||
private cancelQueryTimeoutID: NodeJS.Timeout;
|
||||
private setKeyboardActions: KeyboardHandlerSetter;
|
||||
|
||||
constructor(options: ViewModels.DocumentsTabOptions) {
|
||||
super(options);
|
||||
this.setKeyboardActions = useKeyboardActionGroup(KeyboardActionGroup.ACTIVE_TAB);
|
||||
this.isPreferredApiMongoDB = userContext.apiType === "Mongo" || options.isPreferredApiMongoDB;
|
||||
|
||||
this.idHeader = this.isPreferredApiMongoDB ? "_id" : "id";
|
||||
@@ -462,7 +465,22 @@ export default class DocumentsTab extends TabsBase {
|
||||
|
||||
private initializeNewDocument = (): void => {
|
||||
this.selectedDocumentId(null);
|
||||
const defaultDocument: string = this.renderObjectForEditor({ id: "replace_with_new_document_id" }, null, 4);
|
||||
const newDocument: any = {
|
||||
id: "replace_with_new_document_id",
|
||||
};
|
||||
this.partitionKeyProperties.forEach((partitionKeyProperty) => {
|
||||
let target = newDocument;
|
||||
const keySegments = partitionKeyProperty.split(".");
|
||||
const finalSegment = keySegments.pop();
|
||||
|
||||
// Initialize nested objects as needed
|
||||
keySegments.forEach((segment) => {
|
||||
target = target[segment] = target[segment] || {};
|
||||
});
|
||||
|
||||
target[finalSegment] = "replace_with_new_partition_key_value";
|
||||
});
|
||||
const defaultDocument: string = this.renderObjectForEditor(newDocument, null, 4);
|
||||
this.initialDocumentContent(defaultDocument);
|
||||
this.selectedDocumentContent.setBaseline(defaultDocument);
|
||||
this.editorState(ViewModels.DocumentExplorerState.newDocumentValid);
|
||||
@@ -649,9 +667,38 @@ export default class DocumentsTab extends TabsBase {
|
||||
this.collection && this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Documents);
|
||||
}
|
||||
|
||||
public onFilterKeyDown(model: unknown, e: KeyboardEvent): boolean {
|
||||
if (e.key === "Enter") {
|
||||
this.refreshDocumentsGrid(true);
|
||||
|
||||
// Suppress the default behavior of the key
|
||||
return false;
|
||||
} else if (e.key === "Escape") {
|
||||
this.onHideFilterClick();
|
||||
|
||||
// Suppress the default behavior of the key
|
||||
return false;
|
||||
} else {
|
||||
// Allow the default behavior of the key
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public async onActivate(): Promise<void> {
|
||||
super.onActivate();
|
||||
|
||||
this.setKeyboardActions({
|
||||
[KeyboardAction.SEARCH]: () => {
|
||||
this.onShowFilterClick();
|
||||
return true;
|
||||
},
|
||||
[KeyboardAction.CLEAR_SEARCH]: () => {
|
||||
this.filterContent("");
|
||||
this.refreshDocumentsGrid(true);
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
if (!this._documentsIterator) {
|
||||
try {
|
||||
await this.autoPopulateContent();
|
||||
@@ -893,6 +940,7 @@ export default class DocumentsTab extends TabsBase {
|
||||
buttons.push({
|
||||
iconSrc: NewDocumentIcon,
|
||||
iconAlt: label,
|
||||
keyboardAction: KeyboardAction.NEW_ITEM,
|
||||
onCommandClick: this.onNewDocumentClick,
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
@@ -907,6 +955,7 @@ export default class DocumentsTab extends TabsBase {
|
||||
buttons.push({
|
||||
iconSrc: SaveIcon,
|
||||
iconAlt: label,
|
||||
keyboardAction: KeyboardAction.SAVE_ITEM,
|
||||
onCommandClick: this.onSaveNewDocumentClick,
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
@@ -921,6 +970,7 @@ export default class DocumentsTab extends TabsBase {
|
||||
buttons.push({
|
||||
iconSrc: DiscardIcon,
|
||||
iconAlt: label,
|
||||
keyboardAction: KeyboardAction.CANCEL_OR_DISCARD,
|
||||
onCommandClick: this.onRevertNewDocumentClick,
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
@@ -936,6 +986,7 @@ export default class DocumentsTab extends TabsBase {
|
||||
buttons.push({
|
||||
iconSrc: SaveIcon,
|
||||
iconAlt: label,
|
||||
keyboardAction: KeyboardAction.SAVE_ITEM,
|
||||
onCommandClick: this.onSaveExistingDocumentClick,
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
@@ -950,6 +1001,7 @@ export default class DocumentsTab extends TabsBase {
|
||||
buttons.push({
|
||||
iconSrc: DiscardIcon,
|
||||
iconAlt: label,
|
||||
keyboardAction: KeyboardAction.CANCEL_OR_DISCARD,
|
||||
onCommandClick: this.onRevertExisitingDocumentClick,
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
@@ -965,6 +1017,7 @@ export default class DocumentsTab extends TabsBase {
|
||||
buttons.push({
|
||||
iconSrc: DeleteDocumentIcon,
|
||||
iconAlt: label,
|
||||
keyboardAction: KeyboardAction.DELETE_ITEM,
|
||||
onCommandClick: this.onDeleteExisitingDocumentClick,
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useMongoProxyEndpoint } from "Common/MongoProxyClient";
|
||||
import React, { Component } from "react";
|
||||
import * as Constants from "../../../Common/Constants";
|
||||
import { configContext } from "../../../ConfigContext";
|
||||
@@ -9,7 +10,6 @@ import { isInvalidParentFrameOrigin, isReadyMessage } from "../../../Utils/Messa
|
||||
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../../Utils/NotificationConsoleUtils";
|
||||
import Explorer from "../../Explorer";
|
||||
import TabsBase from "../TabsBase";
|
||||
import { getMongoShellOrigin } from "./getMongoShellOrigin";
|
||||
import { getMongoShellUrl } from "./getMongoShellUrl";
|
||||
|
||||
//eslint-disable-next-line
|
||||
@@ -50,13 +50,15 @@ export default class MongoShellTabComponent extends Component<
|
||||
IMongoShellTabComponentStates
|
||||
> {
|
||||
private _logTraces: Map<string, number>;
|
||||
private _useMongoProxyEndpoint: boolean;
|
||||
|
||||
constructor(props: IMongoShellTabComponentProps) {
|
||||
super(props);
|
||||
this._logTraces = new Map();
|
||||
this._useMongoProxyEndpoint = useMongoProxyEndpoint("legacyMongoShell");
|
||||
|
||||
this.state = {
|
||||
url: getMongoShellUrl(),
|
||||
url: getMongoShellUrl(this._useMongoProxyEndpoint),
|
||||
};
|
||||
|
||||
props.onMongoShellTabAccessor({
|
||||
@@ -119,9 +121,10 @@ export default class MongoShellTabComponent extends Component<
|
||||
) + Constants.MongoDBAccounts.defaultPort.toString();
|
||||
const databaseId = this.props.collection.databaseId;
|
||||
const collectionId = this.props.collection.id();
|
||||
const apiEndpoint = configContext.BACKEND_ENDPOINT;
|
||||
const apiEndpoint = this._useMongoProxyEndpoint
|
||||
? configContext.MONGO_PROXY_ENDPOINT
|
||||
: configContext.BACKEND_ENDPOINT;
|
||||
const encryptedAuthToken: string = userContext.accessToken;
|
||||
const targetOrigin = getMongoShellOrigin();
|
||||
|
||||
shellIframe.contentWindow.postMessage(
|
||||
{
|
||||
@@ -137,7 +140,7 @@ export default class MongoShellTabComponent extends Component<
|
||||
apiEndpoint: apiEndpoint,
|
||||
},
|
||||
},
|
||||
targetOrigin,
|
||||
window.origin,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
import { extractFeatures } from "Platform/Hosted/extractFeatures";
|
||||
import { configContext } from "../../../ConfigContext";
|
||||
import { updateUserContext } from "../../../UserContext";
|
||||
import { getMongoShellOrigin } from "./getMongoShellOrigin";
|
||||
|
||||
describe("getMongoShellOrigin", () => {
|
||||
(window as { origin: string }).origin = "window_origin";
|
||||
|
||||
beforeEach(() => {
|
||||
updateUserContext({
|
||||
features: extractFeatures(
|
||||
new URLSearchParams({
|
||||
"feature.enableLegacyMongoShellV1": "false",
|
||||
"feature.enableLegacyMongoShellV2": "false",
|
||||
"feature.enableLegacyMongoShellV1Debug": "false",
|
||||
"feature.enableLegacyMongoShellV2Debug": "false",
|
||||
"feature.loadLegacyMongoShellFromBE": "false",
|
||||
}),
|
||||
),
|
||||
});
|
||||
});
|
||||
|
||||
it("should return by default", () => {
|
||||
expect(getMongoShellOrigin()).toBe(window.origin);
|
||||
});
|
||||
|
||||
it("should return window.origin when enableLegacyMongoShellV1", () => {
|
||||
updateUserContext({
|
||||
features: extractFeatures(
|
||||
new URLSearchParams({
|
||||
"feature.enableLegacyMongoShellV1": "true",
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
expect(getMongoShellOrigin()).toBe(window.origin);
|
||||
});
|
||||
|
||||
it("should return window.origin when enableLegacyMongoShellV2===true", () => {
|
||||
updateUserContext({
|
||||
features: extractFeatures(
|
||||
new URLSearchParams({
|
||||
"feature.enableLegacyMongoShellV2": "true",
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
expect(getMongoShellOrigin()).toBe(window.origin);
|
||||
});
|
||||
|
||||
it("should return window.origin when enableLegacyMongoShellV1Debug===true", () => {
|
||||
updateUserContext({
|
||||
features: extractFeatures(
|
||||
new URLSearchParams({
|
||||
"feature.enableLegacyMongoShellV1Debug": "true",
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
expect(getMongoShellOrigin()).toBe(window.origin);
|
||||
});
|
||||
|
||||
it("should return window.origin when enableLegacyMongoShellV2Debug===true", () => {
|
||||
updateUserContext({
|
||||
features: extractFeatures(
|
||||
new URLSearchParams({
|
||||
"feature.enableLegacyMongoShellV2Debug": "true",
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
expect(getMongoShellOrigin()).toBe(window.origin);
|
||||
});
|
||||
|
||||
it("should return BACKEND_ENDPOINT when loadLegacyMongoShellFromBE===true", () => {
|
||||
updateUserContext({
|
||||
features: extractFeatures(
|
||||
new URLSearchParams({
|
||||
"feature.loadLegacyMongoShellFromBE": "true",
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
expect(getMongoShellOrigin()).toBe(configContext.BACKEND_ENDPOINT);
|
||||
});
|
||||
});
|
||||
@@ -1,10 +0,0 @@
|
||||
import { configContext } from "../../../ConfigContext";
|
||||
import { userContext } from "../../../UserContext";
|
||||
|
||||
export function getMongoShellOrigin(): string {
|
||||
if (userContext.features.loadLegacyMongoShellFromBE === true) {
|
||||
return configContext.BACKEND_ENDPOINT;
|
||||
}
|
||||
|
||||
return window.origin;
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import { extractFeatures } from "Platform/Hosted/extractFeatures";
|
||||
import { Platform, configContext, resetConfigContext, updateConfigContext } from "../../../ConfigContext";
|
||||
import { Platform, resetConfigContext, updateConfigContext } from "../../../ConfigContext";
|
||||
import { updateUserContext, userContext } from "../../../UserContext";
|
||||
import { getExtensionEndpoint, getMongoShellUrl } from "./getMongoShellUrl";
|
||||
import { getMongoShellUrl } from "./getMongoShellUrl";
|
||||
|
||||
const mongoBackendEndpoint = "https://localhost:1234";
|
||||
|
||||
@@ -32,175 +31,18 @@ describe("getMongoShellUrl", () => {
|
||||
cassandraEndpoint: "fakeCassandraEndpoint",
|
||||
},
|
||||
},
|
||||
features: extractFeatures(
|
||||
new URLSearchParams({
|
||||
"feature.enableLegacyMongoShellV1": "false",
|
||||
"feature.enableLegacyMongoShellV2": "false",
|
||||
"feature.enableLegacyMongoShellV1Debug": "false",
|
||||
"feature.enableLegacyMongoShellV2Debug": "false",
|
||||
"feature.loadLegacyMongoShellFromBE": "false",
|
||||
}),
|
||||
),
|
||||
portalEnv: "prod",
|
||||
});
|
||||
|
||||
queryString = `resourceId=${userContext.databaseAccount.id}&accountName=${userContext.databaseAccount.name}&mongoEndpoint=${userContext.databaseAccount.properties.documentEndpoint}`;
|
||||
});
|
||||
|
||||
it("should return /mongoshell/indexv2.html by default", () => {
|
||||
expect(getMongoShellUrl()).toBe(`/mongoshell/indexv2.html?${queryString}`);
|
||||
it("should return /indexv2.html by default", () => {
|
||||
expect(getMongoShellUrl().toString()).toContain(`/indexv2.html?${queryString}`);
|
||||
});
|
||||
|
||||
it("should return /mongoshell/indexv2.html when portalEnv==localhost", () => {
|
||||
updateUserContext({
|
||||
portalEnv: "localhost",
|
||||
});
|
||||
|
||||
expect(getMongoShellUrl()).toBe(`/mongoshell/indexv2.html?${queryString}`);
|
||||
});
|
||||
|
||||
it("should return /mongoshell/index.html when enableLegacyMongoShellV1===true", () => {
|
||||
updateUserContext({
|
||||
features: extractFeatures(
|
||||
new URLSearchParams({
|
||||
"feature.enableLegacyMongoShellV1": "true",
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
expect(getMongoShellUrl()).toBe(`/mongoshell/index.html?${queryString}`);
|
||||
});
|
||||
|
||||
it("should return /mongoshell/index.html when enableLegacyMongoShellV2===true", () => {
|
||||
updateUserContext({
|
||||
features: extractFeatures(
|
||||
new URLSearchParams({
|
||||
"feature.enableLegacyMongoShellV2": "true",
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
expect(getMongoShellUrl()).toBe(`/mongoshell/indexv2.html?${queryString}`);
|
||||
});
|
||||
|
||||
it("should return /mongoshell/index.html when enableLegacyMongoShellV1Debug===true", () => {
|
||||
updateUserContext({
|
||||
features: extractFeatures(
|
||||
new URLSearchParams({
|
||||
"feature.enableLegacyMongoShellV1Debug": "true",
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
expect(getMongoShellUrl()).toBe(`/mongoshell/debug/index.html?${queryString}`);
|
||||
});
|
||||
|
||||
it("should return /mongoshell/index.html when enableLegacyMongoShellV2Debug===true", () => {
|
||||
updateUserContext({
|
||||
features: extractFeatures(
|
||||
new URLSearchParams({
|
||||
"feature.enableLegacyMongoShellV2Debug": "true",
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
expect(getMongoShellUrl()).toBe(`/mongoshell/debug/indexv2.html?${queryString}`);
|
||||
});
|
||||
|
||||
describe("loadLegacyMongoShellFromBE===true", () => {
|
||||
beforeEach(() => {
|
||||
resetConfigContext();
|
||||
updateConfigContext({
|
||||
BACKEND_ENDPOINT: mongoBackendEndpoint,
|
||||
platform: Platform.Hosted,
|
||||
});
|
||||
|
||||
updateUserContext({
|
||||
features: extractFeatures(
|
||||
new URLSearchParams({
|
||||
"feature.loadLegacyMongoShellFromBE": "true",
|
||||
}),
|
||||
),
|
||||
});
|
||||
});
|
||||
|
||||
it("should return /mongoshell/index.html", () => {
|
||||
const endpoint = getExtensionEndpoint(configContext.platform, configContext.BACKEND_ENDPOINT);
|
||||
expect(getMongoShellUrl()).toBe(`${endpoint}/content/mongoshell/debug/index.html?${queryString}`);
|
||||
});
|
||||
|
||||
it("configContext.platform !== Platform.Hosted, should return /mongoshell/indexv2.html", () => {
|
||||
updateConfigContext({
|
||||
platform: Platform.Portal,
|
||||
});
|
||||
|
||||
const endpoint = getExtensionEndpoint(configContext.platform, configContext.BACKEND_ENDPOINT);
|
||||
expect(getMongoShellUrl()).toBe(`${endpoint}/content/mongoshell/debug/index.html?${queryString}`);
|
||||
});
|
||||
|
||||
it("configContext.BACKEND_ENDPOINT !== '' and configContext.platform !== Platform.Hosted, should return /mongoshell/indexv2.html", () => {
|
||||
resetConfigContext();
|
||||
updateConfigContext({
|
||||
platform: Platform.Portal,
|
||||
BACKEND_ENDPOINT: mongoBackendEndpoint,
|
||||
});
|
||||
|
||||
const endpoint = getExtensionEndpoint(configContext.platform, configContext.BACKEND_ENDPOINT);
|
||||
expect(getMongoShellUrl()).toBe(`${endpoint}/content/mongoshell/debug/index.html?${queryString}`);
|
||||
});
|
||||
|
||||
it("configContext.BACKEND_ENDPOINT === '' and configContext.platform === Platform.Hosted, should return /mongoshell/indexv2.html", () => {
|
||||
resetConfigContext();
|
||||
updateConfigContext({
|
||||
platform: Platform.Hosted,
|
||||
});
|
||||
|
||||
const endpoint = getExtensionEndpoint(configContext.platform, configContext.BACKEND_ENDPOINT);
|
||||
expect(getMongoShellUrl()).toBe(`${endpoint}/content/mongoshell/debug/index.html?${queryString}`);
|
||||
});
|
||||
|
||||
it("configContext.BACKEND_ENDPOINT === '' and configContext.platform !== Platform.Hosted, should return /mongoshell/indexv2.html", () => {
|
||||
resetConfigContext();
|
||||
updateConfigContext({
|
||||
platform: Platform.Portal,
|
||||
});
|
||||
|
||||
const endpoint = getExtensionEndpoint(configContext.platform, configContext.BACKEND_ENDPOINT);
|
||||
expect(getMongoShellUrl()).toBe(`${endpoint}/content/mongoshell/debug/index.html?${queryString}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("getExtensionEndpoint", () => {
|
||||
it("when platform === Platform.Hosted, backendEndpoint is undefined", () => {
|
||||
expect(getExtensionEndpoint(Platform.Hosted, undefined)).toBe("");
|
||||
});
|
||||
|
||||
it("when platform === Platform.Hosted, backendEndpoint === ''", () => {
|
||||
expect(getExtensionEndpoint(Platform.Hosted, "")).toBe("");
|
||||
});
|
||||
|
||||
it("when platform === Platform.Hosted, backendEndpoint === null", () => {
|
||||
expect(getExtensionEndpoint(Platform.Hosted, null)).toBe("");
|
||||
});
|
||||
|
||||
it("when platform === Platform.Hosted, backendEndpoint != ''", () => {
|
||||
expect(getExtensionEndpoint(Platform.Hosted, "foo")).toBe("foo");
|
||||
});
|
||||
|
||||
it("when platform === Platform.Portal, backendEndpoint is udefined", () => {
|
||||
expect(getExtensionEndpoint(Platform.Portal, undefined)).toBe("");
|
||||
});
|
||||
|
||||
it("when platform === Platform.Portal, backendEndpoint === ''", () => {
|
||||
expect(getExtensionEndpoint(Platform.Portal, "")).toBe("");
|
||||
});
|
||||
|
||||
it("when platform === Platform.Portal, backendEndpoint === null", () => {
|
||||
expect(getExtensionEndpoint(Platform.Portal, null)).toBe("");
|
||||
});
|
||||
|
||||
it("when platform !== Platform.Portal, backendEndpoint != ''", () => {
|
||||
expect(getExtensionEndpoint(Platform.Portal, "foo")).toBe("foo");
|
||||
it("should return /index.html when useMongoProxyEndpoint is true", () => {
|
||||
const useMongoProxyEndpoint: boolean = true;
|
||||
expect(getMongoShellUrl(useMongoProxyEndpoint).toString()).toContain(`/index.html?${queryString}`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,45 +1,11 @@
|
||||
import { configContext, Platform } from "../../../ConfigContext";
|
||||
import { userContext } from "../../../UserContext";
|
||||
|
||||
export function getMongoShellUrl(): string {
|
||||
export function getMongoShellUrl(useMongoProxyEndpoint?: boolean): string {
|
||||
const { databaseAccount: account } = userContext;
|
||||
const resourceId = account?.id;
|
||||
const accountName = account?.name;
|
||||
const mongoEndpoint = account?.properties?.mongoEndpoint || account?.properties?.documentEndpoint;
|
||||
const queryString = `resourceId=${resourceId}&accountName=${accountName}&mongoEndpoint=${mongoEndpoint}`;
|
||||
|
||||
if (userContext.features.enableLegacyMongoShellV1 === true) {
|
||||
return `/mongoshell/index.html?${queryString}`;
|
||||
}
|
||||
|
||||
if (userContext.features.enableLegacyMongoShellV1Debug === true) {
|
||||
return `/mongoshell/debug/index.html?${queryString}`;
|
||||
}
|
||||
|
||||
if (userContext.features.enableLegacyMongoShellV2 === true) {
|
||||
return `/mongoshell/indexv2.html?${queryString}`;
|
||||
}
|
||||
|
||||
if (userContext.features.enableLegacyMongoShellV2Debug === true) {
|
||||
return `/mongoshell/debug/indexv2.html?${queryString}`;
|
||||
}
|
||||
|
||||
if (userContext.portalEnv === "localhost") {
|
||||
return `/mongoshell/indexv2.html?${queryString}`;
|
||||
}
|
||||
|
||||
if (userContext.features.loadLegacyMongoShellFromBE === true) {
|
||||
const extensionEndpoint: string = getExtensionEndpoint(configContext.platform, configContext.BACKEND_ENDPOINT);
|
||||
return `${extensionEndpoint}/content/mongoshell/debug/index.html?${queryString}`;
|
||||
}
|
||||
|
||||
return `/mongoshell/indexv2.html?${queryString}`;
|
||||
}
|
||||
|
||||
export function getExtensionEndpoint(platform: string, backendEndpoint: string): string {
|
||||
const runtimeEndpoint = platform === Platform.Hosted ? backendEndpoint : "";
|
||||
|
||||
const extensionEndpoint: string = backendEndpoint || runtimeEndpoint || "";
|
||||
|
||||
return extensionEndpoint;
|
||||
return useMongoProxyEndpoint ? `/mongoshell/index.html?${queryString}` : `/mongoshell/indexv2.html?${queryString}`;
|
||||
}
|
||||
|
||||
@@ -381,9 +381,13 @@ export const QueryResultSection: React.FC<QueryResultProps> = ({
|
||||
<img className="paneErrorIcon" src={InfoColor} alt="Error" />
|
||||
</span>
|
||||
<span className="warningErrorDetailsLinkContainer">
|
||||
We have detected you may be using a subquery. Non-correlated subqueries are not currently supported.
|
||||
<a href="https://docs.microsoft.com/en-us/azure/cosmos-db/sql-query-subquery">
|
||||
Please see Cosmos sub query documentation for further information
|
||||
We detected you may be using a subquery. To learn more about subqueries effectively,{" "}
|
||||
<a
|
||||
href="https://learn.microsoft.com/azure/cosmos-db/nosql/query/subquery"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
visit the documentation
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -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?.(),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { OnExecuteQueryClick, QueryDocumentsPerPage } from "Explorer/QueryCopilo
|
||||
import { QueryCopilotSidebar } from "Explorer/QueryCopilot/V2/Sidebar/QueryCopilotSidebar";
|
||||
import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection";
|
||||
import { useSelectedNode } from "Explorer/useSelectedNode";
|
||||
import { KeyboardAction } from "KeyboardShortcuts";
|
||||
import { QueryConstants } from "Shared/Constants";
|
||||
import { LocalStorageUtility, StorageKey, getRUThreshold, ruThresholdEnabled } from "Shared/StorageUtility";
|
||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||
@@ -21,6 +22,7 @@ import "react-splitter-layout/lib/index.css";
|
||||
import { format } from "react-string-format";
|
||||
import QueryCommandIcon from "../../../../images/CopilotCommand.svg";
|
||||
import LaunchCopilot from "../../../../images/CopilotTabIcon.svg";
|
||||
import DownloadQueryIcon from "../../../../images/DownloadQuery.svg";
|
||||
import CancelQueryIcon from "../../../../images/Entity_cancel.svg";
|
||||
import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg";
|
||||
import SaveQueryIcon from "../../../../images/save-cosmos.svg";
|
||||
@@ -134,7 +136,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: "",
|
||||
@@ -224,6 +226,20 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||
}
|
||||
};
|
||||
|
||||
public onDownloadQueryClick = (): void => {
|
||||
const text = this.getCurrentEditorQuery();
|
||||
const queryFile = new File([text], `SavedQuery.txt`, { type: "text/plain" });
|
||||
|
||||
// It appears the most consistent to download a file from a blob is to create an anchor element and simulate clicking it
|
||||
const blobUrl = URL.createObjectURL(queryFile);
|
||||
const anchor = document.createElement("a");
|
||||
anchor.href = blobUrl;
|
||||
anchor.download = queryFile.name;
|
||||
document.body.appendChild(anchor); // Must put the anchor in the document.
|
||||
anchor.click();
|
||||
document.body.removeChild(anchor); // Clean up the anchor.
|
||||
};
|
||||
|
||||
public onSaveQueryClick = (): void => {
|
||||
useSidePanel.getState().openSidePanel("Save Query", <SaveQueryPane explorer={this.props.collection.container} />);
|
||||
};
|
||||
@@ -393,6 +409,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||
buttons.push({
|
||||
iconSrc: ExecuteQueryIcon,
|
||||
iconAlt: label,
|
||||
keyboardAction: KeyboardAction.EXECUTE_ITEM,
|
||||
onCommandClick: this.props.isSampleCopilotActive
|
||||
? () => OnExecuteQueryClick(this.props.copilotStore)
|
||||
: this.onExecuteQueryClick,
|
||||
@@ -403,14 +420,28 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||
});
|
||||
}
|
||||
|
||||
if (this.saveQueryButton.visible && configContext.platform !== Platform.Fabric) {
|
||||
const label = "Save Query";
|
||||
if (this.saveQueryButton.visible) {
|
||||
if (configContext.platform !== Platform.Fabric) {
|
||||
const label = "Save Query";
|
||||
buttons.push({
|
||||
iconSrc: SaveQueryIcon,
|
||||
iconAlt: label,
|
||||
keyboardAction: KeyboardAction.SAVE_ITEM,
|
||||
onCommandClick: this.onSaveQueryClick,
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: !this.saveQueryButton.enabled,
|
||||
});
|
||||
}
|
||||
|
||||
buttons.push({
|
||||
iconSrc: SaveQueryIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: this.onSaveQueryClick,
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
iconSrc: DownloadQueryIcon,
|
||||
iconAlt: "Download Query",
|
||||
keyboardAction: KeyboardAction.DOWNLOAD_ITEM,
|
||||
onCommandClick: this.onDownloadQueryClick,
|
||||
commandButtonLabel: "Download Query",
|
||||
ariaLabel: "Download Query",
|
||||
hasPopup: false,
|
||||
disabled: !this.saveQueryButton.enabled,
|
||||
});
|
||||
@@ -437,7 +468,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||
hasPopup: false,
|
||||
};
|
||||
|
||||
const launchCopilotButton = {
|
||||
const launchCopilotButton: CommandButtonComponentProps = {
|
||||
iconSrc: LaunchCopilot,
|
||||
iconAlt: mainButtonLabel,
|
||||
onCommandClick: this.launchQueryCopilotChat,
|
||||
@@ -450,14 +481,15 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||
}
|
||||
|
||||
if (this.props.copilotEnabled) {
|
||||
const toggleCopilotButton = {
|
||||
const toggleCopilotButton: CommandButtonComponentProps = {
|
||||
iconSrc: QueryCommandIcon,
|
||||
iconAlt: "Copilot",
|
||||
iconAlt: "Query Advisor",
|
||||
keyboardAction: KeyboardAction.TOGGLE_COPILOT,
|
||||
onCommandClick: () => {
|
||||
this._toggleCopilot(!this.state.copilotActive);
|
||||
},
|
||||
commandButtonLabel: this.state.copilotActive ? "Disable Copilot" : "Enable Copilot",
|
||||
ariaLabel: this.state.copilotActive ? "Disable Copilot" : "Enable Copilot",
|
||||
commandButtonLabel: this.state.copilotActive ? "Disable Query Advisor" : "Enable Query Advisor",
|
||||
ariaLabel: this.state.copilotActive ? "Disable Query Advisor" : "Enable Query Advisor",
|
||||
hasPopup: false,
|
||||
};
|
||||
buttons.push(toggleCopilotButton);
|
||||
@@ -468,6 +500,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||
buttons.push({
|
||||
iconSrc: CancelQueryIcon,
|
||||
iconAlt: label,
|
||||
keyboardAction: KeyboardAction.CANCEL_OR_DISCARD,
|
||||
onCommandClick: () => this.queryAbortController.abort(),
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
@@ -496,13 +529,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 = {
|
||||
@@ -517,6 +553,8 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||
}
|
||||
}
|
||||
|
||||
this.saveQueryButton.enabled = newContent.length > 0;
|
||||
|
||||
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
||||
}
|
||||
|
||||
@@ -544,7 +582,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 +639,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,5 +1,6 @@
|
||||
import { Resource, StoredProcedureDefinition } from "@azure/cosmos";
|
||||
import { Pivot, PivotItem } from "@fluentui/react";
|
||||
import { KeyboardAction } from "KeyboardShortcuts";
|
||||
import React from "react";
|
||||
import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg";
|
||||
import DiscardIcon from "../../../../images/discard.svg";
|
||||
@@ -321,6 +322,7 @@ export default class StoredProcedureTabComponent extends React.Component<
|
||||
buttons.push({
|
||||
iconSrc: SaveIcon,
|
||||
iconAlt: label,
|
||||
keyboardAction: KeyboardAction.SAVE_ITEM,
|
||||
onCommandClick: this.onSaveClick,
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
@@ -334,6 +336,7 @@ export default class StoredProcedureTabComponent extends React.Component<
|
||||
buttons.push({
|
||||
iconSrc: SaveIcon,
|
||||
iconAlt: label,
|
||||
keyboardAction: KeyboardAction.SAVE_ITEM,
|
||||
onCommandClick: this.onUpdateClick,
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
@@ -347,6 +350,7 @@ export default class StoredProcedureTabComponent extends React.Component<
|
||||
buttons.push({
|
||||
iconSrc: DiscardIcon,
|
||||
iconAlt: label,
|
||||
keyboardAction: KeyboardAction.CANCEL_OR_DISCARD,
|
||||
onCommandClick: this.onDiscard,
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
@@ -360,6 +364,7 @@ export default class StoredProcedureTabComponent extends React.Component<
|
||||
buttons.push({
|
||||
iconSrc: ExecuteQueryIcon,
|
||||
iconAlt: label,
|
||||
keyboardAction: KeyboardAction.EXECUTE_ITEM,
|
||||
onCommandClick: () => {
|
||||
this.collection.container.openExecuteSprocParamsPanel(this.node);
|
||||
},
|
||||
|
||||
@@ -6,6 +6,7 @@ import { IpRule } from "Contracts/DataModels";
|
||||
import { MessageTypes } from "Contracts/ExplorerContracts";
|
||||
import { CollectionTabKind } from "Contracts/ViewModels";
|
||||
import Explorer from "Explorer/Explorer";
|
||||
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
||||
import { QueryCopilotTab } from "Explorer/QueryCopilot/QueryCopilotTab";
|
||||
import { SplashScreen } from "Explorer/SplashScreen/SplashScreen";
|
||||
import { ConnectTab } from "Explorer/Tabs/ConnectTab";
|
||||
@@ -13,6 +14,7 @@ import { PostgresConnectTab } from "Explorer/Tabs/PostgresConnectTab";
|
||||
import { QuickstartTab } from "Explorer/Tabs/QuickstartTab";
|
||||
import { VcoreMongoConnectTab } from "Explorer/Tabs/VCoreMongoConnectTab";
|
||||
import { VcoreMongoQuickstartTab } from "Explorer/Tabs/VCoreMongoQuickstartTab";
|
||||
import { KeyboardAction, KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
|
||||
import { hasRUThresholdBeenConfigured } from "Shared/StorageUtility";
|
||||
import { userContext } from "UserContext";
|
||||
import { CassandraProxyOutboundIPs, MongoProxyOutboundIPs, PortalBackendIPs } from "Utils/EndpointUtils";
|
||||
@@ -41,6 +43,16 @@ export const Tabs = ({ explorer }: TabsProps): JSX.Element => {
|
||||
showMongoAndCassandraProxiesNetworkSettingsWarningState,
|
||||
setShowMongoAndCassandraProxiesNetworkSettingsWarningState,
|
||||
] = useState<boolean>(showMongoAndCassandraProxiesNetworkSettingsWarning());
|
||||
|
||||
const setKeyboardHandlers = useKeyboardActionGroup(KeyboardActionGroup.TABS);
|
||||
useEffect(() => {
|
||||
setKeyboardHandlers({
|
||||
[KeyboardAction.SELECT_LEFT_TAB]: () => useTabs.getState().selectLeftTab(),
|
||||
[KeyboardAction.SELECT_RIGHT_TAB]: () => useTabs.getState().selectRightTab(),
|
||||
[KeyboardAction.CLOSE_TAB]: () => useTabs.getState().closeActiveTab(),
|
||||
});
|
||||
}, [setKeyboardHandlers]);
|
||||
|
||||
return (
|
||||
<div className="tabsManagerContainer">
|
||||
{networkSettingsWarning && (
|
||||
@@ -297,6 +309,9 @@ const isQueryErrorThrown = (tab?: Tab, tabKind?: ReactTabKind): boolean => {
|
||||
};
|
||||
|
||||
const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): JSX.Element => {
|
||||
// React tabs have no context buttons.
|
||||
useCommandBar.getState().setContextButtons([]);
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
switch (activeReactTab) {
|
||||
case ReactTabKind.Connect:
|
||||
@@ -324,13 +339,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.Local) ||
|
||||
(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;
|
||||
}
|
||||
@@ -344,9 +363,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({
|
||||
@@ -368,9 +387,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;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { KeyboardActionGroup, clearKeyboardActionGroup } from "KeyboardShortcuts";
|
||||
import * as ko from "knockout";
|
||||
import * as Constants from "../../Common/Constants";
|
||||
import * as ThemeUtility from "../../Common/ThemeUtility";
|
||||
@@ -107,6 +108,7 @@ export default class TabsBase extends WaitsForTemplateViewModel {
|
||||
}
|
||||
|
||||
public onActivate(): void {
|
||||
clearKeyboardActionGroup(KeyboardActionGroup.ACTIVE_TAB);
|
||||
this.updateSelectedNode();
|
||||
this.collection?.selectedSubnodeKind(this.tabKind);
|
||||
this.database?.selectedSubnodeKind(this.tabKind);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { TriggerDefinition } from "@azure/cosmos";
|
||||
import { Dropdown, IDropdownOption, Label, TextField } from "@fluentui/react";
|
||||
import { KeyboardAction } from "KeyboardShortcuts";
|
||||
import React, { Component } from "react";
|
||||
import DiscardIcon from "../../../images/discard.svg";
|
||||
import SaveIcon from "../../../images/save-cosmos.svg";
|
||||
@@ -218,6 +219,18 @@ export class TriggerTabContent extends Component<TriggerTab, ITriggerTabContentS
|
||||
return !!value;
|
||||
}
|
||||
|
||||
componentDidUpdate(_prevProps: TriggerTab, prevState: ITriggerTabContentState): void {
|
||||
const { triggerBody, triggerId, triggerType, triggerOperation } = this.state;
|
||||
if (
|
||||
triggerId !== prevState.triggerId ||
|
||||
triggerBody !== prevState.triggerBody ||
|
||||
triggerType !== prevState.triggerType ||
|
||||
triggerOperation !== prevState.triggerOperation
|
||||
) {
|
||||
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
||||
}
|
||||
}
|
||||
|
||||
protected getTabsButtons(): CommandButtonComponentProps[] {
|
||||
const buttons: CommandButtonComponentProps[] = [];
|
||||
const label = "Save";
|
||||
@@ -227,6 +240,7 @@ export class TriggerTabContent extends Component<TriggerTab, ITriggerTabContentS
|
||||
...this,
|
||||
iconSrc: SaveIcon,
|
||||
iconAlt: label,
|
||||
keyboardAction: KeyboardAction.SAVE_ITEM,
|
||||
onCommandClick: this.onSaveClick,
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
@@ -241,6 +255,7 @@ export class TriggerTabContent extends Component<TriggerTab, ITriggerTabContentS
|
||||
...this,
|
||||
iconSrc: SaveIcon,
|
||||
iconAlt: label,
|
||||
keyboardAction: KeyboardAction.SAVE_ITEM,
|
||||
onCommandClick: this.onUpdateClick,
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
@@ -256,6 +271,7 @@ export class TriggerTabContent extends Component<TriggerTab, ITriggerTabContentS
|
||||
...this,
|
||||
iconSrc: DiscardIcon,
|
||||
iconAlt: label,
|
||||
keyboardAction: KeyboardAction.CANCEL_OR_DISCARD,
|
||||
onCommandClick: this.onDiscard,
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
@@ -287,7 +303,6 @@ export class TriggerTabContent extends Component<TriggerTab, ITriggerTabContentS
|
||||
};
|
||||
|
||||
render(): JSX.Element {
|
||||
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
||||
const { triggerId, triggerType, triggerOperation, triggerBody, isIdEditable } = this.state;
|
||||
return (
|
||||
<div className="tab-pane flexContainer trigger-form" role="tabpanel">
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { UserDefinedFunctionDefinition } from "@azure/cosmos";
|
||||
import { Label, TextField } from "@fluentui/react";
|
||||
import { KeyboardAction } from "KeyboardShortcuts";
|
||||
import React, { Component } from "react";
|
||||
import DiscardIcon from "../../../images/discard.svg";
|
||||
import SaveIcon from "../../../images/save-cosmos.svg";
|
||||
import * as Constants from "../../Common/Constants";
|
||||
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
||||
import { createUserDefinedFunction } from "../../Common/dataAccess/createUserDefinedFunction";
|
||||
import { updateUserDefinedFunction } from "../../Common/dataAccess/updateUserDefinedFunction";
|
||||
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
@@ -80,6 +81,7 @@ export default class UserDefinedFunctionTabContent extends Component<
|
||||
setState: this.setState,
|
||||
iconSrc: SaveIcon,
|
||||
iconAlt: label,
|
||||
keyboardAction: KeyboardAction.SAVE_ITEM,
|
||||
onCommandClick: this.onSaveClick,
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
@@ -94,6 +96,7 @@ export default class UserDefinedFunctionTabContent extends Component<
|
||||
...this,
|
||||
iconSrc: SaveIcon,
|
||||
iconAlt: label,
|
||||
keyboardAction: KeyboardAction.SAVE_ITEM,
|
||||
onCommandClick: this.onUpdateClick,
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
@@ -109,6 +112,7 @@ export default class UserDefinedFunctionTabContent extends Component<
|
||||
...this,
|
||||
iconSrc: DiscardIcon,
|
||||
iconAlt: label,
|
||||
keyboardAction: KeyboardAction.CANCEL_OR_DISCARD,
|
||||
onCommandClick: this.onDiscard,
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react";
|
||||
import { SampleDataTree } from "Explorer/Tree/SampleDataTree";
|
||||
import { getItemName } from "Utils/APITypeUtils";
|
||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||
import * as React from "react";
|
||||
import shallow from "zustand/shallow";
|
||||
import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg";
|
||||
import GalleryIcon from "../../../images/GalleryIcon.svg";
|
||||
import DeleteIcon from "../../../images/delete.svg";
|
||||
import CopyIcon from "../../../images/notebook/Notebook-copy.svg";
|
||||
import NewNotebookIcon from "../../../images/notebook/Notebook-new.svg";
|
||||
@@ -14,17 +12,14 @@ import FileIcon from "../../../images/notebook/file-cosmos.svg";
|
||||
import PublishIcon from "../../../images/notebook/publish_content.svg";
|
||||
import RefreshIcon from "../../../images/refresh-cosmos.svg";
|
||||
import CollectionIcon from "../../../images/tree-collection.svg";
|
||||
import { Areas, ConnectionStatusType, Notebook } from "../../Common/Constants";
|
||||
import { isPublicInternetAccessAllowed } from "../../Common/DatabaseAccountUtility";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility";
|
||||
import { Action, ActionModifiers, Source } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { isServerlessAccount } from "../../Utils/CapabilityUtils";
|
||||
import * as GitHubUtils from "../../Utils/GitHubUtils";
|
||||
import { useSidePanel } from "../../hooks/useSidePanel";
|
||||
import { useTabs } from "../../hooks/useTabs";
|
||||
import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory";
|
||||
import { AccordionComponent, AccordionItemComponent } from "../Controls/Accordion/AccordionComponent";
|
||||
@@ -36,7 +31,6 @@ import { mostRecentActivity } from "../MostRecentActivity/MostRecentActivity";
|
||||
import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem";
|
||||
import { NotebookUtil } from "../Notebook/NotebookUtil";
|
||||
import { useNotebook } from "../Notebook/useNotebook";
|
||||
import { GitHubReposPanel } from "../Panes/GitHubReposPanel/GitHubReposPanel";
|
||||
import TabsBase from "../Tabs/TabsBase";
|
||||
import { useDatabases } from "../useDatabases";
|
||||
import { useSelectedNode } from "../useSelectedNode";
|
||||
@@ -75,152 +69,6 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
|
||||
configContext.platform !== Platform.Fabric && (userContext.apiType === "SQL" || userContext.apiType === "Gremlin");
|
||||
const pseudoDirPath = "PsuedoDir";
|
||||
|
||||
const buildGalleryCallout = (): JSX.Element => {
|
||||
if (
|
||||
LocalStorageUtility.hasItem(StorageKey.GalleryCalloutDismissed) &&
|
||||
LocalStorageUtility.getEntryBoolean(StorageKey.GalleryCalloutDismissed)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const calloutProps: ICalloutProps = {
|
||||
calloutMaxWidth: 350,
|
||||
ariaLabel: "New gallery",
|
||||
role: "alertdialog",
|
||||
gapSpace: 0,
|
||||
target: ".galleryHeader",
|
||||
directionalHint: DirectionalHint.leftTopEdge,
|
||||
onDismiss: () => {
|
||||
LocalStorageUtility.setEntryBoolean(StorageKey.GalleryCalloutDismissed, true);
|
||||
},
|
||||
setInitialFocus: true,
|
||||
};
|
||||
|
||||
const openGalleryProps: ILinkProps = {
|
||||
onClick: () => {
|
||||
LocalStorageUtility.setEntryBoolean(StorageKey.GalleryCalloutDismissed, true);
|
||||
container.openGallery();
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<Callout {...calloutProps}>
|
||||
<Stack tokens={{ childrenGap: 10, padding: 20 }}>
|
||||
<Text variant="xLarge" block>
|
||||
New gallery
|
||||
</Text>
|
||||
<Text block>
|
||||
Sample notebooks are now combined in gallery. View and try out samples provided by Microsoft and other
|
||||
contributors.
|
||||
</Text>
|
||||
<Link {...openGalleryProps}>Open gallery</Link>
|
||||
</Stack>
|
||||
</Callout>
|
||||
);
|
||||
};
|
||||
|
||||
const buildNotebooksTree = (): TreeNode => {
|
||||
const notebooksTree: TreeNode = {
|
||||
label: undefined,
|
||||
isExpanded: true,
|
||||
children: [],
|
||||
};
|
||||
|
||||
if (!useNotebook.getState().isPhoenixNotebooks) {
|
||||
notebooksTree.children.push(buildNotebooksTemporarilyDownTree());
|
||||
} else {
|
||||
if (galleryContentRoot) {
|
||||
notebooksTree.children.push(buildGalleryNotebooksTree());
|
||||
}
|
||||
|
||||
if (
|
||||
myNotebooksContentRoot &&
|
||||
useNotebook.getState().isPhoenixNotebooks &&
|
||||
useNotebook.getState().connectionInfo.status === ConnectionStatusType.Connected
|
||||
) {
|
||||
notebooksTree.children.push(buildMyNotebooksTree());
|
||||
}
|
||||
if (container.notebookManager?.gitHubOAuthService.isLoggedIn()) {
|
||||
// collapse all other notebook nodes
|
||||
notebooksTree.children.forEach((node) => (node.isExpanded = false));
|
||||
notebooksTree.children.push(buildGitHubNotebooksTree(true));
|
||||
}
|
||||
}
|
||||
return notebooksTree;
|
||||
};
|
||||
|
||||
const buildNotebooksTemporarilyDownTree = (): TreeNode => {
|
||||
return {
|
||||
label: Notebook.temporarilyDownMsg,
|
||||
className: "clickDisabled",
|
||||
};
|
||||
};
|
||||
|
||||
const buildGalleryNotebooksTree = (): TreeNode => {
|
||||
return {
|
||||
label: "Gallery",
|
||||
iconSrc: GalleryIcon,
|
||||
className: "notebookHeader galleryHeader",
|
||||
onClick: () => container.openGallery(),
|
||||
isSelected: () => activeTab?.tabKind === ViewModels.CollectionTabKind.Gallery,
|
||||
};
|
||||
};
|
||||
|
||||
const buildMyNotebooksTree = (): TreeNode => {
|
||||
const myNotebooksTree: TreeNode = buildNotebookDirectoryNode(
|
||||
myNotebooksContentRoot,
|
||||
(item: NotebookContentItem) => {
|
||||
container.openNotebook(item);
|
||||
},
|
||||
);
|
||||
|
||||
myNotebooksTree.isExpanded = true;
|
||||
myNotebooksTree.isAlphaSorted = true;
|
||||
// Remove "Delete" menu item from context menu
|
||||
myNotebooksTree.contextMenu = myNotebooksTree.contextMenu.filter((menuItem) => menuItem.label !== "Delete");
|
||||
return myNotebooksTree;
|
||||
};
|
||||
|
||||
const buildGitHubNotebooksTree = (isConnected: boolean): TreeNode => {
|
||||
const gitHubNotebooksTree: TreeNode = buildNotebookDirectoryNode(
|
||||
gitHubNotebooksContentRoot,
|
||||
(item: NotebookContentItem) => {
|
||||
container.openNotebook(item);
|
||||
},
|
||||
true,
|
||||
);
|
||||
const manageGitContextMenu: TreeNodeMenuItem[] = [
|
||||
{
|
||||
label: "Manage GitHub settings",
|
||||
onClick: () =>
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
"Manage GitHub settings",
|
||||
<GitHubReposPanel
|
||||
explorer={container}
|
||||
gitHubClientProp={container.notebookManager.gitHubClient}
|
||||
junoClientProp={container.notebookManager.junoClient}
|
||||
/>,
|
||||
),
|
||||
},
|
||||
{
|
||||
label: "Disconnect from GitHub",
|
||||
onClick: () => {
|
||||
TelemetryProcessor.trace(Action.NotebooksGitHubDisconnect, ActionModifiers.Mark, {
|
||||
dataExplorerArea: Areas.Notebook,
|
||||
});
|
||||
container.notebookManager?.gitHubOAuthService.logout();
|
||||
},
|
||||
},
|
||||
];
|
||||
gitHubNotebooksTree.contextMenu = manageGitContextMenu;
|
||||
gitHubNotebooksTree.isExpanded = true;
|
||||
gitHubNotebooksTree.isAlphaSorted = true;
|
||||
|
||||
return gitHubNotebooksTree;
|
||||
};
|
||||
|
||||
const buildChildNodes = (
|
||||
item: NotebookContentItem,
|
||||
onFileClick: (item: NotebookContentItem) => void,
|
||||
@@ -373,16 +221,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,
|
||||
onClick: () => container.openUploadFilePanel(item),
|
||||
},
|
||||
];
|
||||
|
||||
//disallow renaming of temporary notebook workspace
|
||||
@@ -786,12 +624,7 @@ 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()} */}
|
||||
</>
|
||||
)}
|
||||
{!isNotebookEnabled && isSampleDataEnabled && (
|
||||
@@ -804,8 +637,6 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
|
||||
<SampleDataTree sampleDataResourceTokenCollection={sampleDataResourceTokenCollection} />
|
||||
</AccordionItemComponent>
|
||||
</AccordionComponent>
|
||||
|
||||
{/* {buildGalleryCallout()} */}
|
||||
</>
|
||||
)}
|
||||
{isNotebookEnabled && isSampleDataEnabled && (
|
||||
@@ -817,12 +648,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
|
||||
<AccordionItemComponent title={"SAMPLE DATA"} containerStyles={{ display: "table" }}>
|
||||
<SampleDataTree sampleDataResourceTokenCollection={sampleDataResourceTokenCollection} />
|
||||
</AccordionItemComponent>
|
||||
<AccordionItemComponent title={"NOTEBOOKS"}>
|
||||
<TreeComponent className="notebookResourceTree" rootNode={buildNotebooksTree()} />
|
||||
</AccordionItemComponent>
|
||||
</AccordionComponent>
|
||||
|
||||
{/* {buildGalleryCallout()} */}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react";
|
||||
import { getItemName } from "Utils/APITypeUtils";
|
||||
import * as ko from "knockout";
|
||||
import * as React from "react";
|
||||
import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg";
|
||||
import GalleryIcon from "../../../images/GalleryIcon.svg";
|
||||
import DeleteIcon from "../../../images/delete.svg";
|
||||
import CopyIcon from "../../../images/notebook/Notebook-copy.svg";
|
||||
import NewNotebookIcon from "../../../images/notebook/Notebook-new.svg";
|
||||
@@ -13,21 +11,17 @@ import PublishIcon from "../../../images/notebook/publish_content.svg";
|
||||
import RefreshIcon from "../../../images/refresh-cosmos.svg";
|
||||
import CollectionIcon from "../../../images/tree-collection.svg";
|
||||
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
|
||||
import { Areas } from "../../Common/Constants";
|
||||
import { isPublicInternetAccessAllowed } from "../../Common/DatabaseAccountUtility";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { IPinnedRepo } from "../../Juno/JunoClient";
|
||||
import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility";
|
||||
import { Action, ActionModifiers, Source } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { isServerlessAccount } from "../../Utils/CapabilityUtils";
|
||||
import * as GitHubUtils from "../../Utils/GitHubUtils";
|
||||
import { useSidePanel } from "../../hooks/useSidePanel";
|
||||
import { useTabs } from "../../hooks/useTabs";
|
||||
import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory";
|
||||
import { AccordionComponent, AccordionItemComponent } from "../Controls/Accordion/AccordionComponent";
|
||||
import { useDialog } from "../Controls/Dialog";
|
||||
import { TreeComponent, TreeNode, TreeNodeMenuItem } from "../Controls/TreeComponent/TreeComponent";
|
||||
import Explorer from "../Explorer";
|
||||
@@ -36,7 +30,6 @@ import { mostRecentActivity } from "../MostRecentActivity/MostRecentActivity";
|
||||
import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem";
|
||||
import { NotebookUtil } from "../Notebook/NotebookUtil";
|
||||
import { useNotebook } from "../Notebook/useNotebook";
|
||||
import { GitHubReposPanel } from "../Panes/GitHubReposPanel/GitHubReposPanel";
|
||||
import TabsBase from "../Tabs/TabsBase";
|
||||
import { useDatabases } from "../useDatabases";
|
||||
import { useSelectedNode } from "../useSelectedNode";
|
||||
@@ -102,26 +95,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
||||
|
||||
public renderComponent(): JSX.Element {
|
||||
const dataRootNode = this.buildDataTree();
|
||||
const notebooksRootNode = this.buildNotebooksTrees();
|
||||
|
||||
if (useNotebook.getState().isNotebookEnabled) {
|
||||
return (
|
||||
<>
|
||||
<AccordionComponent>
|
||||
<AccordionItemComponent title={ResourceTreeAdapter.DataTitle} isExpanded={!this.gitHubNotebooksContentRoot}>
|
||||
<TreeComponent className="dataResourceTree" rootNode={dataRootNode} />
|
||||
</AccordionItemComponent>
|
||||
<AccordionItemComponent title={ResourceTreeAdapter.NotebooksTitle}>
|
||||
<TreeComponent className="notebookResourceTree" rootNode={notebooksRootNode} />
|
||||
</AccordionItemComponent>
|
||||
</AccordionComponent>
|
||||
|
||||
{/* {this.galleryContentRoot && this.buildGalleryCallout()} */}
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
return <TreeComponent className="dataResourceTree" rootNode={dataRootNode} />;
|
||||
}
|
||||
return <TreeComponent className="dataResourceTree" rootNode={dataRootNode} />;
|
||||
}
|
||||
|
||||
public async initialize(): Promise<void[]> {
|
||||
@@ -504,156 +478,6 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
||||
return traverse(schema);
|
||||
}
|
||||
|
||||
private buildNotebooksTrees(): TreeNode {
|
||||
let notebooksTree: TreeNode = {
|
||||
label: undefined,
|
||||
isExpanded: true,
|
||||
children: [],
|
||||
};
|
||||
|
||||
if (this.galleryContentRoot) {
|
||||
notebooksTree.children.push(this.buildGalleryNotebooksTree());
|
||||
}
|
||||
|
||||
if (this.myNotebooksContentRoot) {
|
||||
notebooksTree.children.push(this.buildMyNotebooksTree());
|
||||
}
|
||||
|
||||
if (this.gitHubNotebooksContentRoot) {
|
||||
// collapse all other notebook nodes
|
||||
notebooksTree.children.forEach((node) => (node.isExpanded = false));
|
||||
notebooksTree.children.push(this.buildGitHubNotebooksTree());
|
||||
}
|
||||
|
||||
return notebooksTree;
|
||||
}
|
||||
|
||||
private buildGalleryCallout(): JSX.Element {
|
||||
if (
|
||||
LocalStorageUtility.hasItem(StorageKey.GalleryCalloutDismissed) &&
|
||||
LocalStorageUtility.getEntryBoolean(StorageKey.GalleryCalloutDismissed)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const calloutProps: ICalloutProps = {
|
||||
calloutMaxWidth: 350,
|
||||
ariaLabel: "New gallery",
|
||||
role: "alertdialog",
|
||||
gapSpace: 0,
|
||||
target: ".galleryHeader",
|
||||
directionalHint: DirectionalHint.leftTopEdge,
|
||||
onDismiss: () => {
|
||||
LocalStorageUtility.setEntryBoolean(StorageKey.GalleryCalloutDismissed, true);
|
||||
this.triggerRender();
|
||||
},
|
||||
setInitialFocus: true,
|
||||
};
|
||||
|
||||
const openGalleryProps: ILinkProps = {
|
||||
onClick: () => {
|
||||
LocalStorageUtility.setEntryBoolean(StorageKey.GalleryCalloutDismissed, true);
|
||||
this.container.openGallery();
|
||||
this.triggerRender();
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<Callout {...calloutProps}>
|
||||
<Stack tokens={{ childrenGap: 10, padding: 20 }}>
|
||||
<Text variant="xLarge" block>
|
||||
New gallery
|
||||
</Text>
|
||||
<Text block>
|
||||
Sample notebooks are now combined in gallery. View and try out samples provided by Microsoft and other
|
||||
contributors.
|
||||
</Text>
|
||||
<Link {...openGalleryProps}>Open gallery</Link>
|
||||
</Stack>
|
||||
</Callout>
|
||||
);
|
||||
}
|
||||
|
||||
private buildGalleryNotebooksTree(): TreeNode {
|
||||
return {
|
||||
label: "Gallery",
|
||||
iconSrc: GalleryIcon,
|
||||
className: "notebookHeader galleryHeader",
|
||||
onClick: () => this.container.openGallery(),
|
||||
isSelected: () => {
|
||||
const activeTab = useTabs.getState().activeTab;
|
||||
return activeTab && activeTab.tabKind === ViewModels.CollectionTabKind.Gallery;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private buildMyNotebooksTree(): TreeNode {
|
||||
const myNotebooksTree: TreeNode = this.buildNotebookDirectoryNode(
|
||||
this.myNotebooksContentRoot,
|
||||
(item: NotebookContentItem) => {
|
||||
this.container.openNotebook(item).then((hasOpened) => {
|
||||
if (hasOpened) {
|
||||
mostRecentActivity.notebookWasItemOpened(userContext.databaseAccount?.id, item);
|
||||
}
|
||||
});
|
||||
},
|
||||
true,
|
||||
true,
|
||||
);
|
||||
|
||||
myNotebooksTree.isExpanded = true;
|
||||
myNotebooksTree.isAlphaSorted = true;
|
||||
// Remove "Delete" menu item from context menu
|
||||
myNotebooksTree.contextMenu = myNotebooksTree.contextMenu.filter((menuItem) => menuItem.label !== "Delete");
|
||||
return myNotebooksTree;
|
||||
}
|
||||
|
||||
private buildGitHubNotebooksTree(): TreeNode {
|
||||
const gitHubNotebooksTree: TreeNode = this.buildNotebookDirectoryNode(
|
||||
this.gitHubNotebooksContentRoot,
|
||||
(item: NotebookContentItem) => {
|
||||
this.container.openNotebook(item).then((hasOpened) => {
|
||||
if (hasOpened) {
|
||||
mostRecentActivity.notebookWasItemOpened(userContext.databaseAccount?.id, item);
|
||||
}
|
||||
});
|
||||
},
|
||||
true,
|
||||
true,
|
||||
);
|
||||
|
||||
gitHubNotebooksTree.contextMenu = [
|
||||
{
|
||||
label: "Manage GitHub settings",
|
||||
onClick: () =>
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
"Manage GitHub settings",
|
||||
<GitHubReposPanel
|
||||
explorer={this.container}
|
||||
gitHubClientProp={this.container.notebookManager.gitHubClient}
|
||||
junoClientProp={this.container.notebookManager.junoClient}
|
||||
/>,
|
||||
),
|
||||
},
|
||||
{
|
||||
label: "Disconnect from GitHub",
|
||||
onClick: () => {
|
||||
TelemetryProcessor.trace(Action.NotebooksGitHubDisconnect, ActionModifiers.Mark, {
|
||||
dataExplorerArea: Areas.Notebook,
|
||||
});
|
||||
this.container.notebookManager?.gitHubOAuthService.logout();
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
gitHubNotebooksTree.isExpanded = true;
|
||||
gitHubNotebooksTree.isAlphaSorted = true;
|
||||
|
||||
return gitHubNotebooksTree;
|
||||
}
|
||||
|
||||
private buildChildNodes(
|
||||
item: NotebookContentItem,
|
||||
onFileClick: (item: NotebookContentItem) => void,
|
||||
@@ -800,16 +624,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,
|
||||
onClick: () => this.container.openUploadFilePanel(item),
|
||||
},
|
||||
];
|
||||
|
||||
//disallow renaming of temporary notebook workspace
|
||||
|
||||
183
src/KeyboardShortcuts.tsx
Normal file
183
src/KeyboardShortcuts.tsx
Normal file
@@ -0,0 +1,183 @@
|
||||
import * as React from "react";
|
||||
import { PropsWithChildren, useEffect } from "react";
|
||||
import { KeyBindingMap, tinykeys } from "tinykeys";
|
||||
import create, { UseStore } from "zustand";
|
||||
|
||||
/**
|
||||
* Represents a keyboard shortcut handler.
|
||||
* Return `true` to prevent the default action of the keyboard shortcut.
|
||||
* Any other return value will allow the default action to proceed.
|
||||
*/
|
||||
export type KeyboardActionHandler = (e: KeyboardEvent) => boolean | void;
|
||||
|
||||
export type KeyboardHandlerMap = Partial<Record<KeyboardAction, KeyboardActionHandler>>;
|
||||
|
||||
/**
|
||||
* The groups of keyboard actions that can be managed by the application.
|
||||
* Each group can be updated separately, but, when updated, must be completely replaced.
|
||||
*/
|
||||
export enum KeyboardActionGroup {
|
||||
/** Keyboard actions related to tab navigation. */
|
||||
TABS = "TABS",
|
||||
|
||||
/** Keyboard actions managed by the global command bar. */
|
||||
COMMAND_BAR = "COMMAND_BAR",
|
||||
|
||||
/**
|
||||
* Keyboard actions specific to the active tab.
|
||||
* This group is automatically cleared when the active tab changes.
|
||||
*/
|
||||
ACTIVE_TAB = "ACTIVE_TAB",
|
||||
}
|
||||
|
||||
/**
|
||||
* The possible actions that can be triggered by keyboard shortcuts.
|
||||
*/
|
||||
export enum KeyboardAction {
|
||||
NEW_QUERY = "NEW_QUERY",
|
||||
EXECUTE_ITEM = "EXECUTE_ITEM",
|
||||
CANCEL_OR_DISCARD = "CANCEL_OR_DISCARD",
|
||||
SAVE_ITEM = "SAVE_ITEM",
|
||||
DOWNLOAD_ITEM = "DOWNLOAD_ITEM",
|
||||
OPEN_QUERY = "OPEN_QUERY",
|
||||
OPEN_QUERY_FROM_DISK = "OPEN_QUERY_FROM_DISK",
|
||||
NEW_SPROC = "NEW_SPROC",
|
||||
NEW_UDF = "NEW_UDF",
|
||||
NEW_TRIGGER = "NEW_TRIGGER",
|
||||
NEW_DATABASE = "NEW_DATABASE",
|
||||
NEW_COLLECTION = "NEW_CONTAINER",
|
||||
NEW_ITEM = "NEW_ITEM",
|
||||
DELETE_ITEM = "DELETE_ITEM",
|
||||
TOGGLE_COPILOT = "TOGGLE_COPILOT",
|
||||
SELECT_LEFT_TAB = "SELECT_LEFT_TAB",
|
||||
SELECT_RIGHT_TAB = "SELECT_RIGHT_TAB",
|
||||
CLOSE_TAB = "CLOSE_TAB",
|
||||
SEARCH = "SEARCH",
|
||||
CLEAR_SEARCH = "CLEAR_SEARCH",
|
||||
}
|
||||
|
||||
/**
|
||||
* The keyboard shortcuts for the application.
|
||||
* This record maps each action to the keyboard shortcuts that trigger the action.
|
||||
* Even if an action is specified here, it will not be triggered unless a handler is set for it.
|
||||
*/
|
||||
const bindings: Record<KeyboardAction, string[]> = {
|
||||
// NOTE: The "$mod" special value is used to represent the "Control" key on Windows/Linux and the "Command" key on macOS.
|
||||
// See https://www.npmjs.com/package/tinykeys#commonly-used-keys-and-codes for more information on the expected values for keyboard shortcuts.
|
||||
|
||||
[KeyboardAction.NEW_QUERY]: ["$mod+J", "Alt+N Q"],
|
||||
[KeyboardAction.EXECUTE_ITEM]: ["Shift+Enter", "F5"],
|
||||
[KeyboardAction.CANCEL_OR_DISCARD]: ["Escape"],
|
||||
[KeyboardAction.SAVE_ITEM]: ["$mod+S"],
|
||||
[KeyboardAction.DOWNLOAD_ITEM]: ["$mod+Shift+S"],
|
||||
[KeyboardAction.OPEN_QUERY]: ["$mod+O"],
|
||||
[KeyboardAction.OPEN_QUERY_FROM_DISK]: ["$mod+Shift+O"],
|
||||
[KeyboardAction.NEW_SPROC]: ["Alt+N P"],
|
||||
[KeyboardAction.NEW_UDF]: ["Alt+N F"],
|
||||
[KeyboardAction.NEW_TRIGGER]: ["Alt+N T"],
|
||||
[KeyboardAction.NEW_DATABASE]: ["Alt+N D"],
|
||||
[KeyboardAction.NEW_COLLECTION]: ["Alt+N C"],
|
||||
[KeyboardAction.NEW_ITEM]: ["Alt+N I"],
|
||||
[KeyboardAction.DELETE_ITEM]: ["Alt+D"],
|
||||
[KeyboardAction.TOGGLE_COPILOT]: ["$mod+P"],
|
||||
[KeyboardAction.SELECT_LEFT_TAB]: ["$mod+Alt+[", "$mod+Shift+F6"],
|
||||
[KeyboardAction.SELECT_RIGHT_TAB]: ["$mod+Alt+]", "$mod+F6"],
|
||||
[KeyboardAction.CLOSE_TAB]: ["$mod+Alt+W"],
|
||||
[KeyboardAction.SEARCH]: ["$mod+Shift+F"],
|
||||
[KeyboardAction.CLEAR_SEARCH]: ["$mod+Shift+C"],
|
||||
};
|
||||
|
||||
interface KeyboardShortcutState {
|
||||
/**
|
||||
* A set of all the keyboard shortcuts handlers.
|
||||
*/
|
||||
allHandlers: KeyboardHandlerMap;
|
||||
|
||||
/**
|
||||
* A set of all the groups of keyboard shortcuts handlers.
|
||||
*/
|
||||
groups: Partial<Record<KeyboardActionGroup, KeyboardHandlerMap>>;
|
||||
|
||||
/**
|
||||
* Sets the keyboard shortcut handlers for the given group.
|
||||
*/
|
||||
setHandlers: (group: KeyboardActionGroup, handlers: KeyboardHandlerMap) => void;
|
||||
}
|
||||
|
||||
export type KeyboardHandlerSetter = (handlers: KeyboardHandlerMap) => void;
|
||||
|
||||
/**
|
||||
* Defines the calling component as the manager of the keyboard actions for the given group.
|
||||
* @param group The group of keyboard actions to manage.
|
||||
* @returns A function that can be used to set the keyboard action handlers for the given group.
|
||||
*/
|
||||
export const useKeyboardActionGroup: (group: KeyboardActionGroup) => KeyboardHandlerSetter =
|
||||
(group: KeyboardActionGroup) => (handlers: KeyboardHandlerMap) =>
|
||||
useKeyboardActionHandlers.getState().setHandlers(group, handlers);
|
||||
|
||||
/**
|
||||
* Clears the keyboard action handlers for the given group.
|
||||
* @param group The group of keyboard actions to clear.
|
||||
*/
|
||||
export const clearKeyboardActionGroup = (group: KeyboardActionGroup) => {
|
||||
useKeyboardActionHandlers.getState().setHandlers(group, {});
|
||||
};
|
||||
|
||||
const useKeyboardActionHandlers: UseStore<KeyboardShortcutState> = create((set, get) => ({
|
||||
allHandlers: {},
|
||||
groups: {},
|
||||
setHandlers: (group: KeyboardActionGroup, handlers: KeyboardHandlerMap) => {
|
||||
const state = get();
|
||||
const groups = { ...state.groups, [group]: handlers };
|
||||
|
||||
// Combine all the handlers from all the groups in the correct order.
|
||||
const allHandlers: KeyboardHandlerMap = {};
|
||||
eachKey(groups).forEach((group) => {
|
||||
const groupHandlers = groups[group];
|
||||
if (groupHandlers) {
|
||||
eachKey(groupHandlers).forEach((action) => {
|
||||
// Check for duplicate handlers in development mode.
|
||||
// We don't want to raise an error here in production, but having duplicate handlers is a mistake.
|
||||
if (process.env.NODE_ENV === "development" && allHandlers[action]) {
|
||||
throw new Error(`Duplicate handler for Keyboard Action "${action}".`);
|
||||
}
|
||||
allHandlers[action] = groupHandlers[action];
|
||||
});
|
||||
}
|
||||
});
|
||||
set({ groups, allHandlers });
|
||||
},
|
||||
}));
|
||||
|
||||
function createHandler(action: KeyboardAction): KeyboardActionHandler {
|
||||
return (e) => {
|
||||
const state = useKeyboardActionHandlers.getState();
|
||||
const handler = state.allHandlers[action];
|
||||
if (handler && handler(e)) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const allHandlers: KeyBindingMap = {};
|
||||
eachKey(bindings).forEach((action) => {
|
||||
const shortcuts = bindings[action];
|
||||
shortcuts.forEach((shortcut) => {
|
||||
allHandlers[shortcut] = createHandler(action);
|
||||
});
|
||||
});
|
||||
|
||||
export function KeyboardShortcutRoot({ children }: PropsWithChildren<unknown>) {
|
||||
useEffect(() => {
|
||||
// We bind to the body because Fluent UI components sometimes shift focus to the body, which is above the root React component.
|
||||
tinykeys(document.body, allHandlers);
|
||||
}, []);
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
/** A _typed_ version of `Object.keys` that preserves the original key type */
|
||||
function eachKey<K extends string | number | symbol, V>(record: Partial<Record<K, V>>): K[] {
|
||||
return Object.keys(record) as K[];
|
||||
}
|
||||
92
src/Main.tsx
92
src/Main.tsx
@@ -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";
|
||||
@@ -18,6 +21,7 @@ import "../externals/jquery.typeahead.min.js";
|
||||
// Image Dependencies
|
||||
import { Platform } from "ConfigContext";
|
||||
import { QueryCopilotCarousel } from "Explorer/QueryCopilot/CopilotCarousel";
|
||||
import { KeyboardShortcutRoot } from "KeyboardShortcuts";
|
||||
import "../images/CosmosDB_rgb_ui_lighttheme.ico";
|
||||
import hdeConnectImage from "../images/HdeConnectCosmosDB.svg";
|
||||
import "../images/favicon.ico";
|
||||
@@ -88,52 +92,54 @@ const App: React.FunctionComponent = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flexContainer" aria-hidden="false">
|
||||
<div id="divExplorer" className="flexContainer hideOverflows">
|
||||
<div id="freeTierTeachingBubble"> </div>
|
||||
{/* Main Command Bar - Start */}
|
||||
<CommandBar container={explorer} />
|
||||
{/* Collections Tree and Tabs - Begin */}
|
||||
<div className="resourceTreeAndTabs">
|
||||
{/* Collections Tree - Start */}
|
||||
{userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo" && (
|
||||
<div id="resourcetree" data-test="resourceTreeId" className="resourceTree">
|
||||
<div className="collectionsTreeWithSplitter">
|
||||
{/* Collections Tree Expanded - Start */}
|
||||
<ResourceTreeContainer
|
||||
container={explorer}
|
||||
toggleLeftPaneExpanded={toggleLeftPaneExpanded}
|
||||
isLeftPaneExpanded={isLeftPaneExpanded}
|
||||
/>
|
||||
{/* Collections Tree Expanded - End */}
|
||||
{/* Collections Tree Collapsed - Start */}
|
||||
<CollapsedResourceTree
|
||||
toggleLeftPaneExpanded={toggleLeftPaneExpanded}
|
||||
isLeftPaneExpanded={isLeftPaneExpanded}
|
||||
/>
|
||||
{/* Collections Tree Collapsed - End */}
|
||||
<KeyboardShortcutRoot>
|
||||
<div className="flexContainer" aria-hidden="false">
|
||||
<div id="divExplorer" className="flexContainer hideOverflows">
|
||||
<div id="freeTierTeachingBubble"> </div>
|
||||
{/* Main Command Bar - Start */}
|
||||
<CommandBar container={explorer} />
|
||||
{/* Collections Tree and Tabs - Begin */}
|
||||
<div className="resourceTreeAndTabs">
|
||||
{/* Collections Tree - Start */}
|
||||
{userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo" && (
|
||||
<div id="resourcetree" data-test="resourceTreeId" className="resourceTree">
|
||||
<div className="collectionsTreeWithSplitter">
|
||||
{/* Collections Tree Expanded - Start */}
|
||||
<ResourceTreeContainer
|
||||
container={explorer}
|
||||
toggleLeftPaneExpanded={toggleLeftPaneExpanded}
|
||||
isLeftPaneExpanded={isLeftPaneExpanded}
|
||||
/>
|
||||
{/* Collections Tree Expanded - End */}
|
||||
{/* Collections Tree Collapsed - Start */}
|
||||
<CollapsedResourceTree
|
||||
toggleLeftPaneExpanded={toggleLeftPaneExpanded}
|
||||
isLeftPaneExpanded={isLeftPaneExpanded}
|
||||
/>
|
||||
{/* Collections Tree Collapsed - End */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<Tabs explorer={explorer} />
|
||||
</div>
|
||||
{/* Collections Tree and Tabs - End */}
|
||||
<div
|
||||
className="dataExplorerErrorConsoleContainer"
|
||||
role="contentinfo"
|
||||
aria-label="Notification console"
|
||||
id="explorerNotificationConsole"
|
||||
>
|
||||
<NotificationConsole />
|
||||
)}
|
||||
<Tabs explorer={explorer} />
|
||||
</div>
|
||||
{/* Collections Tree and Tabs - End */}
|
||||
<div
|
||||
className="dataExplorerErrorConsoleContainer"
|
||||
role="contentinfo"
|
||||
aria-label="Notification console"
|
||||
id="explorerNotificationConsole"
|
||||
>
|
||||
<NotificationConsole />
|
||||
</div>
|
||||
</div>
|
||||
<SidePanel />
|
||||
<Dialog />
|
||||
{<QuickstartCarousel isOpen={isCarouselOpen} />}
|
||||
{<SQLQuickstartTutorial />}
|
||||
{<MongoQuickstartTutorial />}
|
||||
{<QueryCopilotCarousel isOpen={isCopilotCarouselOpen} explorer={explorer} />}
|
||||
</div>
|
||||
<SidePanel />
|
||||
<Dialog />
|
||||
{<QuickstartCarousel isOpen={isCarouselOpen} />}
|
||||
{<SQLQuickstartTutorial />}
|
||||
{<MongoQuickstartTutorial />}
|
||||
{<QueryCopilotCarousel isOpen={isCopilotCarouselOpen} explorer={explorer} />}
|
||||
</div>
|
||||
</KeyboardShortcutRoot>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useBoolean } from "@fluentui/react-hooks";
|
||||
import { userContext } from "UserContext";
|
||||
import { usePortalBackendEndpoint } from "Utils/EndpointUtils";
|
||||
import { useNewPortalBackendEndpoint } from "Utils/EndpointUtils";
|
||||
import * as React from "react";
|
||||
import ConnectImage from "../../../../images/HdeConnectCosmosDB.svg";
|
||||
import ErrorImage from "../../../../images/error.svg";
|
||||
@@ -19,7 +19,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export const fetchEncryptedToken = async (connectionString: string): Promise<string> => {
|
||||
if (!usePortalBackendEndpoint(BackendApi.GenerateToken)) {
|
||||
if (!useNewPortalBackendEndpoint(BackendApi.GenerateToken)) {
|
||||
return await fetchEncryptedToken_ToBeDeprecated(connectionString);
|
||||
}
|
||||
|
||||
|
||||
@@ -31,11 +31,6 @@ export type Features = {
|
||||
readonly mongoProxyAPIs?: string;
|
||||
readonly enableThroughputCap: boolean;
|
||||
readonly enableHierarchicalKeys: boolean;
|
||||
readonly enableLegacyMongoShellV1: boolean;
|
||||
readonly enableLegacyMongoShellV1Debug: boolean;
|
||||
readonly enableLegacyMongoShellV2: boolean;
|
||||
readonly enableLegacyMongoShellV2Debug: boolean;
|
||||
readonly loadLegacyMongoShellFromBE: boolean;
|
||||
readonly enableCopilot: boolean;
|
||||
readonly copilotVersion?: string;
|
||||
readonly disableCopilotPhoenixGateaway: boolean;
|
||||
@@ -106,11 +101,6 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
|
||||
notebooksDownBanner: "true" === get("notebooksDownBanner"),
|
||||
enableThroughputCap: "true" === get("enablethroughputcap"),
|
||||
enableHierarchicalKeys: "true" === get("enablehierarchicalkeys"),
|
||||
enableLegacyMongoShellV1: "true" === get("enablelegacymongoshellv1"),
|
||||
enableLegacyMongoShellV1Debug: "true" === get("enablelegacymongoshellv1debug"),
|
||||
enableLegacyMongoShellV2: "true" === get("enablelegacymongoshellv2"),
|
||||
enableLegacyMongoShellV2Debug: "true" === get("enablelegacymongoshellv2debug"),
|
||||
loadLegacyMongoShellFromBE: "true" === get("loadlegacymongoshellfrombe"),
|
||||
enableCopilot: "true" === get("enablecopilot", "true"),
|
||||
copilotVersion: get("copilotversion") ?? "v2.0",
|
||||
disableCopilotPhoenixGateaway: "true" === get("disablecopilotphoenixgateaway"),
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
if (window.parent !== window) {
|
||||
(window as any).__REACT_DEVTOOLS_GLOBAL_HOOK__ = (window.parent as any).__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
||||
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).
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ export const MongoProxyOutboundIPs: { [key: string]: string[] } = {
|
||||
};
|
||||
|
||||
export const allowedMongoProxyEndpoints: ReadonlyArray<string> = [
|
||||
MongoProxyEndpoints.Development,
|
||||
MongoProxyEndpoints.Local,
|
||||
MongoProxyEndpoints.Mpac,
|
||||
MongoProxyEndpoints.Prod,
|
||||
MongoProxyEndpoints.Fairfax,
|
||||
@@ -145,8 +145,30 @@ export const allowedJunoOrigins: ReadonlyArray<string> = [
|
||||
|
||||
export const allowedNotebookServerUrls: ReadonlyArray<string> = [];
|
||||
|
||||
export function usePortalBackendEndpoint(backendApi: BackendApi): boolean {
|
||||
const activePortalBackendEndpoints: string[] = [PortalBackendEndpoints.Development];
|
||||
const activeBackendApi: boolean = configContext.NEW_BACKEND_APIS?.includes(backendApi) || false;
|
||||
return activeBackendApi && activePortalBackendEndpoints.includes(configContext.PORTAL_BACKEND_ENDPOINT as 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,
|
||||
PortalBackendEndpoints.Mpac,
|
||||
PortalBackendEndpoints.Prod,
|
||||
],
|
||||
[BackendApi.PortalSettings]: [
|
||||
PortalBackendEndpoints.Development,
|
||||
PortalBackendEndpoints.Mpac,
|
||||
PortalBackendEndpoints.Prod,
|
||||
],
|
||||
};
|
||||
|
||||
if (!newBackendApiEnvironmentMap[backendApi] || !configContext.PORTAL_BACKEND_ENDPOINT) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return newBackendApiEnvironmentMap[backendApi].includes(configContext.PORTAL_BACKEND_ENDPOINT);
|
||||
}
|
||||
|
||||
@@ -497,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({
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { clamp } from "@fluentui/react";
|
||||
import create, { UseStore } from "zustand";
|
||||
import * as ViewModels from "../Contracts/ViewModels";
|
||||
import { CollectionTabKind } from "../Contracts/ViewModels";
|
||||
@@ -29,6 +30,11 @@ export interface TabsState {
|
||||
setQueryCopilotTabInitialInput: (input: string) => void;
|
||||
setIsTabExecuting: (state: boolean) => void;
|
||||
setIsQueryErrorThrown: (state: boolean) => void;
|
||||
getCurrentTabIndex: () => number;
|
||||
selectTabByIndex: (index: number) => void;
|
||||
selectLeftTab: () => void;
|
||||
selectRightTab: () => void;
|
||||
closeActiveTab: () => void;
|
||||
}
|
||||
|
||||
export enum ReactTabKind {
|
||||
@@ -175,4 +181,44 @@ export const useTabs: UseStore<TabsState> = create((set, get) => ({
|
||||
setIsQueryErrorThrown: (state: boolean) => {
|
||||
set({ isQueryErrorThrown: state });
|
||||
},
|
||||
getCurrentTabIndex: () => {
|
||||
const state = get();
|
||||
if (state.activeReactTab !== undefined) {
|
||||
return state.openedReactTabs.indexOf(state.activeReactTab);
|
||||
} else if (state.activeTab !== undefined) {
|
||||
const nonReactTabIndex = state.openedTabs.indexOf(state.activeTab);
|
||||
if (nonReactTabIndex !== -1) {
|
||||
return state.openedReactTabs.length + nonReactTabIndex;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
},
|
||||
selectTabByIndex: (index: number) => {
|
||||
const state = get();
|
||||
const totalTabCount = state.openedReactTabs.length + state.openedTabs.length;
|
||||
const clampedIndex = clamp(index, totalTabCount - 1, 0);
|
||||
|
||||
if (clampedIndex < state.openedReactTabs.length) {
|
||||
set({ activeTab: undefined, activeReactTab: state.openedReactTabs[clampedIndex] });
|
||||
} else {
|
||||
set({ activeTab: state.openedTabs[clampedIndex - state.openedReactTabs.length], activeReactTab: undefined });
|
||||
}
|
||||
},
|
||||
selectLeftTab: () => {
|
||||
const state = get();
|
||||
state.selectTabByIndex(state.getCurrentTabIndex() - 1);
|
||||
},
|
||||
selectRightTab: () => {
|
||||
const state = get();
|
||||
state.selectTabByIndex(state.getCurrentTabIndex() + 1);
|
||||
},
|
||||
closeActiveTab: () => {
|
||||
const state = get();
|
||||
if (state.activeReactTab !== undefined) {
|
||||
state.closeReactTab(state.activeReactTab);
|
||||
} else if (state.activeTab !== undefined) {
|
||||
state.closeTab(state.activeTab);
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user