Merge branch 'master' into languy-resource-tree-to-react

This commit is contained in:
Laurent Nguyen 2021-03-16 16:27:30 +01:00
commit a17d71d76e
21 changed files with 583 additions and 607 deletions

View File

@ -15,10 +15,10 @@ jobs:
if: github.ref == 'refs/heads/master' if: github.ref == 'refs/heads/master'
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Use Node.js 12.x - name: Use Node.js 14.x
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: 12.x node-version: 14.x
- run: npm ci - run: npm ci
- run: node utils/codeMetrics.js - run: node utils/codeMetrics.js
env: env:
@ -28,10 +28,10 @@ jobs:
name: "Compile TypeScript" name: "Compile TypeScript"
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Use Node.js 12.x - name: Use Node.js 14.x
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: 12.x node-version: 14.x
- run: npm ci - run: npm ci
- run: npm run compile - run: npm run compile
- run: npm run compile:strict - run: npm run compile:strict
@ -40,10 +40,10 @@ jobs:
name: "Check Format" name: "Check Format"
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Use Node.js 12.x - name: Use Node.js 14.x
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: 12.x node-version: 14.x
- run: npm ci - run: npm ci
- run: npm run format:check - run: npm run format:check
lint: lint:
@ -51,10 +51,10 @@ jobs:
name: "Lint" name: "Lint"
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Use Node.js 12.x - name: Use Node.js 14.x
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: 12.x node-version: 14.x
- run: npm ci - run: npm ci
- run: npm run lint - run: npm run lint
unittest: unittest:
@ -62,10 +62,10 @@ jobs:
name: "Unit Tests" name: "Unit Tests"
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Use Node.js 12.x - name: Use Node.js 14.x
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: 12.x node-version: 14.x
- run: npm ci - run: npm ci
- run: npm run test - run: npm run test
build: build:
@ -74,10 +74,10 @@ jobs:
name: "Build" name: "Build"
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Use Node.js 12.x - name: Use Node.js 14.x
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: 12.x node-version: 14.x
- run: npm ci - run: npm ci
- run: npm run build:contracts - run: npm run build:contracts
- name: Restore Build Cache - name: Restore Build Cache
@ -98,10 +98,10 @@ jobs:
runs-on: windows-latest runs-on: windows-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Use Node.js 12.x - name: Use Node.js 14.x
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: 12.x node-version: 14.x
- uses: southpolesteve/cosmos-emulator-github-action@v1 - uses: southpolesteve/cosmos-emulator-github-action@v1
- name: End to End Tests - name: End to End Tests
run: | run: |
@ -125,10 +125,10 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Use Node.js 12.x - name: Use Node.js 14.x
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: 12.x node-version: 14.x
- name: Accessibility Check - name: Accessibility Check
run: | run: |
# Ubuntu gets mad when webpack runs too many files watchers # Ubuntu gets mad when webpack runs too many files watchers
@ -163,6 +163,7 @@ jobs:
TABLES_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_TABLE }} TABLES_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_TABLE }}
DATA_EXPLORER_ENDPOINT: "https://localhost:1234/hostedExplorer.html" DATA_EXPLORER_ENDPOINT: "https://localhost:1234/hostedExplorer.html"
strategy: strategy:
fail-fast: false
matrix: matrix:
test-file: test-file:
- ./test/cassandra/container.spec.ts - ./test/cassandra/container.spec.ts

110
package-lock.json generated
View File

@ -949,14 +949,20 @@
} }
}, },
"node_modules/@babel/plugin-syntax-class-properties": { "node_modules/@babel/plugin-syntax-class-properties": {
"version": "7.12.1", "version": "7.12.13",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.1.tgz", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz",
"integrity": "sha512-U40A76x5gTwmESz+qiqssqmeEsKvcSyvtgktrm0uzcARAmM9I1jR221f6Oq+GmHrcD+LvZDag1UTOTe2fL3TeA==", "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@babel/helper-plugin-utils": "^7.10.4" "@babel/helper-plugin-utils": "^7.12.13"
} }
}, },
"node_modules/@babel/plugin-syntax-class-properties/node_modules/@babel/helper-plugin-utils": {
"version": "7.13.0",
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.13.0.tgz",
"integrity": "sha512-ZPafIPSwzUlAoWT8DKs1W2VyF2gOWthGd5NGFMsBcMMol+ZhK+EQY/e6V96poa6PA/Bh+C9plWN0hXO1uB8AfQ==",
"dev": true
},
"node_modules/@babel/plugin-syntax-decorators": { "node_modules/@babel/plugin-syntax-decorators": {
"version": "7.12.1", "version": "7.12.1",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.12.1.tgz", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.12.1.tgz",
@ -1951,9 +1957,9 @@
} }
}, },
"node_modules/@istanbuljs/schema": { "node_modules/@istanbuljs/schema": {
"version": "0.1.2", "version": "0.1.3",
"resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.2.tgz", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz",
"integrity": "sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==", "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": ">=8" "node": ">=8"
@ -2127,9 +2133,9 @@
} }
}, },
"node_modules/@jest/globals/node_modules/@types/yargs": { "node_modules/@jest/globals/node_modules/@types/yargs": {
"version": "15.0.12", "version": "15.0.13",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.12.tgz", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.13.tgz",
"integrity": "sha512-f+fD/fQAo3BCbCDlrUpznF1A5Zp9rB0noS5vnoormHSIPFKL0Z2DcUJ3Gxp5ytH4uLRNxy7AwYUC9exZzqGMAw==", "integrity": "sha512-kQ5JNTrbDv3Rp5X2n/iUu37IJBDU2gsZ5R/g1/KHOOEc5IKfUFjXT6DENPGduh08I/pamwtEq4oul7gUqKTQDQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@types/yargs-parser": "*" "@types/yargs-parser": "*"
@ -4908,9 +4914,9 @@
} }
}, },
"node_modules/@types/graceful-fs": { "node_modules/@types/graceful-fs": {
"version": "4.1.4", "version": "4.1.5",
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.4.tgz", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz",
"integrity": "sha512-mWA/4zFQhfvOA8zWkXobwJvBD7vzcxgrOQ0J5CH1votGqdq9m7+FwtGaqyCZqC3NyyBkc9z4m+iry4LlqcMWJg==", "integrity": "sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@types/node": "*" "@types/node": "*"
@ -15439,9 +15445,9 @@
} }
}, },
"node_modules/jest/node_modules/@types/yargs": { "node_modules/jest/node_modules/@types/yargs": {
"version": "15.0.12", "version": "15.0.13",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.12.tgz", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.13.tgz",
"integrity": "sha512-f+fD/fQAo3BCbCDlrUpznF1A5Zp9rB0noS5vnoormHSIPFKL0Z2DcUJ3Gxp5ytH4uLRNxy7AwYUC9exZzqGMAw==", "integrity": "sha512-kQ5JNTrbDv3Rp5X2n/iUu37IJBDU2gsZ5R/g1/KHOOEc5IKfUFjXT6DENPGduh08I/pamwtEq4oul7gUqKTQDQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@types/yargs-parser": "*" "@types/yargs-parser": "*"
@ -15756,9 +15762,9 @@
} }
}, },
"node_modules/jest/node_modules/fsevents": { "node_modules/jest/node_modules/fsevents": {
"version": "2.3.1", "version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.1.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-YR47Eg4hChJGAB1O3yEAOkGO+rlzutoICGqGo9EZ4lKWokzZRSyIW1QmTzqjtw8MJdj9srP869CuWw/hyzSiBw==", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true, "dev": true,
"optional": true, "optional": true,
"os": [ "os": [
@ -16751,9 +16757,9 @@
} }
}, },
"node_modules/jest/node_modules/string-width": { "node_modules/jest/node_modules/string-width": {
"version": "4.2.0", "version": "4.2.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz",
"integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"emoji-regex": "^8.0.0", "emoji-regex": "^8.0.0",
@ -22760,11 +22766,6 @@
"safer-buffer": "^2.0.2", "safer-buffer": "^2.0.2",
"tweetnacl": "~0.14.0" "tweetnacl": "~0.14.0"
}, },
"bin": {
"sshpk-conv": "bin/sshpk-conv",
"sshpk-sign": "bin/sshpk-sign",
"sshpk-verify": "bin/sshpk-verify"
},
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@ -25098,8 +25099,7 @@
"node_modules/webcrypto-liner/node_modules/core-js": { "node_modules/webcrypto-liner/node_modules/core-js": {
"version": "3.8.3", "version": "3.8.3",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.8.3.tgz", "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.8.3.tgz",
"integrity": "sha512-KPYXeVZYemC2TkNEkX/01I+7yd+nX3KddKwZ1Ww7SKWdI2wQprSgLmrTddT8nw92AjEklTsPBoSdQBhbI1bQ6Q==", "integrity": "sha512-KPYXeVZYemC2TkNEkX/01I+7yd+nX3KddKwZ1Ww7SKWdI2wQprSgLmrTddT8nw92AjEklTsPBoSdQBhbI1bQ6Q=="
"hasInstallScript": true
}, },
"node_modules/webfontloader": { "node_modules/webfontloader": {
"version": "1.6.28", "version": "1.6.28",
@ -27019,12 +27019,20 @@
} }
}, },
"@babel/plugin-syntax-class-properties": { "@babel/plugin-syntax-class-properties": {
"version": "7.12.1", "version": "7.12.13",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.1.tgz", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz",
"integrity": "sha512-U40A76x5gTwmESz+qiqssqmeEsKvcSyvtgktrm0uzcARAmM9I1jR221f6Oq+GmHrcD+LvZDag1UTOTe2fL3TeA==", "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/helper-plugin-utils": "^7.10.4" "@babel/helper-plugin-utils": "^7.12.13"
},
"dependencies": {
"@babel/helper-plugin-utils": {
"version": "7.13.0",
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.13.0.tgz",
"integrity": "sha512-ZPafIPSwzUlAoWT8DKs1W2VyF2gOWthGd5NGFMsBcMMol+ZhK+EQY/e6V96poa6PA/Bh+C9plWN0hXO1uB8AfQ==",
"dev": true
}
} }
}, },
"@babel/plugin-syntax-decorators": { "@babel/plugin-syntax-decorators": {
@ -27993,9 +28001,9 @@
} }
}, },
"@istanbuljs/schema": { "@istanbuljs/schema": {
"version": "0.1.2", "version": "0.1.3",
"resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.2.tgz", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz",
"integrity": "sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==", "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==",
"dev": true "dev": true
}, },
"@jest/console": { "@jest/console": {
@ -28135,9 +28143,9 @@
} }
}, },
"@types/yargs": { "@types/yargs": {
"version": "15.0.12", "version": "15.0.13",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.12.tgz", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.13.tgz",
"integrity": "sha512-f+fD/fQAo3BCbCDlrUpznF1A5Zp9rB0noS5vnoormHSIPFKL0Z2DcUJ3Gxp5ytH4uLRNxy7AwYUC9exZzqGMAw==", "integrity": "sha512-kQ5JNTrbDv3Rp5X2n/iUu37IJBDU2gsZ5R/g1/KHOOEc5IKfUFjXT6DENPGduh08I/pamwtEq4oul7gUqKTQDQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/yargs-parser": "*" "@types/yargs-parser": "*"
@ -30752,9 +30760,9 @@
} }
}, },
"@types/graceful-fs": { "@types/graceful-fs": {
"version": "4.1.4", "version": "4.1.5",
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.4.tgz", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz",
"integrity": "sha512-mWA/4zFQhfvOA8zWkXobwJvBD7vzcxgrOQ0J5CH1votGqdq9m7+FwtGaqyCZqC3NyyBkc9z4m+iry4LlqcMWJg==", "integrity": "sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw==",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/node": "*" "@types/node": "*"
@ -39218,9 +39226,9 @@
} }
}, },
"@types/yargs": { "@types/yargs": {
"version": "15.0.12", "version": "15.0.13",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.12.tgz", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.13.tgz",
"integrity": "sha512-f+fD/fQAo3BCbCDlrUpznF1A5Zp9rB0noS5vnoormHSIPFKL0Z2DcUJ3Gxp5ytH4uLRNxy7AwYUC9exZzqGMAw==", "integrity": "sha512-kQ5JNTrbDv3Rp5X2n/iUu37IJBDU2gsZ5R/g1/KHOOEc5IKfUFjXT6DENPGduh08I/pamwtEq4oul7gUqKTQDQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/yargs-parser": "*" "@types/yargs-parser": "*"
@ -39473,9 +39481,9 @@
} }
}, },
"fsevents": { "fsevents": {
"version": "2.3.1", "version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.1.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-YR47Eg4hChJGAB1O3yEAOkGO+rlzutoICGqGo9EZ4lKWokzZRSyIW1QmTzqjtw8MJdj9srP869CuWw/hyzSiBw==", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true, "dev": true,
"optional": true "optional": true
}, },
@ -40261,9 +40269,9 @@
} }
}, },
"string-width": { "string-width": {
"version": "4.2.0", "version": "4.2.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz",
"integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==",
"dev": true, "dev": true,
"requires": { "requires": {
"emoji-regex": "^8.0.0", "emoji-regex": "^8.0.0",

View File

@ -1,10 +1,10 @@
import * as Cosmos from "@azure/cosmos"; import * as Cosmos from "@azure/cosmos";
import { RequestInfo, setAuthorizationTokenHeaderUsingMasterKey } from "@azure/cosmos"; import { RequestInfo, setAuthorizationTokenHeaderUsingMasterKey } from "@azure/cosmos";
import { configContext, Platform } from "../ConfigContext"; import { configContext, Platform } from "../ConfigContext";
import { getErrorMessage } from "./ErrorHandlingUtils"; import { userContext } from "../UserContext";
import { logConsoleError } from "../Utils/NotificationConsoleUtils"; import { logConsoleError } from "../Utils/NotificationConsoleUtils";
import { EmulatorMasterKey, HttpHeaders } from "./Constants"; import { EmulatorMasterKey, HttpHeaders } from "./Constants";
import { userContext } from "../UserContext"; import { getErrorMessage } from "./ErrorHandlingUtils";
const _global = typeof self === "undefined" ? window : self; const _global = typeof self === "undefined" ? window : self;

View File

@ -1,8 +1,9 @@
import { ARMError } from "../Utils/arm/request";
import { HttpStatusCodes } from "./Constants";
import { MessageTypes } from "../Contracts/ExplorerContracts"; import { MessageTypes } from "../Contracts/ExplorerContracts";
import { SubscriptionType } from "../Contracts/SubscriptionType"; import { SubscriptionType } from "../Contracts/SubscriptionType";
import { userContext } from "../UserContext";
import { ARMError } from "../Utils/arm/request";
import { logConsoleError } from "../Utils/NotificationConsoleUtils"; import { logConsoleError } from "../Utils/NotificationConsoleUtils";
import { HttpStatusCodes } from "./Constants";
import { logError } from "./Logger"; import { logError } from "./Logger";
import { sendMessage } from "./MessageHandler"; import { sendMessage } from "./MessageHandler";
@ -44,7 +45,7 @@ const sendNotificationForError = (errorMessage: string, errorCode: number | stri
const replaceKnownError = (errorMessage: string): string => { const replaceKnownError = (errorMessage: string): string => {
if ( if (
window.dataExplorer?.subscriptionType() === SubscriptionType.Internal && userContext.subscriptionType === SubscriptionType.Internal &&
errorMessage?.indexOf("SharedOffer is Disabled for your account") >= 0 errorMessage?.indexOf("SharedOffer is Disabled for your account") >= 0
) { ) {
return "Database throughput is not supported for internal subscriptions."; return "Database throughput is not supported for internal subscriptions.";

View File

@ -777,14 +777,8 @@ export default class Explorer {
$(document.body).click(() => $(".commandDropdownContainer").hide()); $(document.body).click(() => $(".commandDropdownContainer").hide());
}); });
// TODO move this to API customization class switch (userContext.apiType) {
this.defaultExperience.subscribe((defaultExperience) => { case "SQL":
const defaultExperienceNormalizedString = (
defaultExperience || Constants.DefaultAccountExperience.Default
).toLowerCase();
switch (defaultExperienceNormalizedString) {
case Constants.DefaultAccountExperience.DocumentDB.toLowerCase():
this.addCollectionText("New Container"); this.addCollectionText("New Container");
this.addDatabaseText("New Database"); this.addDatabaseText("New Database");
this.collectionTitle("SQL API"); this.collectionTitle("SQL API");
@ -800,8 +794,7 @@ export default class Explorer {
this.deleteCollectionConfirmationPane.collectionIdConfirmationText("Confirm by typing the container id"); this.deleteCollectionConfirmationPane.collectionIdConfirmationText("Confirm by typing the container id");
this.refreshTreeTitle("Refresh containers"); this.refreshTreeTitle("Refresh containers");
break; break;
case Constants.DefaultAccountExperience.MongoDB.toLowerCase(): case "Mongo":
case Constants.DefaultAccountExperience.ApiForMongoDB.toLowerCase():
this.addCollectionText("New Collection"); this.addCollectionText("New Collection");
this.addDatabaseText("New Database"); this.addDatabaseText("New Database");
this.collectionTitle("Collections"); this.collectionTitle("Collections");
@ -815,7 +808,7 @@ export default class Explorer {
); );
this.refreshTreeTitle("Refresh collections"); this.refreshTreeTitle("Refresh collections");
break; break;
case Constants.DefaultAccountExperience.Graph.toLowerCase(): case "Gremlin":
this.addCollectionText("New Graph"); this.addCollectionText("New Graph");
this.addDatabaseText("New Database"); this.addDatabaseText("New Database");
this.deleteCollectionText("Delete Graph"); this.deleteCollectionText("Delete Graph");
@ -829,7 +822,7 @@ export default class Explorer {
this.deleteCollectionConfirmationPane.collectionIdConfirmationText("Confirm by typing the graph id"); this.deleteCollectionConfirmationPane.collectionIdConfirmationText("Confirm by typing the graph id");
this.refreshTreeTitle("Refresh graphs"); this.refreshTreeTitle("Refresh graphs");
break; break;
case Constants.DefaultAccountExperience.Table.toLowerCase(): case "Tables":
this.addCollectionText("New Table"); this.addCollectionText("New Table");
this.addDatabaseText("New Database"); this.addDatabaseText("New Database");
this.deleteCollectionText("Delete Table"); this.deleteCollectionText("Delete Table");
@ -846,7 +839,7 @@ export default class Explorer {
this.deleteCollectionConfirmationPane.collectionIdConfirmationText("Confirm by typing the table id"); this.deleteCollectionConfirmationPane.collectionIdConfirmationText("Confirm by typing the table id");
this.tableDataClient = new TablesAPIDataClient(); this.tableDataClient = new TablesAPIDataClient();
break; break;
case Constants.DefaultAccountExperience.Cassandra.toLowerCase(): case "Cassandra":
this.addCollectionText("New Table"); this.addCollectionText("New Table");
this.addDatabaseText("New Keyspace"); this.addDatabaseText("New Keyspace");
this.deleteCollectionText("Delete Table"); this.deleteCollectionText("Delete Table");
@ -866,7 +859,6 @@ export default class Explorer {
this.tableDataClient = new CassandraAPIDataClient(); this.tableDataClient = new CassandraAPIDataClient();
break; break;
} }
});
this.commandBarComponentAdapter = new CommandBarComponentAdapter(this); this.commandBarComponentAdapter = new CommandBarComponentAdapter(this);

View File

@ -0,0 +1,86 @@
import { observable } from "knockout";
import { mostRecentActivity } from "./MostRecentActivity";
describe("MostRecentActivity", () => {
const accountId = "some account";
beforeEach(() => mostRecentActivity.clear(accountId));
it("Has no items at first", () => {
expect(mostRecentActivity.getItems(accountId)).toStrictEqual([]);
});
it("Can record collections being opened", () => {
const collectionId = "some collection";
const databaseId = "some database";
const collection = {
id: observable(collectionId),
databaseId,
};
mostRecentActivity.collectionWasOpened(accountId, collection);
const activity = mostRecentActivity.getItems(accountId);
expect(activity).toEqual([
expect.objectContaining({
collectionId,
databaseId,
}),
]);
});
it("Can record notebooks being opened", () => {
const name = "some notebook";
const path = "some path";
const notebook = { name, path };
mostRecentActivity.notebookWasItemOpened(accountId, notebook);
const activity = mostRecentActivity.getItems(accountId);
expect(activity).toEqual([expect.objectContaining(notebook)]);
});
it("Filters out duplicates", () => {
const name = "some notebook";
const path = "some path";
const notebook = { name, path };
const sameNotebook = { name, path };
mostRecentActivity.notebookWasItemOpened(accountId, notebook);
mostRecentActivity.notebookWasItemOpened(accountId, sameNotebook);
const activity = mostRecentActivity.getItems(accountId);
expect(activity.length).toEqual(1);
expect(activity).toEqual([expect.objectContaining(notebook)]);
});
it("Allows for multiple accounts", () => {
const name = "some notebook";
const path = "some path";
const notebook = { name, path };
const anotherNotebook = { name: "Another " + name, path };
const anotherAccountId = "Another " + accountId;
mostRecentActivity.notebookWasItemOpened(accountId, notebook);
mostRecentActivity.notebookWasItemOpened(anotherAccountId, anotherNotebook);
expect(mostRecentActivity.getItems(accountId)).toEqual([expect.objectContaining(notebook)]);
expect(mostRecentActivity.getItems(anotherAccountId)).toEqual([expect.objectContaining(anotherNotebook)]);
});
it("Can store multiple distinct elements, in FIFO order", () => {
const name = "some notebook";
const path = "some path";
const first = { name, path };
const second = { name: "Another " + name, path };
const third = { name, path: "Another " + path };
mostRecentActivity.notebookWasItemOpened(accountId, first);
mostRecentActivity.notebookWasItemOpened(accountId, second);
mostRecentActivity.notebookWasItemOpened(accountId, third);
const activity = mostRecentActivity.getItems(accountId);
expect(activity).toEqual([third, second, first].map(expect.objectContaining));
});
});

View File

@ -1,4 +1,6 @@
import { CollectionBase } from "../../Contracts/ViewModels";
import { StorageKey, LocalStorageUtility } from "../../Shared/StorageUtility"; import { StorageKey, LocalStorageUtility } from "../../Shared/StorageUtility";
import { NotebookContentItem } from "../Notebook/NotebookContentItem";
export enum Type { export enum Type {
OpenCollection, OpenCollection,
@ -6,21 +8,18 @@ export enum Type {
} }
export interface OpenNotebookItem { export interface OpenNotebookItem {
type: Type.OpenNotebook;
name: string; name: string;
path: string; path: string;
} }
export interface OpenCollectionItem { export interface OpenCollectionItem {
type: Type.OpenCollection;
databaseId: string; databaseId: string;
collectionId: string; collectionId: string;
} }
export interface Item { type Item = OpenNotebookItem | OpenCollectionItem;
type: Type;
title: string;
description: string;
data: OpenNotebookItem | OpenCollectionItem;
}
// Update schemaVersion if you are going to change this interface // Update schemaVersion if you are going to change this interface
interface StoredData { interface StoredData {
@ -32,7 +31,7 @@ interface StoredData {
* Stores most recent activity * Stores most recent activity
*/ */
class MostRecentActivity { class MostRecentActivity {
private static readonly schemaVersion: string = "1"; private static readonly schemaVersion: string = "2";
private static itemsMaxNumber: number = 5; private static itemsMaxNumber: number = 5;
private storedData: StoredData; private storedData: StoredData;
constructor() { constructor() {
@ -92,7 +91,7 @@ class MostRecentActivity {
LocalStorageUtility.setEntryString(StorageKey.MostRecentActivity, JSON.stringify(this.storedData)); LocalStorageUtility.setEntryString(StorageKey.MostRecentActivity, JSON.stringify(this.storedData));
} }
public addItem(accountId: string, newItem: Item): void { private addItem(accountId: string, newItem: Item): void {
// When debugging, accountId is "undefined": most recent activity cannot be saved by account. Uncomment to disable. // When debugging, accountId is "undefined": most recent activity cannot be saved by account. Uncomment to disable.
// if (!accountId) { // if (!accountId) {
// return; // return;
@ -111,6 +110,23 @@ class MostRecentActivity {
return this.storedData.itemsMap[accountId] || []; return this.storedData.itemsMap[accountId] || [];
} }
public collectionWasOpened(accountId: string, { id, databaseId }: Pick<CollectionBase, "id" | "databaseId">) {
const collectionId = id();
this.addItem(accountId, {
type: Type.OpenCollection,
databaseId,
collectionId,
});
}
public notebookWasItemOpened(accountId: string, { name, path }: Pick<NotebookContentItem, "name" | "path">) {
this.addItem(accountId, {
type: Type.OpenNotebook,
name,
path,
});
}
public clear(accountId: string): void { public clear(accountId: string): void {
delete this.storedData.itemsMap[accountId]; delete this.storedData.itemsMap[accountId];
this.saveToLocalStorage(); this.saveToLocalStorage();
@ -128,11 +144,7 @@ class MostRecentActivity {
let index = -1; let index = -1;
for (let i = 0; i < itemsArray.length; i++) { for (let i = 0; i < itemsArray.length; i++) {
const currentItem = itemsArray[i]; const currentItem = itemsArray[i];
if ( if (JSON.stringify(currentItem) === JSON.stringify(item)) {
currentItem.title === item.title &&
currentItem.description === item.description &&
JSON.stringify(currentItem.data) === JSON.stringify(item.data)
) {
index = i; index = i;
break; break;
} }

View File

@ -217,42 +217,6 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
return heroes; return heroes;
} }
private getItemIcon(item: MostRecentActivity.Item): string {
switch (item.type) {
case MostRecentActivity.Type.OpenCollection:
return CollectionIcon;
case MostRecentActivity.Type.OpenNotebook:
return NotebookIcon;
default:
return null;
}
}
private onItemClicked(item: MostRecentActivity.Item) {
switch (item.type) {
case MostRecentActivity.Type.OpenCollection: {
const openCollectionitem = item.data as MostRecentActivity.OpenCollectionItem;
const collection = this.container.findCollection(
openCollectionitem.databaseId,
openCollectionitem.collectionId
);
if (collection) {
collection.openTab();
}
break;
}
case MostRecentActivity.Type.OpenNotebook: {
const openNotebookItem = item.data as MostRecentActivity.OpenNotebookItem;
const notebookItem = this.container.createNotebookContentItemFile(openNotebookItem.name, openNotebookItem.path);
notebookItem && this.container.openNotebook(notebookItem);
break;
}
default:
console.error("Unknown item type", item);
break;
}
}
private createCommonTaskItems(): SplashScreenItem[] { private createCommonTaskItems(): SplashScreenItem[] {
const items: SplashScreenItem[] = []; const items: SplashScreenItem[] = [];
@ -333,23 +297,45 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
return items; return items;
} }
private static getInfo(item: MostRecentActivity.Item): string { private decorateOpenCollectionActivity({ databaseId, collectionId }: MostRecentActivity.OpenCollectionItem) {
if (item.type === MostRecentActivity.Type.OpenNotebook) { return {
const data = item.data as MostRecentActivity.OpenNotebookItem; iconSrc: NotebookIcon,
return data.path; title: collectionId,
} else { description: "Data",
return undefined; onClick: () => {
const collection = this.container.findCollection(databaseId, collectionId);
collection && collection.openTab();
},
};
} }
private decorateOpenNotebookActivity({ name, path }: MostRecentActivity.OpenNotebookItem) {
return {
info: path,
iconSrc: CollectionIcon,
title: name,
description: "Notebook",
onClick: () => {
const notebookItem = this.container.createNotebookContentItemFile(name, path);
notebookItem && this.container.openNotebook(notebookItem);
},
};
} }
private createRecentItems(): SplashScreenItem[] { private createRecentItems(): SplashScreenItem[] {
return MostRecentActivity.mostRecentActivity.getItems(userContext.databaseAccount?.id).map((item) => ({ return MostRecentActivity.mostRecentActivity.getItems(userContext.databaseAccount?.id).map((activity) => {
iconSrc: this.getItemIcon(item), switch (activity.type) {
title: item.title, default: {
description: item.description, const unknownActivity: never = activity;
info: SplashScreen.getInfo(item), throw new Error(`Unknown activity: ${unknownActivity}`);
onClick: () => this.onItemClicked(item), }
})); case MostRecentActivity.Type.OpenNotebook:
return this.decorateOpenNotebookActivity(activity);
case MostRecentActivity.Type.OpenCollection:
return this.decorateOpenCollectionActivity(activity);
}
});
} }
private createTipsItems(): SplashScreenItem[] { private createTipsItems(): SplashScreenItem[] {

View File

@ -5,7 +5,7 @@ import { TreeComponent, TreeNode, TreeNodeMenuItem } from "../Controls/TreeCompo
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem"; import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem";
import { ResourceTreeContextMenuButtonFactory } from "../ContextMenuButtonFactory"; import { ResourceTreeContextMenuButtonFactory } from "../ContextMenuButtonFactory";
import * as MostRecentActivity from "../MostRecentActivity/MostRecentActivity"; import { mostRecentActivity } from "../MostRecentActivity/MostRecentActivity";
import CopyIcon from "../../../images/notebook/Notebook-copy.svg"; import CopyIcon from "../../../images/notebook/Notebook-copy.svg";
import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg"; import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg";
import CollectionIcon from "../../../images/tree-collection.svg"; import CollectionIcon from "../../../images/tree-collection.svg";
@ -179,15 +179,7 @@ export class ResourceTree extends React.Component<ResourceTreeProps> {
onClick: () => { onClick: () => {
collection.openTab(); collection.openTab();
// push to most recent // push to most recent
MostRecentActivity.mostRecentActivity.addItem(userContext.databaseAccount?.id, { mostRecentActivity.collectionWasOpened(userContext.databaseAccount?.id, collection);
type: MostRecentActivity.Type.OpenCollection,
title: collection.id(),
description: "Data",
data: {
databaseId: collection.databaseId,
collectionId: collection.id(),
},
});
}, },
isSelected: () => isSelected: () =>
this.isDataNodeSelected(collection.databaseId, collection.id(), [ this.isDataNodeSelected(collection.databaseId, collection.id(), [
@ -492,7 +484,7 @@ export class ResourceTree extends React.Component<ResourceTreeProps> {
(item: NotebookContentItem) => { (item: NotebookContentItem) => {
this.container.openNotebook(item).then((hasOpened) => { this.container.openNotebook(item).then((hasOpened) => {
if (hasOpened) { if (hasOpened) {
this.pushItemToMostRecent(item); mostRecentActivity.notebookWasItemOpened(userContext.databaseAccount?.id, item);
} }
}); });
}, },
@ -513,7 +505,7 @@ export class ResourceTree extends React.Component<ResourceTreeProps> {
(item: NotebookContentItem) => { (item: NotebookContentItem) => {
this.container.openNotebook(item).then((hasOpened) => { this.container.openNotebook(item).then((hasOpened) => {
if (hasOpened) { if (hasOpened) {
this.pushItemToMostRecent(item); mostRecentActivity.notebookWasItemOpened(userContext.databaseAccount?.id, item);
} }
}); });
}, },
@ -543,18 +535,6 @@ export class ResourceTree extends React.Component<ResourceTreeProps> {
return gitHubNotebooksTree; return gitHubNotebooksTree;
} }
private pushItemToMostRecent(item: NotebookContentItem) {
MostRecentActivity.mostRecentActivity.addItem(userContext.databaseAccount?.id, {
type: MostRecentActivity.Type.OpenNotebook,
title: item.name,
description: "Notebook",
data: {
name: item.name,
path: item.path,
},
});
}
private buildChildNodes( private buildChildNodes(
item: NotebookContentItem, item: NotebookContentItem,
onFileClick: (item: NotebookContentItem) => void, onFileClick: (item: NotebookContentItem) => void,

View File

@ -1,5 +1,5 @@
import * as ko from "knockout"; import * as ko from "knockout";
import * as MostRecentActivity from "../MostRecentActivity/MostRecentActivity"; import { mostRecentActivity } from "../MostRecentActivity/MostRecentActivity";
import * as React from "react"; import * as React from "react";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { NotebookContentItem } from "../Notebook/NotebookContentItem"; import { NotebookContentItem } from "../Notebook/NotebookContentItem";
@ -44,15 +44,7 @@ export class ResourceTreeAdapterForResourceToken implements ReactAdapter {
onClick: () => { onClick: () => {
collection.onDocumentDBDocumentsClick(); collection.onDocumentDBDocumentsClick();
// push to most recent // push to most recent
MostRecentActivity.mostRecentActivity.addItem(userContext.databaseAccount?.id, { mostRecentActivity.collectionWasOpened(userContext.databaseAccount?.id, collection);
type: MostRecentActivity.Type.OpenCollection,
title: collection.id(),
description: "Data",
data: {
databaseId: collection.databaseId,
collectionId: collection.id(),
},
});
}, },
isSelected: () => isSelected: () =>
this.isDataNodeSelected(collection.databaseId, collection.id(), ViewModels.CollectionTabKind.Documents), this.isDataNodeSelected(collection.databaseId, collection.id(), ViewModels.CollectionTabKind.Documents),

View File

@ -1,76 +1,71 @@
// CSS Dependencies // CSS Dependencies
import "abort-controller/polyfill";
import "babel-polyfill";
import "bootstrap/dist/css/bootstrap.css"; import "bootstrap/dist/css/bootstrap.css";
import "../less/documentDB.less"; import "es6-object-assign/auto";
import "../less/tree.less"; import "es6-symbol/implement";
import "../less/forms.less"; import "object.entries/auto";
import "../less/menus.less"; import { initializeIcons } from "office-ui-fabric-react/lib/Icons";
import "../less/infobox.less"; import "promise-polyfill/src/polyfill";
import "../less/messagebox.less"; import "promise.prototype.finally/auto";
import "./Explorer/Controls/ErrorDisplayComponent/ErrorDisplayComponent.less"; import React, { useState } from "react";
import "./Explorer/Menus/NotificationConsole/NotificationConsole.less"; import ReactDOM from "react-dom";
import "./Explorer/Menus/CommandBar/CommandBarComponent.less"; import "url-polyfill/url-polyfill.min";
import "./Explorer/Menus/CommandBar/MemoryTrackerComponent.less"; import "webcrypto-liner/build/webcrypto-liner.shim.min";
import "./Explorer/Controls/CollapsiblePanel/CollapsiblePanelComponent.less"; import "whatwg-fetch";
import "./Explorer/Controls/DynamicList/DynamicListComponent.less";
import "./Explorer/Controls/JsonEditor/JsonEditorComponent.less";
import "./Explorer/Graph/GraphExplorerComponent/graphExplorer.less";
import "./Explorer/Panes/PanelComponent.less";
import "../less/TableStyles/queryBuilder.less";
import "../externals/jquery.dataTables.min.css";
import "../less/TableStyles/fulldatatables.less";
import "../less/TableStyles/EntityEditor.less";
import "../less/TableStyles/CustomizeColumns.less";
import "../less/resourceTree.less";
import "../externals/jquery.typeahead.min.css";
import "../externals/jquery-ui.min.css"; import "../externals/jquery-ui.min.css";
import "../externals/jquery-ui.min.js";
import "../externals/jquery-ui.structure.min.css"; import "../externals/jquery-ui.structure.min.css";
import "../externals/jquery-ui.theme.min.css"; import "../externals/jquery-ui.theme.min.css";
import "./Explorer/Graph/NewVertexComponent/newVertexComponent.less"; import "../externals/jquery.dataTables.min.css";
import "./Explorer/Panes/GraphNewVertexPane.less"; import "../externals/jquery.typeahead.min.css";
import "./Explorer/Tabs/QueryTab.less"; import "../externals/jquery.typeahead.min.js";
import "./Explorer/Controls/TreeComponent/treeComponent.less";
import "./Explorer/Controls/Accordion/AccordionComponent.less";
import "./Explorer/SplashScreen/SplashScreen.less";
import "./Explorer/Controls/Notebook/NotebookTerminalComponent.less";
// Image Dependencies // Image Dependencies
import "../images/CosmosDB_rgb_ui_lighttheme.ico"; import "../images/CosmosDB_rgb_ui_lighttheme.ico";
import "../images/favicon.ico"; import "../images/favicon.ico";
import "./Shared/appInsights";
import "babel-polyfill";
import "es6-symbol/implement";
import "webcrypto-liner/build/webcrypto-liner.shim.min";
import "./Libs/jquery";
import "bootstrap/dist/js/npm";
import "../externals/jquery.typeahead.min.js";
import "../externals/jquery-ui.min.js";
import "promise-polyfill/src/polyfill";
import "abort-controller/polyfill";
import "whatwg-fetch";
import "es6-object-assign/auto";
import "promise.prototype.finally/auto";
import "object.entries/auto";
import "./Libs/is-integer-polyfill";
import "url-polyfill/url-polyfill.min";
import { initializeIcons } from "office-ui-fabric-react/lib/Icons";
import Explorer, { ExplorerParams } from "./Explorer/Explorer";
import React, { useState } from "react";
import ReactDOM from "react-dom";
import hdeConnectImage from "../images/HdeConnectCosmosDB.svg"; import hdeConnectImage from "../images/HdeConnectCosmosDB.svg";
import refreshImg from "../images/refresh-cosmos.svg";
import arrowLeftImg from "../images/imgarrowlefticon.svg"; import arrowLeftImg from "../images/imgarrowlefticon.svg";
import { KOCommentEnd, KOCommentIfStart } from "./koComment"; import refreshImg from "../images/refresh-cosmos.svg";
import { useConfig } from "./hooks/useConfig"; import "../less/documentDB.less";
import { useKnockoutExplorer } from "./hooks/useKnockoutExplorer"; import "../less/forms.less";
import { useSidePanel } from "./hooks/useSidePanel"; import "../less/infobox.less";
import "../less/menus.less";
import "../less/messagebox.less";
import "../less/resourceTree.less";
import "../less/TableStyles/CustomizeColumns.less";
import "../less/TableStyles/EntityEditor.less";
import "../less/TableStyles/fulldatatables.less";
import "../less/TableStyles/queryBuilder.less";
import "../less/tree.less";
import "./Explorer/Controls/Accordion/AccordionComponent.less";
import "./Explorer/Controls/CollapsiblePanel/CollapsiblePanelComponent.less";
import { Dialog, DialogProps } from "./Explorer/Controls/Dialog";
import "./Explorer/Controls/DynamicList/DynamicListComponent.less";
import "./Explorer/Controls/ErrorDisplayComponent/ErrorDisplayComponent.less";
import "./Explorer/Controls/JsonEditor/JsonEditorComponent.less";
import "./Explorer/Controls/Notebook/NotebookTerminalComponent.less";
import "./Explorer/Controls/TreeComponent/treeComponent.less";
import Explorer, { ExplorerParams } from "./Explorer/Explorer";
import "./Explorer/Graph/GraphExplorerComponent/graphExplorer.less";
import "./Explorer/Graph/NewVertexComponent/newVertexComponent.less";
import "./Explorer/Menus/CommandBar/CommandBarComponent.less";
import "./Explorer/Menus/CommandBar/MemoryTrackerComponent.less";
import "./Explorer/Menus/NotificationConsole/NotificationConsole.less";
import { NotificationConsoleComponent } from "./Explorer/Menus/NotificationConsole/NotificationConsoleComponent"; import { NotificationConsoleComponent } from "./Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
import "./Explorer/Panes/GraphNewVertexPane.less";
import "./Explorer/Panes/PanelComponent.less";
import { PanelContainerComponent } from "./Explorer/Panes/PanelContainerComponent"; import { PanelContainerComponent } from "./Explorer/Panes/PanelContainerComponent";
import { SplashScreen } from "./Explorer/SplashScreen/SplashScreen"; import { SplashScreen } from "./Explorer/SplashScreen/SplashScreen";
import { Dialog, DialogProps } from "./Explorer/Controls/Dialog"; import "./Explorer/SplashScreen/SplashScreen.less";
import { ResourceTree } from "./Explorer/Tree/ResourceTree"; import "./Explorer/Tabs/QueryTab.less";
import { useConfig } from "./hooks/useConfig";
import { useKnockoutExplorer } from "./hooks/useKnockoutExplorer";
import { useNotebooks } from "./hooks/useNotebooks"; import { useNotebooks } from "./hooks/useNotebooks";
import { useSidePanel } from "./hooks/useSidePanel";
import { KOCommentEnd, KOCommentIfStart } from "./koComment";
import "./Libs/is-integer-polyfill";
import "./Libs/jquery";
import "./Shared/appInsights";
initializeIcons(); initializeIcons();
@ -121,6 +116,9 @@ const App: React.FunctionComponent = () => {
const explorer = useKnockoutExplorer(config?.platform, explorerParams); const explorer = useKnockoutExplorer(config?.platform, explorerParams);
context.container = explorer; context.container = explorer;
if (!explorer) {
return <LoadingExplorer />;
}
return ( return (
<div className="flexContainer"> <div className="flexContainer">
@ -263,21 +261,6 @@ const App: React.FunctionComponent = () => {
/> />
</div> </div>
</div> </div>
{/* Global loader - Start */}
<div className="splashLoaderContainer" data-bind="visible: isRefreshingExplorer">
<div className="splashLoaderContentContainer">
<p className="connectExplorerContent">
<img src={hdeConnectImage} alt="Azure Cosmos DB" />
</p>
<p className="splashLoaderTitle" id="explorerLoadingStatusTitle">
Welcome to Azure Cosmos DB
</p>
<p className="splashLoaderText" id="explorerLoadingStatusText" role="alert">
Connecting...
</p>
</div>
</div>
{/* Global loader - End */}
<PanelContainerComponent <PanelContainerComponent
isOpen={isPanelOpen} isOpen={isPanelOpen}
panelContent={panelContent} panelContent={panelContent}
@ -321,3 +304,21 @@ const App: React.FunctionComponent = () => {
}; };
ReactDOM.render(<App />, document.body); ReactDOM.render(<App />, document.body);
function LoadingExplorer(): JSX.Element {
return (
<div className="splashLoaderContainer">
<div className="splashLoaderContentContainer">
<p className="connectExplorerContent">
<img src={hdeConnectImage} alt="Azure Cosmos DB" />
</p>
<p className="splashLoaderTitle" id="explorerLoadingStatusTitle">
Welcome to Azure Cosmos DB
</p>
<p className="splashLoaderText" id="explorerLoadingStatusText" role="alert">
Connecting...
</p>
</div>
</div>
);
}

View File

@ -1,4 +1,4 @@
import { useEffect } from "react"; import { useEffect, useState } from "react";
import { applyExplorerBindings } from "../applyExplorerBindings"; import { applyExplorerBindings } from "../applyExplorerBindings";
import { AuthType } from "../AuthType"; import { AuthType } from "../AuthType";
import { AccountKind, DefaultAccountExperience } from "../Common/Constants"; import { AccountKind, DefaultAccountExperience } from "../Common/Constants";
@ -32,53 +32,65 @@ import { isInvalidParentFrameOrigin } from "../Utils/MessageValidation";
// This hook will create a new instance of Explorer.ts and bind it to the DOM // This hook will create a new instance of Explorer.ts and bind it to the DOM
// This hook has a LOT of magic, but ideally we can delete it once we have removed KO and switched entirely to React // This hook has a LOT of magic, but ideally we can delete it once we have removed KO and switched entirely to React
// Pleas tread carefully :) // Pleas tread carefully :)
let explorer: Explorer;
export function useKnockoutExplorer(platform: Platform, explorerParams: ExplorerParams): Explorer { export function useKnockoutExplorer(platform: Platform, explorerParams: ExplorerParams): Explorer {
explorer = explorer || new Explorer(explorerParams); const [explorer, setExplorer] = useState<Explorer>();
useEffect(() => { useEffect(() => {
const effect = async () => { const effect = async () => {
if (platform) { if (platform) {
if (platform === Platform.Hosted) { if (platform === Platform.Hosted) {
await configureHosted(); const explorer = await configureHosted(explorerParams);
applyExplorerBindings(explorer); setExplorer(explorer);
} else if (platform === Platform.Emulator) { } else if (platform === Platform.Emulator) {
configureEmulator(); const explorer = configureEmulator(explorerParams);
applyExplorerBindings(explorer); setExplorer(explorer);
} else if (platform === Platform.Portal) { } else if (platform === Platform.Portal) {
configurePortal(); const explorer = await configurePortal(explorerParams);
setExplorer(explorer);
} }
} }
}; };
effect(); effect();
}, [platform]); }, [platform]);
useEffect(() => {
if (explorer) {
applyExplorerBindings(explorer);
}
}, [explorer]);
return explorer; return explorer;
} }
async function configureHosted() { async function configureHosted(explorerParams: ExplorerParams): Promise<Explorer> {
const win = (window as unknown) as HostedExplorerChildFrame; const win = (window as unknown) as HostedExplorerChildFrame;
if (win.hostedConfig.authType === AuthType.EncryptedToken) { if (win.hostedConfig.authType === AuthType.EncryptedToken) {
configureHostedWithEncryptedToken(win.hostedConfig); return configureHostedWithEncryptedToken(win.hostedConfig, explorerParams);
} else if (win.hostedConfig.authType === AuthType.ResourceToken) { } else if (win.hostedConfig.authType === AuthType.ResourceToken) {
configureHostedWithResourceToken(win.hostedConfig); return configureHostedWithResourceToken(win.hostedConfig, explorerParams);
} else if (win.hostedConfig.authType === AuthType.ConnectionString) { } else if (win.hostedConfig.authType === AuthType.ConnectionString) {
configureHostedWithConnectionString(win.hostedConfig); return configureHostedWithConnectionString(win.hostedConfig, explorerParams);
} else if (win.hostedConfig.authType === AuthType.AAD) { } else if (win.hostedConfig.authType === AuthType.AAD) {
await configureHostedWithAAD(win.hostedConfig); return configureHostedWithAAD(win.hostedConfig, explorerParams);
} }
throw new Error(`Unknown hosted config: ${win.hostedConfig}`);
} }
async function configureHostedWithAAD(config: AAD) { async function configureHostedWithAAD(config: AAD, explorerParams: ExplorerParams): Promise<Explorer> {
const account = config.databaseAccount; const account = config.databaseAccount;
const accountResourceId = account.id; const accountResourceId = account.id;
const subscriptionId = accountResourceId && accountResourceId.split("subscriptions/")[1].split("/")[0]; const subscriptionId = accountResourceId && accountResourceId.split("subscriptions/")[1].split("/")[0];
const resourceGroup = accountResourceId && accountResourceId.split("resourceGroups/")[1].split("/")[0]; const resourceGroup = accountResourceId && accountResourceId.split("resourceGroups/")[1].split("/")[0];
updateUserContext({ updateUserContext({
subscriptionId,
resourceGroup,
authType: AuthType.AAD, authType: AuthType.AAD,
authorizationToken: `Bearer ${config.authorizationToken}`, authorizationToken: `Bearer ${config.authorizationToken}`,
databaseAccount: config.databaseAccount, databaseAccount: config.databaseAccount,
}); });
const keys = await listKeys(subscriptionId, resourceGroup, account.name); const keys = await listKeys(subscriptionId, resourceGroup, account.name);
const explorer = new Explorer(explorerParams);
explorer.configure({ explorer.configure({
databaseAccount: account, databaseAccount: account,
subscriptionId, subscriptionId,
@ -87,9 +99,10 @@ async function configureHostedWithAAD(config: AAD) {
authorizationToken: `Bearer ${config.authorizationToken}`, authorizationToken: `Bearer ${config.authorizationToken}`,
features: extractFeatures(), features: extractFeatures(),
}); });
return explorer;
} }
function configureHostedWithConnectionString(config: ConnectionString) { function configureHostedWithConnectionString(config: ConnectionString, explorerParams: ExplorerParams): Explorer {
const apiExperience = DefaultExperienceUtility.getDefaultExperienceFromApiKind(config.encryptedTokenMetadata.apiKind); const apiExperience = DefaultExperienceUtility.getDefaultExperienceFromApiKind(config.encryptedTokenMetadata.apiKind);
const databaseAccount = { const databaseAccount = {
id: "", id: "",
@ -106,14 +119,16 @@ function configureHostedWithConnectionString(config: ConnectionString) {
accessToken: encodeURIComponent(config.encryptedToken), accessToken: encodeURIComponent(config.encryptedToken),
databaseAccount, databaseAccount,
}); });
const explorer = new Explorer(explorerParams);
explorer.configure({ explorer.configure({
databaseAccount, databaseAccount,
masterKey: config.masterKey, masterKey: config.masterKey,
features: extractFeatures(), features: extractFeatures(),
}); });
return explorer;
} }
function configureHostedWithResourceToken(config: ResourceToken) { function configureHostedWithResourceToken(config: ResourceToken, explorerParams: ExplorerParams): Explorer {
const parsedResourceToken = parseResourceTokenConnectionString(config.resourceToken); const parsedResourceToken = parseResourceTokenConnectionString(config.resourceToken);
const databaseAccount = { const databaseAccount = {
id: "", id: "",
@ -131,6 +146,7 @@ function configureHostedWithResourceToken(config: ResourceToken) {
resourceToken: parsedResourceToken.resourceToken, resourceToken: parsedResourceToken.resourceToken,
endpoint: parsedResourceToken.accountEndpoint, endpoint: parsedResourceToken.accountEndpoint,
}); });
const explorer = new Explorer(explorerParams);
explorer.resourceTokenDatabaseId(parsedResourceToken.databaseId); explorer.resourceTokenDatabaseId(parsedResourceToken.databaseId);
explorer.resourceTokenCollectionId(parsedResourceToken.collectionId); explorer.resourceTokenCollectionId(parsedResourceToken.collectionId);
if (parsedResourceToken.partitionKey) { if (parsedResourceToken.partitionKey) {
@ -142,9 +158,10 @@ function configureHostedWithResourceToken(config: ResourceToken) {
isAuthWithresourceToken: true, isAuthWithresourceToken: true,
}); });
explorer.isRefreshingExplorer(false); explorer.isRefreshingExplorer(false);
return explorer;
} }
function configureHostedWithEncryptedToken(config: EncryptedToken) { function configureHostedWithEncryptedToken(config: EncryptedToken, explorerParams: ExplorerParams): Explorer {
updateUserContext({ updateUserContext({
authType: AuthType.EncryptedToken, authType: AuthType.EncryptedToken,
accessToken: encodeURIComponent(config.encryptedToken), accessToken: encodeURIComponent(config.encryptedToken),
@ -152,6 +169,7 @@ function configureHostedWithEncryptedToken(config: EncryptedToken) {
const apiExperience: string = DefaultExperienceUtility.getDefaultExperienceFromApiKind( const apiExperience: string = DefaultExperienceUtility.getDefaultExperienceFromApiKind(
config.encryptedTokenMetadata.apiKind config.encryptedTokenMetadata.apiKind
); );
const explorer = new Explorer(explorerParams);
explorer.configure({ explorer.configure({
databaseAccount: { databaseAccount: {
id: "", id: "",
@ -162,21 +180,25 @@ function configureHostedWithEncryptedToken(config: EncryptedToken) {
}, },
features: extractFeatures(), features: extractFeatures(),
}); });
return explorer;
} }
function configureEmulator() { function configureEmulator(explorerParams: ExplorerParams): Explorer {
updateUserContext({ updateUserContext({
databaseAccount: emulatorAccount, databaseAccount: emulatorAccount,
authType: AuthType.MasterKey, authType: AuthType.MasterKey,
}); });
const explorer = new Explorer(explorerParams);
explorer.databaseAccount(emulatorAccount); explorer.databaseAccount(emulatorAccount);
explorer.isAccountReady(true); explorer.isAccountReady(true);
return explorer;
} }
function configurePortal() { async function configurePortal(explorerParams: ExplorerParams): Promise<Explorer> {
updateUserContext({ updateUserContext({
authType: AuthType.AAD, authType: AuthType.AAD,
}); });
return new Promise((resolve) => {
// In development mode, try to load the iframe message from session storage. // In development mode, try to load the iframe message from session storage.
// This allows webpack hot reload to function properly in the portal // This allows webpack hot reload to function properly in the portal
if (process.env.NODE_ENV === "development" && !window.location.search.includes("disablePortalInitCache")) { if (process.env.NODE_ENV === "development" && !window.location.search.includes("disablePortalInitCache")) {
@ -187,8 +209,9 @@ function configurePortal() {
"Loaded cached portal iframe message from session storage. Do a full page refresh to get a new message" "Loaded cached portal iframe message from session storage. Do a full page refresh to get a new message"
); );
console.dir(message); console.dir(message);
const explorer = new Explorer(explorerParams);
explorer.configure(message); explorer.configure(message);
applyExplorerBindings(explorer); resolve(explorer);
} }
} }
@ -236,8 +259,9 @@ function configurePortal() {
quotaId: inputs.quotaId, quotaId: inputs.quotaId,
}); });
const explorer = new Explorer(explorerParams);
explorer.configure(inputs); explorer.configure(inputs);
applyExplorerBindings(explorer); resolve(explorer);
if (openAction) { if (openAction) {
handleOpenAction(openAction, explorer.nonSystemDatabases(), explorer); handleOpenAction(openAction, explorer.nonSystemDatabases(), explorer);
} }
@ -247,6 +271,7 @@ function configurePortal() {
); );
sendMessage("ready"); sendMessage("ready");
});
} }
function shouldProcessMessage(event: MessageEvent): boolean { function shouldProcessMessage(event: MessageEvent): boolean {

View File

@ -1,8 +1,5 @@
import "expect-puppeteer"; import "expect-puppeteer";
import { getTestExplorerFrame } from "../testExplorer/TestExplorerUtils"; import { createDatabase, generateUniqueName, onClickSaveButton } from "../utils/shared";
import { createDatabase, onClickSaveButton } from "../utils/shared";
import { generateUniqueName } from "../utils/shared";
import { ApiKind } from "../../src/Contracts/DataModels";
const LOADING_STATE_DELAY = 5000; const LOADING_STATE_DELAY = 5000;
jest.setTimeout(300000); jest.setTimeout(300000);
@ -12,7 +9,9 @@ describe("MongoDB Index policy tests", () => {
try { try {
const singleFieldId = generateUniqueName("key"); const singleFieldId = generateUniqueName("key");
const wildCardId = generateUniqueName("key") + "$**"; const wildCardId = generateUniqueName("key") + "$**";
const frame = await getTestExplorerFrame(ApiKind.MongoDB); await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-mongo-runner");
const handle = await page.waitForSelector("iframe");
const frame = await handle.contentFrame();
const dropDown = "Index Type "; const dropDown = "Index Type ";
let index = 0; let index = 0;
@ -20,24 +19,18 @@ describe("MongoDB Index policy tests", () => {
await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true }); await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true });
await frame.waitFor(LOADING_STATE_DELAY); await frame.waitFor(LOADING_STATE_DELAY);
await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true }); await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true });
const dbId = await createDatabase(frame); const { databaseId, collectionId } = await createDatabase(frame);
await frame.waitFor(25000); await frame.waitFor(25000);
// click on database // click on database
await frame.waitForSelector(`div[data-test="${dbId}"]`); await frame.waitForSelector(`div[data-test="${databaseId}"]`);
await frame.waitFor(LOADING_STATE_DELAY); await frame.waitFor(LOADING_STATE_DELAY);
await frame.click(`div[data-test="${dbId}"]`); await frame.click(`div[data-test="${databaseId}"]`);
await frame.waitFor(LOADING_STATE_DELAY); await frame.waitFor(LOADING_STATE_DELAY);
// click on scale & setting // click on scale & setting
const containers = await frame.$$( await frame.waitFor(`div[data-test="${collectionId}"]`), { visible: true };
`div[class="nodeChildren"] > div[class="collectionHeader main2 nodeItem "]> div[class="treeNodeHeader "]`
);
const selectedContainer = (await frame.evaluate((element) => element.innerText, containers[0]))
.replace(/[\u{0080}-\u{FFFF}]/gu, "")
.trim();
await frame.waitFor(`div[data-test="${selectedContainer}"]`), { visible: true };
await frame.waitFor(LOADING_STATE_DELAY); await frame.waitFor(LOADING_STATE_DELAY);
await frame.click(`div[data-test="${selectedContainer}"]`); await frame.click(`div[data-test="${collectionId}"]`);
await frame.waitFor(`div[data-test="Scale & Settings"]`), { visible: true }; await frame.waitFor(`div[data-test="Scale & Settings"]`), { visible: true };
await frame.waitFor(LOADING_STATE_DELAY); await frame.waitFor(LOADING_STATE_DELAY);

View File

@ -1,19 +1,17 @@
import { uploadNotebookIfNotExist } from "./notebookTestUtils"; import { uploadNotebookIfNotExist } from "./notebookTestUtils";
import { ElementHandle, Frame } from "puppeteer";
import { getTestExplorerFrame } from "../testExplorer/TestExplorerUtils";
jest.setTimeout(300000); jest.setTimeout(300000);
const notebookName = "GettingStarted.ipynb"; const notebookName = "GettingStarted.ipynb";
let frame: Frame;
let uploadedNotebookNode: ElementHandle<Element>;
describe("Notebook UI tests", () => { describe("Notebook UI tests", () => {
it("Upload, Open and Delete Notebook", async () => { it("Upload, Open and Delete Notebook", async () => {
try { try {
frame = await getTestExplorerFrame(); await page.goto("https://localhost:1234/testExplorer.html");
const handle = await page.waitForSelector("iframe");
const frame = await handle.contentFrame();
await frame.waitForSelector(".galleryHeader"); await frame.waitForSelector(".galleryHeader");
uploadedNotebookNode = await uploadNotebookIfNotExist(frame, notebookName); const uploadedNotebookNode = await uploadNotebookIfNotExist(frame, notebookName);
await uploadedNotebookNode.click(); await uploadedNotebookNode.click();
await frame.waitForSelector(".tabNavText"); await frame.waitForSelector(".tabNavText");
const tabTitle = await frame.$eval(".tabNavText", (element) => element.textContent); const tabTitle = await frame.$eval(".tabNavText", (element) => element.textContent);

View File

@ -1,19 +1,11 @@
import { Frame } from "puppeteer";
import { TestExplorerParams } from "../testExplorer/TestExplorerParams";
import { getTestExplorerFrame } from "../testExplorer/TestExplorerUtils";
import { SelfServeType } from "../../src/SelfServe/SelfServeUtils";
import { ApiKind } from "../../src/Contracts/DataModels";
jest.setTimeout(300000); jest.setTimeout(300000);
let frame: Frame;
describe("Self Serve", () => { describe("Self Serve", () => {
it("Launch Self Serve Example", async () => { it("Launch Self Serve Example", async () => {
try { try {
frame = await getTestExplorerFrame( await page.goto("https://localhost:1234/testExplorer.html?iframeSrc=selfServe.html");
ApiKind.SQL, const handle = await page.waitForSelector("iframe");
new Map<string, string>([[TestExplorerParams.selfServeType, SelfServeType.example]]) const frame = await handle.contentFrame();
);
// wait for refresh RP call to end // wait for refresh RP call to end
await frame.waitFor(10000); await frame.waitFor(10000);

View File

@ -1,82 +1,50 @@
/* eslint-disable no-console */
import { ClientSecretCredential } from "@azure/identity";
import "../../less/hostedexplorer.less"; import "../../less/hostedexplorer.less";
import { TestExplorerParams } from "./TestExplorerParams"; import { DataExplorerInputsFrame } from "../../src/Contracts/ViewModels";
import { CosmosDBManagementClient } from "@azure/arm-cosmosdb"; import { updateUserContext } from "../../src/UserContext";
import * as msRest from "@azure/ms-rest-js"; import { get, listKeys } from "../../src/Utils/arm/generatedClients/2020-04-01/databaseAccounts";
import * as ViewModels from "../../src/Contracts/ViewModels";
import { Capability, DatabaseAccount } from "../../src/Contracts/DataModels";
class CustomSigner implements msRest.ServiceClientCredentials { const resourceGroup = process.env.RESOURCE_GROUP || "";
private token: string; const subscriptionId = process.env.SUBSCRIPTION_ID || "";
constructor(token: string) { const urlSearchParams = new URLSearchParams(window.location.search);
this.token = token; const accountName = urlSearchParams.get("accountName") || "portal-sql-runner";
} const selfServeType = urlSearchParams.get("selfServeType") || "example";
const iframeSrc = urlSearchParams.get("iframeSrc") || "explorer.html?platform=Portal&disablePortalInitCache";
async signRequest(webResource: msRest.WebResourceLike): Promise<msRest.WebResourceLike> { if (!process.env.AZURE_CLIENT_SECRET) {
webResource.headers.set("authorization", `bearer ${this.token}`); throw new Error(
return webResource; "process.env.AZURE_CLIENT_SECRET was not set! Set it in your .env file and restart webpack dev server"
} );
} }
const getDatabaseAccount = async ( // Azure SDK clients accept the credential as a parameter
token: string, const credentials = new ClientSecretCredential(
notebooksAccountSubscriptonId: string, process.env.AZURE_TENANT_ID,
notebooksAccountResourceGroup: string, process.env.AZURE_CLIENT_ID,
notebooksAccountName: string process.env.AZURE_CLIENT_SECRET,
): Promise<DatabaseAccount> => { {
const client = new CosmosDBManagementClient(new CustomSigner(token), notebooksAccountSubscriptonId); authorityHost: "https://localhost:1234",
const databaseAccountGetResponse = await client.databaseAccounts.get( }
notebooksAccountResourceGroup, );
notebooksAccountName
);
const databaseAccount: DatabaseAccount = { console.log("Resource Group:", resourceGroup);
id: databaseAccountGetResponse.id, console.log("Subcription: ", subscriptionId);
name: databaseAccountGetResponse.name, console.log("Account Name: ", accountName);
location: databaseAccountGetResponse.location,
type: databaseAccountGetResponse.type,
kind: databaseAccountGetResponse.kind,
tags: databaseAccountGetResponse.tags,
properties: {
documentEndpoint: databaseAccountGetResponse.documentEndpoint,
tableEndpoint: undefined,
gremlinEndpoint: undefined,
cassandraEndpoint: undefined,
capabilities: databaseAccountGetResponse.capabilities.map((capability) => {
return { name: capability.name } as Capability;
}),
},
};
return databaseAccount;
};
const initTestExplorer = async (): Promise<void> => { const initTestExplorer = async (): Promise<void> => {
const urlSearchParams = new URLSearchParams(window.location.search); const { token } = await credentials.getToken("https://management.core.windows.net/.default");
const portalRunnerDatabaseAccount = decodeURIComponent( updateUserContext({
urlSearchParams.get(TestExplorerParams.portalRunnerDatabaseAccount) authorizationToken: `bearer ${token}`,
); });
const portalRunnerDatabaseAccountKey = decodeURIComponent( const databaseAccount = await get(subscriptionId, resourceGroup, accountName);
urlSearchParams.get(TestExplorerParams.portalRunnerDatabaseAccountKey) const keys = await listKeys(subscriptionId, resourceGroup, accountName);
);
const portalRunnerSubscripton = decodeURIComponent(urlSearchParams.get(TestExplorerParams.portalRunnerSubscripton));
const portalRunnerResourceGroup = decodeURIComponent(
urlSearchParams.get(TestExplorerParams.portalRunnerResourceGroup)
);
const selfServeType = urlSearchParams.get(TestExplorerParams.selfServeType);
const token = decodeURIComponent(urlSearchParams.get(TestExplorerParams.token));
const databaseAccount = await getDatabaseAccount(
token,
portalRunnerSubscripton,
portalRunnerResourceGroup,
portalRunnerDatabaseAccount
);
const initTestExplorerContent = { const initTestExplorerContent = {
inputs: { inputs: {
databaseAccount: databaseAccount, databaseAccount: databaseAccount,
subscriptionId: portalRunnerSubscripton, subscriptionId,
resourceGroup: portalRunnerResourceGroup, resourceGroup,
authorizationToken: `Bearer ${token}`, authorizationToken: `Bearer ${token}`,
features: {}, features: {},
hasWriteAccess: true, hasWriteAccess: true,
@ -88,7 +56,7 @@ const initTestExplorer = async (): Promise<void> => {
quotaId: "Internal_2014-09-01", quotaId: "Internal_2014-09-01",
addCollectionDefaultFlight: "2", addCollectionDefaultFlight: "2",
isTryCosmosDBSubscription: false, isTryCosmosDBSubscription: false,
masterKey: portalRunnerDatabaseAccountKey, masterKey: keys.primaryMasterKey,
loadDatabaseAccountTimestamp: 1604663109836, loadDatabaseAccountTimestamp: 1604663109836,
dataExplorerVersion: "1.0.1", dataExplorerVersion: "1.0.1",
sharedThroughputMinimum: 400, sharedThroughputMinimum: 400,
@ -101,7 +69,7 @@ const initTestExplorer = async (): Promise<void> => {
// add UI test only when feature is not dependent on flights anymore // add UI test only when feature is not dependent on flights anymore
flights: [], flights: [],
selfServeType, selfServeType,
} as ViewModels.DataExplorerInputsFrame, } as DataExplorerInputsFrame,
}; };
const iframe = document.createElement("iframe"); const iframe = document.createElement("iframe");
@ -127,16 +95,8 @@ const initTestExplorer = async (): Promise<void> => {
iframe.name = "explorer"; iframe.name = "explorer";
iframe.classList.add("iframe"); iframe.classList.add("iframe");
iframe.title = "explorer"; iframe.title = "explorer";
iframe.src = getIframeSrc(selfServeType); iframe.src = iframeSrc;
document.body.appendChild(iframe); document.body.appendChild(iframe);
}; };
const getIframeSrc = (selfServeType: string): string => {
let iframeSrc = "explorer.html?platform=Portal&disablePortalInitCache";
if (selfServeType) {
iframeSrc = `selfServe.html?selfServeType=${selfServeType}`;
}
return iframeSrc;
};
initTestExplorer(); initTestExplorer();

View File

@ -1,8 +0,0 @@
export enum TestExplorerParams {
portalRunnerDatabaseAccount = "portalRunnerDatabaseAccount",
portalRunnerDatabaseAccountKey = "portalRunnerDatabaseAccountKey",
portalRunnerSubscripton = "portalRunnerSubscripton",
portalRunnerResourceGroup = "portalRunnerResourceGroup",
selfServeType = "selfServeType",
token = "token",
}

View File

@ -1,64 +0,0 @@
import { Frame } from "puppeteer";
import { TestExplorerParams } from "./TestExplorerParams";
import { ClientSecretCredential } from "@azure/identity";
import { ApiKind } from "../../src/Contracts/DataModels";
let testExplorerFrame: Frame;
export const getTestExplorerFrame = async (apiKind?: ApiKind, params?: Map<string, string>): Promise<Frame> => {
if (testExplorerFrame) {
return testExplorerFrame;
}
let portalRunnerDatabaseAccount: string;
let portalRunnerDatabaseAccountKey: string;
switch (apiKind) {
case ApiKind.MongoDB:
portalRunnerDatabaseAccount = process.env.PORTAL_RUNNER_MONGO_DATABASE_ACCOUNT;
portalRunnerDatabaseAccountKey = process.env.PORTAL_RUNNER_MONGO_DATABASE_ACCOUNT_KEY;
break;
default:
portalRunnerDatabaseAccount = process.env.PORTAL_RUNNER_DATABASE_ACCOUNT;
portalRunnerDatabaseAccountKey = process.env.PORTAL_RUNNER_DATABASE_ACCOUNT_KEY;
}
const notebooksTestRunnerTenantId = process.env.NOTEBOOKS_TEST_RUNNER_TENANT_ID;
const notebooksTestRunnerClientId = process.env.NOTEBOOKS_TEST_RUNNER_CLIENT_ID;
const notebooksTestRunnerClientSecret = process.env.NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET;
const portalRunnerSubscripton = process.env.PORTAL_RUNNER_SUBSCRIPTION;
const portalRunnerResourceGroup = process.env.PORTAL_RUNNER_RESOURCE_GROUP;
const credentials = new ClientSecretCredential(
notebooksTestRunnerTenantId,
notebooksTestRunnerClientId,
notebooksTestRunnerClientSecret
);
const { token } = await credentials.getToken("https://management.core.windows.net/.default");
const testExplorerUrl = new URL("testExplorer.html", "https://localhost:1234");
testExplorerUrl.searchParams.append(
TestExplorerParams.portalRunnerDatabaseAccount,
encodeURI(portalRunnerDatabaseAccount)
);
testExplorerUrl.searchParams.append(
TestExplorerParams.portalRunnerDatabaseAccountKey,
encodeURI(portalRunnerDatabaseAccountKey)
);
testExplorerUrl.searchParams.append(TestExplorerParams.portalRunnerSubscripton, encodeURI(portalRunnerSubscripton));
testExplorerUrl.searchParams.append(
TestExplorerParams.portalRunnerResourceGroup,
encodeURI(portalRunnerResourceGroup)
);
testExplorerUrl.searchParams.append(TestExplorerParams.token, encodeURI(token));
if (params) {
for (const key of params.keys()) {
testExplorerUrl.searchParams.append(key, encodeURI(params.get(key)));
}
}
await page.goto(testExplorerUrl.toString());
const handle = await page.waitForSelector("iframe");
return await handle.contentFrame();
};

View File

@ -30,8 +30,8 @@ export function generateDatabaseName(baseName = "db", length = 1): string {
return `${baseName}${crypto.randomBytes(length).toString("hex")}-${Date.now()}`; return `${baseName}${crypto.randomBytes(length).toString("hex")}-${Date.now()}`;
} }
export async function createDatabase(frame: Frame): Promise<string> { export async function createDatabase(frame: Frame): Promise<{ databaseId: string; collectionId: string }> {
const dbId = generateDatabaseName(); const databaseId = generateDatabaseName();
const collectionId = generateUniqueName("col"); const collectionId = generateUniqueName("col");
const shardKey = "partitionKey"; const shardKey = "partitionKey";
// create new collection // create new collection
@ -50,7 +50,7 @@ export async function createDatabase(frame: Frame): Promise<string> {
await frame.waitFor('input[data-test="addCollection-newDatabaseId"]'); await frame.waitFor('input[data-test="addCollection-newDatabaseId"]');
const dbInput = await frame.$('input[data-test="addCollection-newDatabaseId"]'); const dbInput = await frame.$('input[data-test="addCollection-newDatabaseId"]');
await dbInput.press("Backspace"); await dbInput.press("Backspace");
await dbInput.type(dbId); await dbInput.type(databaseId);
// type collection id // type collection id
await frame.waitFor('input[data-test="addCollection-collectionId"]'); await frame.waitFor('input[data-test="addCollection-collectionId"]');
@ -67,7 +67,7 @@ export async function createDatabase(frame: Frame): Promise<string> {
// click submit // click submit
await frame.waitFor("#submitBtnAddCollection"); await frame.waitFor("#submitBtnAddCollection");
await frame.click("#submitBtnAddCollection"); await frame.click("#submitBtnAddCollection");
return dbId; return { databaseId, collectionId };
} }
export async function onClickSaveButton(frame: Frame): Promise<void> { export async function onClickSaveButton(frame: Frame): Promise<void> {

View File

@ -1,7 +1,6 @@
const msRestNodeAuth = require("@azure/ms-rest-nodeauth"); const msRestNodeAuth = require("@azure/ms-rest-nodeauth");
const { CosmosDBManagementClient } = require("@azure/arm-cosmosdb"); const { CosmosDBManagementClient } = require("@azure/arm-cosmosdb");
const ms = require("ms"); const ms = require("ms");
const { time } = require("console");
const clientId = process.env["NOTEBOOKS_TEST_RUNNER_CLIENT_ID"]; const clientId = process.env["NOTEBOOKS_TEST_RUNNER_CLIENT_ID"];
const secret = process.env["NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET"]; const secret = process.env["NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET"];
@ -9,7 +8,7 @@ const tenantId = "72f988bf-86f1-41af-91ab-2d7cd011db47";
const subscriptionId = "69e02f2d-f059-4409-9eac-97e8a276ae2c"; const subscriptionId = "69e02f2d-f059-4409-9eac-97e8a276ae2c";
const resourceGroupName = "runners"; const resourceGroupName = "runners";
const sixtyMinutesAgo = new Date(Date.now() - 1000 * 60 * 60).getTime(); const thirtyMinutesAgo = new Date(Date.now() - 1000 * 60 * 30).getTime();
function friendlyTime(date) { function friendlyTime(date) {
try { try {
@ -29,7 +28,7 @@ async function main() {
const mongoDatabases = await client.mongoDBResources.listMongoDBDatabases(resourceGroupName, account.name); const mongoDatabases = await client.mongoDBResources.listMongoDBDatabases(resourceGroupName, account.name);
for (const database of mongoDatabases) { for (const database of mongoDatabases) {
const timestamp = Number(database.name.split("-")[1]); const timestamp = Number(database.name.split("-")[1]);
if (timestamp && timestamp < sixtyMinutesAgo) { if (timestamp && timestamp < thirtyMinutesAgo) {
await client.mongoDBResources.deleteMongoDBDatabase(resourceGroupName, account.name, database.name); await client.mongoDBResources.deleteMongoDBDatabase(resourceGroupName, account.name, database.name);
console.log(`DELETED: ${account.name} | ${database.name} | Age: ${friendlyTime(Date.now() - timestamp)}`); console.log(`DELETED: ${account.name} | ${database.name} | Age: ${friendlyTime(Date.now() - timestamp)}`);
} else { } else {
@ -40,7 +39,7 @@ async function main() {
const sqlDatabases = await client.sqlResources.listSqlDatabases(resourceGroupName, account.name); const sqlDatabases = await client.sqlResources.listSqlDatabases(resourceGroupName, account.name);
for (const database of sqlDatabases) { for (const database of sqlDatabases) {
const timestamp = Number(database.name.split("-")[1]); const timestamp = Number(database.name.split("-")[1]);
if (timestamp && timestamp < sixtyMinutesAgo) { if (timestamp && timestamp < thirtyMinutesAgo) {
await client.sqlResources.deleteSqlDatabase(resourceGroupName, account.name, database.name); await client.sqlResources.deleteSqlDatabase(resourceGroupName, account.name, database.name);
console.log(`DELETED: ${account.name} | ${database.name} | Age: ${friendlyTime(Date.now() - timestamp)}`); console.log(`DELETED: ${account.name} | ${database.name} | Age: ${friendlyTime(Date.now() - timestamp)}`);
} else { } else {

View File

@ -1,3 +1,4 @@
require("dotenv/config");
const path = require("path"); const path = require("path");
const MonacoWebpackPlugin = require("monaco-editor-webpack-plugin"); const MonacoWebpackPlugin = require("monaco-editor-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin"); const HtmlWebpackPlugin = require("html-webpack-plugin");
@ -14,6 +15,16 @@ const isCI = require("is-ci");
const gitSha = childProcess.execSync("git rev-parse HEAD").toString("utf8"); const gitSha = childProcess.execSync("git rev-parse HEAD").toString("utf8");
const AZURE_CLIENT_ID = "fd8753b0-0707-4e32-84e9-2532af865fb4";
const AZURE_TENANT_ID = "72f988bf-86f1-41af-91ab-2d7cd011db47";
const SUBSCRIPTION_ID = "69e02f2d-f059-4409-9eac-97e8a276ae2c";
const RESOURCE_GROUP = "runners";
const AZURE_CLIENT_SECRET = process.env.AZURE_CLIENT_SECRET || process.env.NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET; // TODO Remove. Exists for backwards compat with old .env files. Prefer AZURE_CLIENT_SECRET
if (!AZURE_CLIENT_SECRET) {
console.warn("AZURE_CLIENT_SECRET is not set. testExplorer.html will not work.");
}
const cssRule = { const cssRule = {
test: /\.css$/, test: /\.css$/,
use: [MiniCssExtractPlugin.loader, "css-loader"], use: [MiniCssExtractPlugin.loader, "css-loader"],
@ -102,6 +113,11 @@ module.exports = function (env = {}, argv = {}) {
if (mode === "development") { if (mode === "development") {
envVars.NODE_ENV = "development"; envVars.NODE_ENV = "development";
envVars.AZURE_CLIENT_ID = AZURE_CLIENT_ID;
envVars.AZURE_TENANT_ID = AZURE_TENANT_ID;
envVars.AZURE_CLIENT_SECRET = AZURE_CLIENT_SECRET;
envVars.SUBSCRIPTION_ID = SUBSCRIPTION_ID;
envVars.RESOURCE_GROUP = RESOURCE_GROUP;
typescriptRule.use[0].options.compilerOptions = { target: "ES2018" }; typescriptRule.use[0].options.compilerOptions = { target: "ES2018" };
} }
@ -282,6 +298,12 @@ module.exports = function (env = {}, argv = {}) {
secure: false, secure: false,
logLevel: "debug", logLevel: "debug",
}, },
[`/${AZURE_TENANT_ID}`]: {
target: "https://login.microsoftonline.com/",
changeOrigin: true,
secure: false,
logLevel: "debug",
},
}, },
}, },
stats: "minimal", stats: "minimal",