mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-23 02:41:39 +00:00
Compare commits
27 Commits
e2e-runner
...
PROD-2020-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ec07ff05a4 | ||
|
|
7512b3c1d5 | ||
|
|
dd199e6565 | ||
|
|
8200cc521f | ||
|
|
1d3b672a14 | ||
|
|
e5fc6f2022 | ||
|
|
3bf42b23dd | ||
|
|
d22cb598a9 | ||
|
|
269ea6a349 | ||
|
|
123902e7ee | ||
|
|
bccebaade5 | ||
|
|
9fedf63a77 | ||
|
|
4abfcc5e25 | ||
|
|
3d9256abc6 | ||
|
|
7f1355b1a4 | ||
|
|
3eff440680 | ||
|
|
4da0887e5e | ||
|
|
b783445130 | ||
|
|
73f2c612ed | ||
|
|
d70e30c4fc | ||
|
|
f8f1df4183 | ||
|
|
d427fc729e | ||
|
|
9c36782661 | ||
|
|
d32bd8851e | ||
|
|
23b2d8100f | ||
|
|
1662d20e8a | ||
|
|
582ac865ff |
6
.env.example
Normal file
6
.env.example
Normal file
@@ -0,0 +1,6 @@
|
||||
# These options are only needed when if running end to end tests locally
|
||||
PORTAL_RUNNER_USERNAME=
|
||||
PORTAL_RUNNER_PASSWORD=
|
||||
PORTAL_RUNNER_SUBSCRIPTION=
|
||||
PORTAL_RUNNER_RESOURCE_GROUP=
|
||||
PORTAL_RUNNER_DATABASE_ACCOUNT=
|
||||
@@ -1,4 +1,5 @@
|
||||
**/node_modules/
|
||||
dist/
|
||||
src/Api/Apis.ts
|
||||
src/AuthType.ts
|
||||
src/Bindings/BindingHandlersRegisterer.ts
|
||||
@@ -25,7 +26,6 @@ src/Common/Logger.test.ts
|
||||
src/Common/MessageHandler.test.ts
|
||||
src/Common/MessageHandler.ts
|
||||
src/Common/MongoProxyClient.test.ts
|
||||
src/Common/MongoProxyClient.ts
|
||||
src/Common/MongoUtility.ts
|
||||
src/Common/NotificationsClientBase.ts
|
||||
src/Common/ObjectCache.test.ts
|
||||
@@ -202,7 +202,6 @@ src/Explorer/Tabs/GraphTab.ts
|
||||
src/Explorer/Tabs/MongoDocumentsTab.ts
|
||||
src/Explorer/Tabs/MongoQueryTab.ts
|
||||
src/Explorer/Tabs/MongoShellTab.ts
|
||||
src/Explorer/Tabs/NotebookTab.ts
|
||||
src/Explorer/Tabs/NotebookV2Tab.ts
|
||||
src/Explorer/Tabs/QueryTab.test.ts
|
||||
src/Explorer/Tabs/QueryTab.ts
|
||||
@@ -216,7 +215,6 @@ src/Explorer/Tabs/TabComponents.ts
|
||||
src/Explorer/Tabs/TabsBase.ts
|
||||
src/Explorer/Tabs/TriggerTab.ts
|
||||
src/Explorer/Tabs/UserDefinedFunctionTab.ts
|
||||
src/Explorer/Tabs/__mocks__/NotebookTab.ts
|
||||
src/Explorer/Tree/AccessibleVerticalList.ts
|
||||
src/Explorer/Tree/Collection.test.ts
|
||||
src/Explorer/Tree/Collection.ts
|
||||
|
||||
13
.eslintrc.js
13
.eslintrc.js
@@ -3,12 +3,8 @@ module.exports = {
|
||||
browser: true,
|
||||
es6: true
|
||||
},
|
||||
plugins: ["@typescript-eslint"],
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
plugins: ["@typescript-eslint", "no-null"],
|
||||
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
|
||||
globals: {
|
||||
Atomics: "readonly",
|
||||
SharedArrayBuffer: "readonly"
|
||||
@@ -40,6 +36,9 @@ module.exports = {
|
||||
}
|
||||
],
|
||||
rules: {
|
||||
curly: "error"
|
||||
curly: "error",
|
||||
"@typescript-eslint/no-unused-vars": "error",
|
||||
"@typescript-eslint/no-extraneous-class": "error",
|
||||
"no-null/no-null": "error"
|
||||
}
|
||||
};
|
||||
|
||||
242
.github/workflows/blank.yml
vendored
242
.github/workflows/blank.yml
vendored
@@ -1,242 +0,0 @@
|
||||
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
|
||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
|
||||
|
||||
name: CI
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
name: "Unit Tests"
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js 12.x
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12.x
|
||||
- run: npm ci
|
||||
- run: npm run test
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
name: "Build"
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js 12.x
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12.x
|
||||
- run: npm ci
|
||||
- run: npm run build:contracts
|
||||
- name: Restore Build Cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: .cache
|
||||
key: ${{ runner.os }}-build-cache
|
||||
- run: npm run pack:prod
|
||||
- run: npm run copyToConsumers
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: dist
|
||||
path: dist/
|
||||
endtoend:
|
||||
name: "End to End Tests"
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: southpolesteve/cosmos-emulator-github-action@v1
|
||||
- name: Use Node.js 12.x
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12.x
|
||||
- name: Restore Cypress Binary Cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.cache/Cypress
|
||||
key: ${{ runner.os }}-cypress-binary-cache
|
||||
- name: End to End Tests
|
||||
run: |
|
||||
npm ci
|
||||
npm start &
|
||||
npm ci --prefix ./cypress
|
||||
npm run test:ci --prefix ./cypress -- --spec ./integration/dataexplorer/ci-tests/createDatabase.spec.ts
|
||||
shell: bash
|
||||
env:
|
||||
EMULATOR_ENDPOINT: https://0.0.0.0:8081/
|
||||
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
||||
CYPRESS_CACHE_FOLDER: ~/.cache/Cypress
|
||||
endtoendprodsql:
|
||||
name: "End to End Tests Prod"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js 12.x
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12.x
|
||||
- name: Restore Cypress Binary Cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.cache/Cypress
|
||||
key: ${{ runner.os }}-cypress-binary-cache
|
||||
- name: End to End Tests
|
||||
run: |
|
||||
npm ci
|
||||
npm start &
|
||||
npm ci --prefix ./cypress
|
||||
npm run test --prefix ./cypress
|
||||
shell: bash
|
||||
env:
|
||||
EMULATOR_ENDPOINT: https://0.0.0.0:8081/
|
||||
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
||||
CYPRESS_CACHE_FOLDER: ~/.cache/Cypress
|
||||
CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_SQL }}
|
||||
endtoendprodmongo:
|
||||
name: "End to End Tests Prod"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js 12.x
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12.x
|
||||
- name: Restore Cypress Binary Cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.cache/Cypress
|
||||
key: ${{ runner.os }}-cypress-binary-cache
|
||||
- name: End to End Tests
|
||||
run: |
|
||||
npm ci
|
||||
npm start &
|
||||
npm ci --prefix ./cypress
|
||||
npm run test --prefix ./cypress
|
||||
shell: bash
|
||||
env:
|
||||
EMULATOR_ENDPOINT: https://0.0.0.0:8081/
|
||||
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
||||
CYPRESS_CACHE_FOLDER: ~/.cache/Cypress
|
||||
CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_MONGO }}
|
||||
endtoendprodtable:
|
||||
name: "End to End Tests Prod"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js 12.x
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12.x
|
||||
- name: Restore Cypress Binary Cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.cache/Cypress
|
||||
key: ${{ runner.os }}-cypress-binary-cache
|
||||
- name: End to End Tests
|
||||
run: |
|
||||
npm ci
|
||||
npm start &
|
||||
npm ci --prefix ./cypress
|
||||
npm run test --prefix ./cypress
|
||||
shell: bash
|
||||
env:
|
||||
EMULATOR_ENDPOINT: https://0.0.0.0:8081/
|
||||
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
||||
CYPRESS_CACHE_FOLDER: ~/.cache/Cypress
|
||||
CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_TABLE }}
|
||||
endtoendprodgraph:
|
||||
name: "End to End Tests Prod"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js 12.x
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12.x
|
||||
- name: Restore Cypress Binary Cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.cache/Cypress
|
||||
key: ${{ runner.os }}-cypress-binary-cache
|
||||
- name: End to End Tests
|
||||
run: |
|
||||
npm ci
|
||||
npm start &
|
||||
npm ci --prefix ./cypress
|
||||
npm run test --prefix ./cypress
|
||||
shell: bash
|
||||
env:
|
||||
EMULATOR_ENDPOINT: https://0.0.0.0:8081/
|
||||
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
||||
CYPRESS_CACHE_FOLDER: ~/.cache/Cypress
|
||||
CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_GRAPH }}
|
||||
endtoendprodcassandra:
|
||||
name: "End to End Tests Prod"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js 12.x
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12.x
|
||||
- name: Restore Cypress Binary Cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.cache/Cypress
|
||||
key: ${{ runner.os }}-cypress-binary-cache
|
||||
- name: End to End Tests
|
||||
run: |
|
||||
npm ci
|
||||
npm start &
|
||||
npm ci --prefix ./cypress
|
||||
npm run test --prefix ./cypress
|
||||
shell: bash
|
||||
env:
|
||||
EMULATOR_ENDPOINT: https://0.0.0.0:8081/
|
||||
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
||||
CYPRESS_CACHE_FOLDER: ~/.cache/Cypress
|
||||
CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_CASSANDRA }}
|
||||
nuget:
|
||||
name: Publish Nuget
|
||||
needs: [build, test, endtoend]
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
|
||||
AZURE_DEVOPS_PAT: ${{ secrets.AZURE_DEVOPS_PAT }}
|
||||
steps:
|
||||
- uses: nuget/setup-nuget@v1
|
||||
with:
|
||||
nuget-api-key: ${{ secrets.NUGET_API_KEY }}
|
||||
- name: Download Dist Folder
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: dist
|
||||
- run: nuget sources add -Name "ADO" -Source "$NUGET_SOURCE" -UserName "GitHub" -Password "$AZURE_DEVOPS_PAT"
|
||||
- run: nuget pack -Version "2.0.0-github-${GITHUB_SHA}"
|
||||
- run: nuget push -Source "$NUGET_SOURCE" -ApiKey Az *.nupkg
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
path: "*.nupkg"
|
||||
nugetmpac:
|
||||
name: Publish Nuget MPAC
|
||||
needs: [build, test, endtoend]
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
|
||||
AZURE_DEVOPS_PAT: ${{ secrets.AZURE_DEVOPS_PAT }}
|
||||
steps:
|
||||
- uses: nuget/setup-nuget@v1
|
||||
with:
|
||||
nuget-api-key: ${{ secrets.NUGET_API_KEY }}
|
||||
- name: Download Dist Folder
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: dist
|
||||
- run: sed -i 's/Azure.Cosmos.DB.Data.Explorer/Azure.Cosmos.DB.Data.Explorer.MPAC/g' DataExplorer.nuspec
|
||||
- run: nuget sources add -Name "ADO" -Source "$NUGET_SOURCE" -UserName "GitHub" -Password "$AZURE_DEVOPS_PAT"
|
||||
- run: nuget pack -Version "2.0.0-github-${GITHUB_SHA}"
|
||||
- run: nuget push -Source "$NUGET_SOURCE" -ApiKey Az *.nupkg
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
path: "*.nupkg"
|
||||
202
.github/workflows/ci.yml
vendored
Normal file
202
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,202 @@
|
||||
name: CI
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
jobs:
|
||||
compile:
|
||||
runs-on: ubuntu-latest
|
||||
name: "Compile TypeScript"
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js 12.x
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12.x
|
||||
- run: npm ci
|
||||
- run: npm run compile
|
||||
- run: npm run compile:strict
|
||||
format:
|
||||
runs-on: ubuntu-latest
|
||||
name: "Check Format"
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js 12.x
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12.x
|
||||
- run: npm ci
|
||||
- run: npm run format:check
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
name: "Lint"
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js 12.x
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12.x
|
||||
- run: npm ci
|
||||
- run: npm run lint
|
||||
unittest:
|
||||
runs-on: ubuntu-latest
|
||||
name: "Unit Tests"
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js 12.x
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12.x
|
||||
- run: npm ci
|
||||
- run: npm run test
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
name: "Build"
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js 12.x
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12.x
|
||||
- run: npm ci
|
||||
- run: npm run build:contracts
|
||||
- name: Restore Build Cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: .cache
|
||||
key: ${{ runner.os }}-build-cache
|
||||
- run: npm run pack:prod
|
||||
- run: cp -r ./Contracts ./dist/contracts
|
||||
- run: cp -r ./configs ./dist/configs
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: dist
|
||||
path: dist/
|
||||
endtoendemulator:
|
||||
name: "End To End Tests | Emulator | SQL"
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: southpolesteve/cosmos-emulator-github-action@v1
|
||||
- name: Use Node.js 12.x
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12.x
|
||||
- name: Restore Cypress Binary Cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.cache/Cypress
|
||||
key: ${{ runner.os }}-cypress-binary-cache
|
||||
- name: End to End Tests
|
||||
run: |
|
||||
npm ci
|
||||
npm start &
|
||||
npm ci --prefix ./cypress
|
||||
npm run test:ci --prefix ./cypress -- --spec ./integration/dataexplorer/ci-tests/createDatabase.spec.ts
|
||||
shell: bash
|
||||
env:
|
||||
EMULATOR_ENDPOINT: https://0.0.0.0:8081/
|
||||
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
||||
CYPRESS_CACHE_FOLDER: ~/.cache/Cypress
|
||||
endtoendsql:
|
||||
name: "End To End Tests | SQL"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js 12.x
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12.x
|
||||
- name: Restore Cypress Binary Cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.cache/Cypress
|
||||
key: ${{ runner.os }}-cypress-binary-cache
|
||||
- run: npm ci
|
||||
- name: End to End Tests
|
||||
run: |
|
||||
npm start &
|
||||
cd cypress
|
||||
npm ci
|
||||
node cleanup.js
|
||||
npm run wait-for-server
|
||||
npx cypress run --browser chrome --headless --spec "./integration/dataexplorer/SQL/*"
|
||||
shell: bash
|
||||
env:
|
||||
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
||||
CYPRESS_CACHE_FOLDER: ~/.cache/Cypress
|
||||
CYPRESS_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_SQL }}
|
||||
endtoendmongo:
|
||||
name: "End To End Tests | Mongo"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js 12.x
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12.x
|
||||
- name: Restore Cypress Binary Cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.cache/Cypress
|
||||
key: ${{ runner.os }}-cypress-binary-cache
|
||||
- name: End to End Tests
|
||||
run: |
|
||||
npm ci
|
||||
npm start &
|
||||
cd cypress
|
||||
npm ci
|
||||
node cleanup.js
|
||||
npm run wait-for-server
|
||||
npx cypress run --browser chrome --headless --spec "./integration/dataexplorer/MONGO/*"
|
||||
shell: bash
|
||||
env:
|
||||
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
||||
CYPRESS_CACHE_FOLDER: ~/.cache/Cypress
|
||||
CYPRESS_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_MONGO }}
|
||||
nuget:
|
||||
name: Publish Nuget
|
||||
needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendsql, endtoendmongo]
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
|
||||
AZURE_DEVOPS_PAT: ${{ secrets.AZURE_DEVOPS_PAT }}
|
||||
steps:
|
||||
- uses: nuget/setup-nuget@v1
|
||||
with:
|
||||
nuget-api-key: ${{ secrets.NUGET_API_KEY }}
|
||||
- name: Download Dist Folder
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: dist
|
||||
- run: cp ./configs/prod.json config.json
|
||||
- run: nuget sources add -Name "ADO" -Source "$NUGET_SOURCE" -UserName "GitHub" -Password "$AZURE_DEVOPS_PAT"
|
||||
- run: nuget pack -Version "2.0.0-github-${GITHUB_SHA}"
|
||||
- run: nuget push -Source "$NUGET_SOURCE" -ApiKey Az *.nupkg
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
path: "*.nupkg"
|
||||
nugetmpac:
|
||||
name: Publish Nuget MPAC
|
||||
needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendsql, endtoendmongo]
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
|
||||
AZURE_DEVOPS_PAT: ${{ secrets.AZURE_DEVOPS_PAT }}
|
||||
steps:
|
||||
- uses: nuget/setup-nuget@v1
|
||||
with:
|
||||
nuget-api-key: ${{ secrets.NUGET_API_KEY }}
|
||||
- name: Download Dist Folder
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: dist
|
||||
- run: cp ./configs/mpac.json config.json
|
||||
- run: sed -i 's/Azure.Cosmos.DB.Data.Explorer/Azure.Cosmos.DB.Data.Explorer.MPAC/g' DataExplorer.nuspec
|
||||
- run: nuget sources add -Name "ADO" -Source "$NUGET_SOURCE" -UserName "GitHub" -Password "$AZURE_DEVOPS_PAT"
|
||||
- run: nuget pack -Version "2.0.0-github-${GITHUB_SHA}"
|
||||
- run: nuget push -Source "$NUGET_SOURCE" -ApiKey Az *.nupkg
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
path: "*.nupkg"
|
||||
20
.github/workflows/runners.yml
vendored
Normal file
20
.github/workflows/runners.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
name: Runners
|
||||
on:
|
||||
schedule:
|
||||
- cron: "*/10 * * * *"
|
||||
jobs:
|
||||
sqlcreatecollection:
|
||||
runs-on: ubuntu-latest
|
||||
name: "SQL | Create Collection"
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
- run: npm ci
|
||||
- run: npm run test:e2e
|
||||
env:
|
||||
PORTAL_RUNNER_APP_INSIGHTS_KEY: ${{ secrets.PORTAL_RUNNER_APP_INSIGHTS_KEY }}
|
||||
PORTAL_RUNNER_USERNAME: ${{ secrets.PORTAL_RUNNER_USERNAME }}
|
||||
PORTAL_RUNNER_PASSWORD: ${{ secrets.PORTAL_RUNNER_PASSWORD }}
|
||||
PORTAL_RUNNER_SUBSCRIPTION: 69e02f2d-f059-4409-9eac-97e8a276ae2c
|
||||
PORTAL_RUNNER_RESOURCE_GROUP: runners
|
||||
PORTAL_RUNNER_DATABASE_ACCOUNT: portal-sql-runner
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -16,3 +16,4 @@ notebookapp/*
|
||||
Contracts/*
|
||||
.DS_Store
|
||||
.cache/
|
||||
.env
|
||||
51
CONTRIBUTING.md
Normal file
51
CONTRIBUTING.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# Contribution guidelines to Data Explorer
|
||||
|
||||
This project welcomes contributions and suggestions. Most contributions require you to agree to a
|
||||
Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
|
||||
the rights to use your contribution. For details, visit https://cla.microsoft.com.
|
||||
|
||||
When you submit a pull request, a CLA-bot will automatically determine whether you need to provide
|
||||
a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions
|
||||
provided by the bot. You will only need to do this once across all repos using our CLA.
|
||||
|
||||
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
|
||||
For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
|
||||
contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
|
||||
|
||||
## Microsoft Open Source Code of Conduct
|
||||
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
|
||||
|
||||
Resources:
|
||||
|
||||
- [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/)
|
||||
- [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/)
|
||||
- Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns
|
||||
|
||||
## Browser support
|
||||
Please make sure to support all modern browsers as well as Internet Explorer 11.
|
||||
For IE support, polyfill is preferred over new usage of lodash or underscore. We already polyfill almost everything by importing babel-polyfill at the top of entry points.
|
||||
|
||||
|
||||
## Coding guidelines, conventions and recommendations
|
||||
### Typescript
|
||||
* Follow this [typescript style guide](https://github.com/excelmicro/typescript) which is based on [airbnb's style guide](https://github.com/airbnb/javascript).
|
||||
* Conventions speficic to this project:
|
||||
* Use double-quotes for string
|
||||
* Don't use null, use undefined
|
||||
* Pascal case for private static readonly fields
|
||||
* Camel case for classnames in markup
|
||||
* Don't use class unless necessary
|
||||
* Code related to notebooks should be dynamically imported so that it is loaded from a separate bundle only if the account is notebook-enabled. There are already top-level notebook components which are dynamically imported and their dependencies can be statically imported from these files.
|
||||
|
||||
### React
|
||||
* Prefer using React class components over function components and hooks unless you have a simple component and require no nested functions:
|
||||
* Nested functions may be harder to test independently
|
||||
* Switching from function component to class component later mayb be painful
|
||||
|
||||
## Testing
|
||||
Any PR should not decrease testing coverage.
|
||||
|
||||
## Recommended Tools and VS Code extensions
|
||||
* [Bookmarks](https://github.com/alefragnani/vscode-bookmarks)
|
||||
* [Bracket pair colorizer](https://github.com/CoenraadS/Bracket-Pair-Colorizer-2)
|
||||
* [GitHub Pull Requests and Issues](https://github.com/Microsoft/vscode-pull-request-github)
|
||||
21
README.md
21
README.md
@@ -70,7 +70,7 @@ Unit tests are located adjacent to the code under test and run with [Jest](https
|
||||
|
||||
`npm run test`
|
||||
|
||||
#### End to End Tests
|
||||
#### End to End CI Tests
|
||||
|
||||
[Cypress](https://www.cypress.io/) is used for end to end tests and are contained in `cypress/`. Currently, it operates as sub project with its own typescript config and dependencies. It also only operates against the emulator. To run cypress tests:
|
||||
|
||||
@@ -80,16 +80,13 @@ Unit tests are located adjacent to the code under test and run with [Jest](https
|
||||
4. Install dependencies: `npm install`
|
||||
5. Run cypress headless(`npm run test`) or in interactive mode(`npm run test:debug`)
|
||||
|
||||
#### End to End Production Runners
|
||||
|
||||
Jest and Puppeteer are used for end to end production runners and are contained in `test/`. To run these tests locally:
|
||||
|
||||
1. Copy .env.example to .env and fill in all variables
|
||||
2. Run `npm run test:e2e`
|
||||
|
||||
# Contributing
|
||||
|
||||
This project welcomes contributions and suggestions. Most contributions require you to agree to a
|
||||
Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
|
||||
the rights to use your contribution. For details, visit https://cla.microsoft.com.
|
||||
|
||||
When you submit a pull request, a CLA-bot will automatically determine whether you need to provide
|
||||
a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions
|
||||
provided by the bot. You will only need to do this once across all repos using our CLA.
|
||||
|
||||
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
|
||||
For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
|
||||
contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
|
||||
Please read the [contribution guidelines](./CONTRIBUTING.md).
|
||||
|
||||
3
configs/mpac.json
Normal file
3
configs/mpac.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"JUNO_ENDPOINT": "https://tools-staging.cosmos.azure.com"
|
||||
}
|
||||
3
configs/prod.json
Normal file
3
configs/prod.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"JUNO_ENDPOINT": "https://tools.cosmos.azure.com"
|
||||
}
|
||||
3
cypress/.gitignore
vendored
3
cypress/.gitignore
vendored
@@ -1 +1,4 @@
|
||||
cypress.env.json
|
||||
cypress/report
|
||||
cypress/screenshots
|
||||
cypress/videos
|
||||
51
cypress/cleanup.js
Normal file
51
cypress/cleanup.js
Normal file
@@ -0,0 +1,51 @@
|
||||
// Cleans up old databases from previous test runs
|
||||
const { CosmosClient } = require("@azure/cosmos");
|
||||
|
||||
// TODO: Add support for other API connection strings
|
||||
const mongoRegex = RegExp("mongodb://.*:(.*)@(.*).mongo.cosmos.azure.com");
|
||||
|
||||
async function cleanup() {
|
||||
const connectionString = process.env.CYPRESS_CONNECTION_STRING;
|
||||
if (!connectionString) {
|
||||
throw new Error("Connection string not provided");
|
||||
}
|
||||
|
||||
let client;
|
||||
switch (true) {
|
||||
case connectionString.includes("mongodb://"): {
|
||||
const [, key, accountName] = connectionString.match(mongoRegex);
|
||||
client = new CosmosClient({
|
||||
key,
|
||||
endpoint: `https://${accountName}.documents.azure.com:443/`
|
||||
});
|
||||
break;
|
||||
}
|
||||
// TODO: Add support for other API connection strings
|
||||
default:
|
||||
client = new CosmosClient(connectionString);
|
||||
break;
|
||||
}
|
||||
|
||||
const response = await client.databases.readAll().fetchAll();
|
||||
return Promise.all(
|
||||
response.resources.map(async db => {
|
||||
const dbTimestamp = new Date(db._ts * 1000);
|
||||
const twentyMinutesAgo = new Date(Date.now() - 1000 * 60 * 20);
|
||||
if (dbTimestamp < twentyMinutesAgo) {
|
||||
await client.database(db.id).delete();
|
||||
console.log(`DELETED: ${db.id} | Timestamp: ${dbTimestamp}`);
|
||||
} else {
|
||||
console.log(`SKIPPED: ${db.id} | Timestamp: ${dbTimestamp}`);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
cleanup()
|
||||
.then(() => {
|
||||
process.exit(0);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -16,7 +16,7 @@ let crypt = require("crypto");
|
||||
|
||||
context("Mongo API Test - createDatabase", () => {
|
||||
beforeEach(() => {
|
||||
connectionString.loginUsingConnectionString(connectionString.constants.mongo);
|
||||
connectionString.loginUsingConnectionString();
|
||||
});
|
||||
|
||||
it("Create a new collection in Mongo API", () => {
|
||||
@@ -63,7 +63,7 @@ context("Mongo API Test - createDatabase", () => {
|
||||
.type(sharedKey);
|
||||
|
||||
cy.wrap($body)
|
||||
.find('input[data-test="addCollection-createCollection"]')
|
||||
.find("#submitBtnAddCollection")
|
||||
.click();
|
||||
|
||||
cy.wait(10000);
|
||||
|
||||
@@ -16,10 +16,10 @@ let crypt = require("crypto");
|
||||
|
||||
context("Mongo API Test", () => {
|
||||
beforeEach(() => {
|
||||
connectionString.loginUsingConnectionString(connectionString.constants.mongo);
|
||||
connectionString.loginUsingConnectionString();
|
||||
});
|
||||
|
||||
it("Create a new collection in Mongo API - Autopilot", () => {
|
||||
it.skip("Create a new collection in Mongo API - Autopilot", () => {
|
||||
const dbId = `TestDatabase${crypt.randomBytes(8).toString("hex")}`;
|
||||
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
|
||||
const sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;
|
||||
|
||||
@@ -4,10 +4,10 @@ let crypt = require("crypto");
|
||||
|
||||
context("Mongo API Test", () => {
|
||||
beforeEach(() => {
|
||||
connectionString.loginUsingConnectionString(connectionString.constants.mongo);
|
||||
connectionString.loginUsingConnectionString();
|
||||
});
|
||||
|
||||
it("Create a new collection in existing database in Mongo API", () => {
|
||||
it.skip("Create a new collection in existing database in Mongo API", () => {
|
||||
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
|
||||
const sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@ const connectionString = require("../../../utilities/connectionString");
|
||||
|
||||
let crypt = require("crypto");
|
||||
|
||||
context("Mongo API Test", () => {
|
||||
context.skip("Mongo API Test", () => {
|
||||
beforeEach(() => {
|
||||
connectionString.loginUsingConnectionString(connectionString.constants.mongo);
|
||||
connectionString.loginUsingConnectionString();
|
||||
});
|
||||
|
||||
it("Create a new collection in Mongo API - Provision database throughput", () => {
|
||||
|
||||
@@ -16,13 +16,14 @@ let crypt = require("crypto");
|
||||
|
||||
context("SQL API Test", () => {
|
||||
beforeEach(() => {
|
||||
connectionString.loginUsingConnectionString(connectionString.constants.sql);
|
||||
connectionString.loginUsingConnectionString();
|
||||
});
|
||||
|
||||
it("Create a new container in SQL API", () => {
|
||||
const dbId = `TestDatabase${crypt.randomBytes(8).toString("hex")}`;
|
||||
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
|
||||
const sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;
|
||||
connectionString.loginUsingConnectionString();
|
||||
|
||||
cy.get("iframe").then($element => {
|
||||
const $body = $element.contents().find("body");
|
||||
@@ -63,7 +64,7 @@ context("SQL API Test", () => {
|
||||
.type(sharedKey);
|
||||
|
||||
cy.wrap($body)
|
||||
.find('input[data-test="addCollection-createCollection"]')
|
||||
.find("#submitBtnAddCollection")
|
||||
.click();
|
||||
|
||||
cy.wait(10000);
|
||||
|
||||
110
cypress/package-lock.json
generated
110
cypress/package-lock.json
generated
@@ -273,77 +273,12 @@
|
||||
"any-observable": "^0.3.0"
|
||||
}
|
||||
},
|
||||
"@types/blob-util": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/blob-util/-/blob-util-1.3.3.tgz",
|
||||
"integrity": "sha512-4ahcL/QDnpjWA2Qs16ZMQif7HjGP2cw3AGjHabybjw7Vm1EKu+cfQN1D78BaZbS1WJNa1opSMF5HNMztx7lR0w==",
|
||||
"@types/sinonjs__fake-timers": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.1.tgz",
|
||||
"integrity": "sha512-yYezQwGWty8ziyYLdZjwxyMb0CZR49h8JALHGrxjQHWlqGgc8kLdHEgWrgL0uZ29DMvEVBDnHU2Wg36zKSIUtA==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/bluebird": {
|
||||
"version": "3.5.29",
|
||||
"resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.29.tgz",
|
||||
"integrity": "sha512-kmVtnxTuUuhCET669irqQmPAez4KFnFVKvpleVRyfC3g+SHD1hIkFZcWLim9BVcwUBLO59o8VZE4yGCmTif8Yw==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/chai": {
|
||||
"version": "4.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.7.tgz",
|
||||
"integrity": "sha512-luq8meHGYwvky0O7u0eQZdA7B4Wd9owUCqvbw2m3XCrCU8mplYOujMBbvyS547AxJkC+pGnd0Cm15eNxEUNU8g==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/chai-jquery": {
|
||||
"version": "1.1.40",
|
||||
"resolved": "https://registry.npmjs.org/@types/chai-jquery/-/chai-jquery-1.1.40.tgz",
|
||||
"integrity": "sha512-mCNEZ3GKP7T7kftKeIs7QmfZZQM7hslGSpYzKbOlR2a2HCFf9ph4nlMRA9UnuOETeOQYJVhJQK7MwGqNZVyUtQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/chai": "*",
|
||||
"@types/jquery": "*"
|
||||
}
|
||||
},
|
||||
"@types/jquery": {
|
||||
"version": "3.3.31",
|
||||
"resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.3.31.tgz",
|
||||
"integrity": "sha512-Lz4BAJihoFw5nRzKvg4nawXPzutkv7wmfQ5121avptaSIXlDNJCUuxZxX/G+9EVidZGuO0UBlk+YjKbwRKJigg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/sizzle": "*"
|
||||
}
|
||||
},
|
||||
"@types/lodash": {
|
||||
"version": "4.14.149",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.149.tgz",
|
||||
"integrity": "sha512-ijGqzZt/b7BfzcK9vTrS6MFljQRPn5BFWOx8oE0GYxribu6uV+aA9zZuXI1zc/etK9E8nrgdoF2+LgUw7+9tJQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/minimatch": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz",
|
||||
"integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/mocha": {
|
||||
"version": "5.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.7.tgz",
|
||||
"integrity": "sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/sinon": {
|
||||
"version": "7.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-7.5.1.tgz",
|
||||
"integrity": "sha512-EZQUP3hSZQyTQRfiLqelC9NMWd1kqLcmQE0dMiklxBkgi84T+cHOhnKpgk4NnOWpGX863yE6+IaGnOXUNFqDnQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/sinon-chai": {
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/sinon-chai/-/sinon-chai-3.2.3.tgz",
|
||||
"integrity": "sha512-TOUFS6vqS0PVL1I8NGVSNcFaNJtFoyZPXZ5zur+qlhDfOmQECZZM4H4kKgca6O8L+QceX/ymODZASfUfn+y4yQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/chai": "*",
|
||||
"@types/sinon": "*"
|
||||
}
|
||||
},
|
||||
"@types/sizzle": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.2.tgz",
|
||||
@@ -827,24 +762,15 @@
|
||||
}
|
||||
},
|
||||
"cypress": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/cypress/-/cypress-4.5.0.tgz",
|
||||
"integrity": "sha512-2A4g5FW5d2fHzq8HKUGAMVTnW6P8nlWYQALiCoGN4bqBLvgwhYM/oG9oKc2CS6LnvgHFiKivKzpm9sfk3uU3zQ==",
|
||||
"version": "4.8.0",
|
||||
"resolved": "https://registry.npmjs.org/cypress/-/cypress-4.8.0.tgz",
|
||||
"integrity": "sha512-Lsff8lF8pq6k/ioNua783tCsxGSLp6gqGXecdIfqCkqjYiOA53XKuEf1CaQJLUVs1dHSf49eDUp/sb620oJjVQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@cypress/listr-verbose-renderer": "0.4.1",
|
||||
"@cypress/request": "2.88.5",
|
||||
"@cypress/xvfb": "1.2.4",
|
||||
"@types/blob-util": "1.3.3",
|
||||
"@types/bluebird": "3.5.29",
|
||||
"@types/chai": "4.2.7",
|
||||
"@types/chai-jquery": "1.1.40",
|
||||
"@types/jquery": "3.3.31",
|
||||
"@types/lodash": "4.14.149",
|
||||
"@types/minimatch": "3.0.3",
|
||||
"@types/mocha": "5.2.7",
|
||||
"@types/sinon": "7.5.1",
|
||||
"@types/sinon-chai": "3.2.3",
|
||||
"@types/sinonjs__fake-timers": "6.0.1",
|
||||
"@types/sizzle": "2.3.2",
|
||||
"arch": "2.1.1",
|
||||
"bluebird": "3.7.2",
|
||||
@@ -878,14 +804,6 @@
|
||||
"untildify": "4.0.0",
|
||||
"url": "0.11.0",
|
||||
"yauzl": "2.10.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"minimist": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"dashdash": {
|
||||
@@ -1081,12 +999,6 @@
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"minimist": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
|
||||
"dev": true
|
||||
},
|
||||
"mkdirp": {
|
||||
"version": "0.5.5",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
|
||||
@@ -1859,6 +1771,12 @@
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
},
|
||||
"minimist": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
|
||||
"dev": true
|
||||
},
|
||||
"mkdirp": {
|
||||
"version": "0.5.1",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
|
||||
|
||||
@@ -5,11 +5,13 @@
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "cypress run",
|
||||
"wait-for-server": "wait-on -t 240000 -i 5000 -v https-get://0.0.0.0:1234/",
|
||||
"test:sql": "cypress run --browser chrome --headless --spec \"./integration/dataexplorer/SQL/*\"",
|
||||
"test:ci": "wait-on -t 240000 -i 5000 -v https-get://0.0.0.0:1234/ https-get://0.0.0.0:8081/_explorer/index.html && cypress run --browser chrome --headless",
|
||||
"test:debug": "cypress open"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cypress": "^4.5.0",
|
||||
"cypress": "^4.8.0",
|
||||
"mocha": "^7.0.1",
|
||||
"mochawesome": "^4.1.0",
|
||||
"mochawesome-merge": "^4.0.1",
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
module.exports = {
|
||||
loginUsingConnectionString: function (api) {
|
||||
|
||||
|
||||
const prodUrl = "https://cosmos.azure.com/";
|
||||
loginUsingConnectionString: function() {
|
||||
const prodUrl = Cypress.env("TEST_ENDPOINT") || "https://localhost:1234/hostedExplorer.html";
|
||||
const timeout = 15000;
|
||||
|
||||
cy.visit(prodUrl);
|
||||
@@ -24,7 +22,7 @@ module.exports = {
|
||||
.last()
|
||||
.click({ force: true });
|
||||
|
||||
const secret = Cypress.env(`CONNECTION_STRING_${api && api.toUpperCase()}`);
|
||||
const secret = Cypress.env("CONNECTION_STRING");
|
||||
|
||||
cy.wrap($body)
|
||||
.find("input[class='inputToken']")
|
||||
@@ -38,19 +36,6 @@ module.exports = {
|
||||
.click({ force: true });
|
||||
|
||||
cy.wait(15000);
|
||||
|
||||
cy.wrap($body)
|
||||
.find(".connectExplorer > p:nth-child(3)")
|
||||
.should("be.visible");
|
||||
|
||||
});
|
||||
},
|
||||
constants:{
|
||||
sql: "sql",
|
||||
mongo: "mongo",
|
||||
table: "table",
|
||||
graph: "graph",
|
||||
cassandra: "cassandra"
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
9
jest-puppeteer.config.js
Normal file
9
jest-puppeteer.config.js
Normal file
@@ -0,0 +1,9 @@
|
||||
const isCI = require("is-ci");
|
||||
|
||||
module.exports = {
|
||||
launch: {
|
||||
headless: isCI,
|
||||
slowMo: isCI ? null : 20,
|
||||
defaultViewport: null
|
||||
}
|
||||
};
|
||||
5
jest.config.e2e.js
Normal file
5
jest.config.e2e.js
Normal file
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
preset: "jest-puppeteer",
|
||||
testMatch: ["<rootDir>/test/**/*.spec.[jt]s?(x)"],
|
||||
setupFiles: ["dotenv/config"]
|
||||
};
|
||||
@@ -150,7 +150,7 @@ module.exports = {
|
||||
// testRegex: "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?|ts?)$",
|
||||
|
||||
// This option allows the use of a custom results processor
|
||||
testResultsProcessor: "./trxProcessor.js",
|
||||
// testResultsProcessor: "./trxProcessor.js",
|
||||
|
||||
// This option allows use of a custom test runner
|
||||
// testRunner: "jasmine2",
|
||||
|
||||
@@ -54,6 +54,8 @@
|
||||
|
||||
@SelectionColor: #3074B0;
|
||||
|
||||
@FocusColor: #00bcf2;
|
||||
|
||||
/******************************************************************************
|
||||
METRICS
|
||||
/******************************************************************************/
|
||||
@@ -198,7 +200,7 @@
|
||||
}
|
||||
|
||||
.focus() {
|
||||
outline: 1px dashed @AccentMedium;
|
||||
outline: 1px dashed @FocusColor;
|
||||
}
|
||||
|
||||
/************************************************************************************************
|
||||
|
||||
@@ -14,6 +14,10 @@ body {
|
||||
font-family: @DataExplorerFont;
|
||||
font-size: 12px;
|
||||
height: 100%;
|
||||
|
||||
:focus {
|
||||
.focus()
|
||||
}
|
||||
}
|
||||
|
||||
.float-right {
|
||||
@@ -570,6 +574,12 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
.fileImportButton {
|
||||
height: 24px;
|
||||
border: @ButtonBorderWidth solid transparent;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.fileUploadSummaryContainer {
|
||||
margin-top: 40px;
|
||||
|
||||
@@ -1016,6 +1026,18 @@ menuQuickStart {
|
||||
background: #262626;
|
||||
}
|
||||
|
||||
.panelContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.panelContentWrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.contextual-pane {
|
||||
top: 0px;
|
||||
right: 0 !important;
|
||||
@@ -1232,23 +1254,25 @@ menuQuickStart {
|
||||
padding: 2px 30px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.btncreatecoll1:hover {
|
||||
background: @AccentMediumHigh;
|
||||
color: #fff;
|
||||
border-color: @AccentMediumHigh;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.btncreatecoll1:active {
|
||||
border: 1px solid #0072c6;
|
||||
&:active {
|
||||
border-color: #0072c6;
|
||||
background-color: #0072c6;
|
||||
color: white;
|
||||
padding: 2px 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.leftpanel-okbut .genericPaneSubmitBtn {
|
||||
border: 1px solid @AccentMediumHigh;
|
||||
background-color: @AccentMediumHigh;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
height: 24px;
|
||||
|
||||
&:active {
|
||||
border-color: #0072c6;
|
||||
background-color: #0072c6;
|
||||
}
|
||||
}
|
||||
|
||||
.btncreatecoll1-off {
|
||||
@@ -1361,6 +1385,15 @@ p {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.headerline .closePaneBtn {
|
||||
float: right;
|
||||
cursor: pointer;
|
||||
width: 16px;
|
||||
height: 100%;
|
||||
margin-right: 4px;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.closeImg {
|
||||
float: right;
|
||||
cursor: pointer;
|
||||
@@ -1710,6 +1743,13 @@ input::-webkit-calendar-picker-indicator {
|
||||
margin: (2 * @MediumSpace) 0px;
|
||||
}
|
||||
|
||||
.contextual-pane .panelMainContent {
|
||||
padding-left: 34px;
|
||||
padding-right: 34px;
|
||||
color: @BaseDark;
|
||||
margin: (2 * @MediumSpace) 0px;
|
||||
}
|
||||
|
||||
.contextual-pane .paneFooter {
|
||||
width: 100%;
|
||||
height: 60px;
|
||||
|
||||
9829
package-lock.json
generated
9829
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
30
package.json
30
package.json
@@ -4,7 +4,7 @@
|
||||
"description": "Cosmos Explorer",
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"@azure/cosmos": "3.6.3",
|
||||
"@azure/cosmos": "3.7.1",
|
||||
"@azure/cosmos-language-service": "0.0.4",
|
||||
"@jupyterlab/services": "4.2.0",
|
||||
"@jupyterlab/terminal": "1.2.1",
|
||||
@@ -37,22 +37,25 @@
|
||||
"@uifabric/react-cards": "0.109.53",
|
||||
"@uifabric/styling": "7.11.2",
|
||||
"abort-controller": "3.0.0",
|
||||
"applicationinsights": "1.8.0",
|
||||
"babel-polyfill": "6.26.0",
|
||||
"bootstrap": "3.3.7",
|
||||
"canvas": "2.6.0",
|
||||
"clean-webpack-plugin": "0.1.19",
|
||||
"copy-webpack-plugin": "4.5.4",
|
||||
"copy-webpack-plugin": "6.0.2",
|
||||
"crossroads": "0.12.2",
|
||||
"css-element-queries": "1.1.1",
|
||||
"datatables.net-colreorder-dt": "1.5.1",
|
||||
"datatables.net-dt": "1.10.19",
|
||||
"date-fns": "1.29.0",
|
||||
"dayjs": "1.8.19",
|
||||
"dotenv": "8.2.0",
|
||||
"es6-object-assign": "1.1.0",
|
||||
"es6-symbol": "3.1.3",
|
||||
"eslint-plugin-jest": "23.8.2",
|
||||
"eslint-plugin-jest": "23.13.2",
|
||||
"hasher": "1.2.0",
|
||||
"immutable": "4.0.0-rc.12",
|
||||
"is-ci": "2.0.0",
|
||||
"jquery": "3.4.0",
|
||||
"jquery-typeahead": "2.10.6",
|
||||
"jquery-ui-dist": "1.12.1",
|
||||
@@ -110,8 +113,8 @@
|
||||
"@types/text-encoding": "0.0.33",
|
||||
"@types/underscore": "1.7.36",
|
||||
"@types/webfontloader": "1.6.29",
|
||||
"@typescript-eslint/eslint-plugin": "2.25.0",
|
||||
"@typescript-eslint/parser": "2.25.0",
|
||||
"@typescript-eslint/eslint-plugin": "3.2.0",
|
||||
"@typescript-eslint/parser": "3.2.0",
|
||||
"adal-angular": "1.0.15",
|
||||
"babel-jest": "24.9.0",
|
||||
"babel-loader": "8.1.0",
|
||||
@@ -123,9 +126,10 @@
|
||||
"enzyme": "3.10.0",
|
||||
"enzyme-adapter-react-16": "1.15.1",
|
||||
"enzyme-to-json": "3.4.3",
|
||||
"eslint": "6.8.0",
|
||||
"eslint": "7.3.1",
|
||||
"eslint-cli": "1.1.1",
|
||||
"eslint-plugin-react": "7.19.0",
|
||||
"eslint-plugin-no-null": "1.0.2",
|
||||
"eslint-plugin-react": "7.20.0",
|
||||
"expose-loader": "0.7.5",
|
||||
"file-loader": "2.0.0",
|
||||
"fs-extra": "7.0.0",
|
||||
@@ -133,8 +137,9 @@
|
||||
"html-loader-jest": "0.2.1",
|
||||
"html-webpack-plugin": "3.2.0",
|
||||
"inline-css": "2.2.5",
|
||||
"jest": "24.9.0",
|
||||
"jest": "25.5.4",
|
||||
"jest-canvas-mock": "2.1.0",
|
||||
"jest-puppeteer": "4.4.0",
|
||||
"jest-trx-results-processor": "0.0.7",
|
||||
"less": "3.8.1",
|
||||
"less-loader": "4.1.0",
|
||||
@@ -142,20 +147,21 @@
|
||||
"mini-css-extract-plugin": "0.4.3",
|
||||
"monaco-editor-webpack-plugin": "1.7.0",
|
||||
"prettier": "1.19.1",
|
||||
"puppeteer": "4.0.0",
|
||||
"raw-loader": "0.5.1",
|
||||
"rimraf": "3.0.0",
|
||||
"sinon": "3.2.1",
|
||||
"style-loader": "0.23.0",
|
||||
"terser-webpack-plugin": "2.3.5",
|
||||
"terser-webpack-plugin": "3.0.5",
|
||||
"ts-loader": "6.2.2",
|
||||
"tslint": "5.11.0",
|
||||
"tslint-microsoft-contrib": "6.0.0",
|
||||
"typescript": "3.8.3",
|
||||
"url-loader": "1.1.1",
|
||||
"webpack": "4.41.2",
|
||||
"webpack": "4.43.0",
|
||||
"webpack-bundle-analyzer": "3.6.1",
|
||||
"webpack-cli": "3.3.10",
|
||||
"webpack-dev-server": "3.9.0",
|
||||
"webpack-dev-server": "3.11.0",
|
||||
"worker-loader": "2.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
@@ -168,8 +174,8 @@
|
||||
"pack:fast": "node --max_old_space_size=10196 ./node_modules/webpack/bin/webpack.js --mode development --progress",
|
||||
"copyToConsumers": "node copyToConsumers",
|
||||
"test": "rimraf coverage && jest",
|
||||
"test:e2e": "jest -c ./jest.config.e2e.js --detectOpenHandles",
|
||||
"watch": "npm run start",
|
||||
"integrationTest": "runIntegrationTests.cmd",
|
||||
"build:ase": "gulp build:ase",
|
||||
"compile": "tsc",
|
||||
"compile:contracts": "tsc -p ./tsconfig.contracts.json",
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
@echo off
|
||||
|
||||
@for /f "delims=" %%P in ('npm prefix -g') do set "NPM_PREFIX=%%P"
|
||||
@echo npm prefix = %NPM_PREFIX%
|
||||
@echo Compiling TypeScript Test Sources ...
|
||||
call %NPM_PREFIX%\tsc -p ./test
|
||||
if %errorlevel% neq 0 goto end
|
||||
|
||||
copy .\test\Integration\TestRunner.html .\test\out\test\Integration /y >nul 2>&1
|
||||
|
||||
@echo Copying files for test simulation against Emulator ...
|
||||
rmdir "%ProgramFiles%\Azure Cosmos DB Emulator\Packages\DataExplorer\test" >nul 2>&1
|
||||
mkdir "%ProgramFiles%\Azure Cosmos DB Emulator\Packages\DataExplorer\test" >nul 2>&1
|
||||
|
||||
xcopy .\node_modules\jasmine-core\lib .\test\out\lib /s /c /i /r /y >nul 2>&1
|
||||
xcopy .\node_modules\jasmine-core\images .\test\out\lib\images /s /c /i /r /y >nul 2>&1
|
||||
xcopy .\test\out "%ProgramFiles%\Azure Cosmos DB Emulator\Packages\DataExplorer\test" /s /c /i /r /y >nul 2>&1
|
||||
|
||||
@echo Initiating test runner ...
|
||||
start https://localhost:8081/_explorer/test/test/Integration/TestRunner.html
|
||||
@echo Done!
|
||||
|
||||
:end
|
||||
@echo on
|
||||
@@ -104,29 +104,25 @@ export class CapabilityNames {
|
||||
}
|
||||
|
||||
export class Features {
|
||||
public static readonly graphs = "graphs";
|
||||
public static readonly cosmosdb = "cosmosdb";
|
||||
public static readonly enableChangeFeedPolicy = "enablechangefeedpolicy";
|
||||
public static readonly enableRupm = "enablerupm";
|
||||
public static readonly cacheOptimizations = "dataexplorercacheoptimizations";
|
||||
public static readonly executeSproc = "dataexplorerexecutesproc";
|
||||
public static readonly hostedDataExplorer = "hosteddataexplorerenabled";
|
||||
public static readonly enableTtl = "enablettl";
|
||||
public static readonly enableNotebooks = "enablenotebooks";
|
||||
public static readonly enableGallery = "enablegallery";
|
||||
public static readonly enableGalleryPublish = "enablegallerypublish";
|
||||
public static readonly enableSpark = "enablespark";
|
||||
public static readonly livyEndpoint = "livyendpoint";
|
||||
public static readonly settingsPane = "dataexplorersettingspane";
|
||||
public static readonly throughputOverview = "throughputOverview";
|
||||
public static readonly enableNteract = "enablenteract";
|
||||
public static readonly notebookServerUrl = "notebookserverurl";
|
||||
public static readonly notebookServerToken = "notebookservertoken";
|
||||
public static readonly notebookBasePath = "notebookbasepath";
|
||||
public static readonly enableLegacyResourceTree = "enablelegacyresourcetree";
|
||||
public static readonly canExceedMaximumValue = "canexceedmaximumvalue";
|
||||
public static readonly enableFixedCollectionWithSharedThroughput = "enablefixedcollectionwithsharedthroughput";
|
||||
public static readonly enableAutoPilotV2 = "enableautopilotv2";
|
||||
public static readonly ttl90Days = "ttl90days";
|
||||
public static readonly enableRightPanelV2 = "enablerightpanelv2";
|
||||
}
|
||||
|
||||
export class AfecFeatures {
|
||||
|
||||
@@ -21,13 +21,15 @@ const _global = typeof self === "undefined" ? window : self;
|
||||
export const tokenProvider = async (requestInfo: RequestInfo) => {
|
||||
const { verb, resourceId, resourceType, headers } = requestInfo;
|
||||
if (config.platform === Platform.Emulator) {
|
||||
// TODO Remove any. SDK expects a return value for tokenProvider, but we are mutating the header object instead.
|
||||
return setAuthorizationTokenHeaderUsingMasterKey(verb, resourceId, resourceType, headers, EmulatorMasterKey) as any;
|
||||
// TODO This SDK method mutates the headers object. Find a better one or fix the SDK.
|
||||
await setAuthorizationTokenHeaderUsingMasterKey(verb, resourceId, resourceType, headers, EmulatorMasterKey);
|
||||
return decodeURIComponent(headers.authorization);
|
||||
}
|
||||
|
||||
if (_masterKey) {
|
||||
// TODO Remove any. SDK expects a return value for tokenProvider, but we are mutating the header object instead.
|
||||
return setAuthorizationTokenHeaderUsingMasterKey(verb, resourceId, resourceType, headers, _masterKey) as any;
|
||||
// TODO This SDK method mutates the headers object. Find a better one or fix the SDK.
|
||||
await setAuthorizationTokenHeaderUsingMasterKey(verb, resourceId, resourceType, headers, EmulatorMasterKey);
|
||||
return decodeURIComponent(headers.authorization);
|
||||
}
|
||||
|
||||
if (_resourceToken) {
|
||||
@@ -47,7 +49,9 @@ export const requestPlugin: Cosmos.Plugin<any> = async (requestContext, next) =>
|
||||
|
||||
export const endpoint = () => {
|
||||
if (config.platform === Platform.Emulator) {
|
||||
return config.EMULATOR_ENDPOINT || window.parent.location.origin;
|
||||
// In worker scope, _global(self).parent does not exist
|
||||
const location = _global.parent ? _global.parent.location : _global.location;
|
||||
return config.EMULATOR_ENDPOINT || location.origin;
|
||||
}
|
||||
return _endpoint || (_databaseAccount && _databaseAccount.properties && _databaseAccount.properties.documentEndpoint);
|
||||
};
|
||||
|
||||
@@ -6,7 +6,7 @@ import Q from "q";
|
||||
import { ConflictDefinition, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
||||
import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import { DataAccessUtilityBase } from "./DataAccessUtilityBase";
|
||||
import { Logger } from "./Logger";
|
||||
import * as Logger from "./Logger";
|
||||
import { MessageHandler } from "./MessageHandler";
|
||||
import { MessageTypes } from "../Contracts/ExplorerContracts";
|
||||
import { MinimalQueryIterator, nextPage } from "./IteratorUtilities";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { LogEntryLevel } from "../Contracts/Diagnostics";
|
||||
import { Logger } from "./Logger";
|
||||
import * as Logger from "./Logger";
|
||||
import { MessageHandler } from "./MessageHandler";
|
||||
import { MessageTypes } from "../Contracts/ExplorerContracts";
|
||||
|
||||
|
||||
@@ -4,50 +4,35 @@ import { appInsights } from "../Shared/appInsights";
|
||||
import { SeverityLevel } from "@microsoft/applicationinsights-web";
|
||||
|
||||
// TODO: Move to a separate Diagnostics folder
|
||||
export class Logger {
|
||||
public static logInfo(message: string | Record<string, any>, area: string, code?: number): void {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function logInfo(message: string | Record<string, any>, area: string, code?: number): void {
|
||||
let logMessage: string;
|
||||
if (typeof message === "string") {
|
||||
logMessage = message;
|
||||
} else {
|
||||
logMessage = JSON.stringify(message, Object.getOwnPropertyNames(message));
|
||||
}
|
||||
const entry: Diagnostics.LogEntry = Logger._generateLogEntry(
|
||||
Diagnostics.LogEntryLevel.Verbose,
|
||||
logMessage,
|
||||
area,
|
||||
code
|
||||
);
|
||||
return Logger._logEntry(entry);
|
||||
}
|
||||
const entry: Diagnostics.LogEntry = _generateLogEntry(Diagnostics.LogEntryLevel.Verbose, logMessage, area, code);
|
||||
return _logEntry(entry);
|
||||
}
|
||||
|
||||
public static logWarning(message: string, area: string, code?: number): void {
|
||||
const entry: Diagnostics.LogEntry = Logger._generateLogEntry(
|
||||
Diagnostics.LogEntryLevel.Warning,
|
||||
message,
|
||||
area,
|
||||
code
|
||||
);
|
||||
return Logger._logEntry(entry);
|
||||
}
|
||||
export function logWarning(message: string, area: string, code?: number): void {
|
||||
const entry: Diagnostics.LogEntry = _generateLogEntry(Diagnostics.LogEntryLevel.Warning, message, area, code);
|
||||
return _logEntry(entry);
|
||||
}
|
||||
|
||||
public static logError(message: string | Error, area: string, code?: number): void {
|
||||
export function logError(message: string | Error, area: string, code?: number): void {
|
||||
let logMessage: string;
|
||||
if (typeof message === "string") {
|
||||
logMessage = message;
|
||||
} else {
|
||||
logMessage = JSON.stringify(message, Object.getOwnPropertyNames(message));
|
||||
}
|
||||
const entry: Diagnostics.LogEntry = Logger._generateLogEntry(
|
||||
Diagnostics.LogEntryLevel.Error,
|
||||
logMessage,
|
||||
area,
|
||||
code
|
||||
);
|
||||
return Logger._logEntry(entry);
|
||||
}
|
||||
const entry: Diagnostics.LogEntry = _generateLogEntry(Diagnostics.LogEntryLevel.Error, logMessage, area, code);
|
||||
return _logEntry(entry);
|
||||
}
|
||||
|
||||
private static _logEntry(entry: Diagnostics.LogEntry): void {
|
||||
function _logEntry(entry: Diagnostics.LogEntry): void {
|
||||
MessageHandler.sendMessage({
|
||||
type: MessageTypes.LogInfo,
|
||||
data: JSON.stringify(entry)
|
||||
@@ -68,14 +53,14 @@ export class Logger {
|
||||
}
|
||||
})(entry.level);
|
||||
appInsights.trackTrace({ message: entry.message, severityLevel }, { area: entry.area });
|
||||
}
|
||||
}
|
||||
|
||||
private static _generateLogEntry(
|
||||
function _generateLogEntry(
|
||||
level: Diagnostics.LogEntryLevel,
|
||||
message: string,
|
||||
area: string,
|
||||
code: number
|
||||
): Diagnostics.LogEntry {
|
||||
): Diagnostics.LogEntry {
|
||||
return {
|
||||
timestamp: new Date().getUTCSeconds(),
|
||||
level: level,
|
||||
@@ -83,5 +68,4 @@ export class Logger {
|
||||
area: area,
|
||||
code: code
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,13 +31,13 @@ function authHeaders(): any {
|
||||
}
|
||||
}
|
||||
|
||||
export function queryIterator(databaseId: string, collection: Collection, query: string) {
|
||||
let continuationToken: string = null;
|
||||
export function queryIterator(databaseId: string, collection: Collection, query: string): any {
|
||||
let continuationToken: string;
|
||||
return {
|
||||
fetchNext: () => {
|
||||
return queryDocuments(databaseId, collection, false, query).then(response => {
|
||||
continuationToken = response.continuationToken;
|
||||
let headers = {} as any;
|
||||
const headers = {} as any;
|
||||
response.headers.forEach((value: any, key: any) => {
|
||||
headers[key] = value;
|
||||
});
|
||||
@@ -114,14 +114,7 @@ export function queryDocuments(
|
||||
headers: response.headers
|
||||
};
|
||||
}
|
||||
const errorMessage = await response.text();
|
||||
if (response.status === HttpStatusCodes.Forbidden) {
|
||||
MessageHandler.sendMessage({
|
||||
type: MessageTypes.ForbiddenError,
|
||||
reason: errorMessage
|
||||
});
|
||||
}
|
||||
throw new Error(errorMessage);
|
||||
return errorHandling(response, "querying documents", params);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -160,11 +153,11 @@ export function readDocument(
|
||||
)
|
||||
}
|
||||
})
|
||||
.then(async response => {
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
return response.json();
|
||||
}
|
||||
errorHandling(response);
|
||||
return errorHandling(response, "reading document", params);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -199,11 +192,11 @@ export function createDocument(
|
||||
...authHeaders()
|
||||
}
|
||||
})
|
||||
.then(async response => {
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
return response.json();
|
||||
}
|
||||
errorHandling(response);
|
||||
return errorHandling(response, "creating document", params);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -243,11 +236,11 @@ export function updateDocument(
|
||||
[CosmosSDKConstants.HttpHeaders.PartitionKey]: JSON.stringify(documentId.partitionKeyHeader())
|
||||
}
|
||||
})
|
||||
.then(async response => {
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
return response.json();
|
||||
}
|
||||
errorHandling(response);
|
||||
return errorHandling(response, "updating document", params);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -285,11 +278,11 @@ export function deleteDocument(
|
||||
[CosmosSDKConstants.HttpHeaders.PartitionKey]: JSON.stringify(documentId.partitionKeyHeader())
|
||||
}
|
||||
})
|
||||
.then(async response => {
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
return;
|
||||
return undefined;
|
||||
}
|
||||
errorHandling(response);
|
||||
return errorHandling(response, "deleting document", params);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -340,15 +333,11 @@ export function createMongoCollectionWithProxy(
|
||||
}
|
||||
}
|
||||
)
|
||||
.then(async response => {
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
return;
|
||||
return undefined;
|
||||
}
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Error creating collection: ${await response.json()}, Payload: ${params}`
|
||||
);
|
||||
errorHandling(response);
|
||||
return errorHandling(response, "creating collection", params);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -407,13 +396,16 @@ export function getEndpoint(databaseAccount: ViewModels.DatabaseAccount): string
|
||||
return url;
|
||||
}
|
||||
|
||||
async function errorHandling(response: any): Promise<any> {
|
||||
async function errorHandling(response: any, action: string, params: any): Promise<any> {
|
||||
const errorMessage = await response.text();
|
||||
// Log the error where the user can see it
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Error ${action}: ${errorMessage}, Payload: ${JSON.stringify(params)}`
|
||||
);
|
||||
if (response.status === HttpStatusCodes.Forbidden) {
|
||||
MessageHandler.sendMessage({
|
||||
type: MessageTypes.ForbiddenError,
|
||||
reason: errorMessage
|
||||
});
|
||||
MessageHandler.sendMessage({ type: MessageTypes.ForbiddenError, reason: errorMessage });
|
||||
return;
|
||||
}
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
@@ -462,14 +454,6 @@ export async function _createMongoCollectionWithARM(
|
||||
rpPayloadToCreateCollection
|
||||
);
|
||||
} catch (response) {
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Error creating collection: ${JSON.stringify(response)}`
|
||||
);
|
||||
if (response.status === HttpStatusCodes.Forbidden) {
|
||||
MessageHandler.sendMessage({ type: MessageTypes.ForbiddenError });
|
||||
return;
|
||||
}
|
||||
throw new Error(`Error creating collection`);
|
||||
return errorHandling(response, "creating collection", undefined);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { BackendDefaults, HttpStatusCodes, SavedQueries } from "./Constants";
|
||||
import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import { CosmosClient } from "./CosmosClient";
|
||||
import { ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
||||
import { Logger } from "./Logger";
|
||||
import * as Logger from "./Logger";
|
||||
import { NotificationConsoleUtils } from "../Utils/NotificationConsoleUtils";
|
||||
import { QueryUtils } from "../Utils/QueryUtils";
|
||||
|
||||
|
||||
@@ -704,49 +704,6 @@ export interface MemoryUsageInfo {
|
||||
totalKB: number;
|
||||
}
|
||||
|
||||
export interface NotebookMetadata {
|
||||
date: string;
|
||||
description: string;
|
||||
tags: string[];
|
||||
author: string;
|
||||
views: number;
|
||||
likes: number;
|
||||
downloads: number;
|
||||
imageUrl: string;
|
||||
}
|
||||
|
||||
export interface UserMetadata {
|
||||
likedNotebooks: string[];
|
||||
}
|
||||
|
||||
export interface GitHubInfoJunoResponse {
|
||||
encoding: string;
|
||||
encodedContent: string;
|
||||
content: string;
|
||||
target: string;
|
||||
submoduleGitUrl: string;
|
||||
name: string;
|
||||
path: string;
|
||||
sha: string;
|
||||
size: number;
|
||||
type: {
|
||||
stringValue: string;
|
||||
value: number;
|
||||
};
|
||||
downloadUrl: string;
|
||||
url: string;
|
||||
gitUrl: string;
|
||||
htmlUrl: string;
|
||||
metadata?: NotebookMetadata;
|
||||
officialSamplesIndex?: number;
|
||||
isLikedNotebook?: boolean;
|
||||
}
|
||||
|
||||
export interface LikedNotebooksJunoResponse {
|
||||
likedNotebooksContent: GitHubInfoJunoResponse[];
|
||||
userMetadata: UserMetadata;
|
||||
}
|
||||
|
||||
export interface resourceTokenConnectionStringProperties {
|
||||
accountEndpoint: string;
|
||||
collectionId: string;
|
||||
|
||||
@@ -9,14 +9,11 @@ import { AccessibleVerticalList } from "../Explorer/Tree/AccessibleVerticalList"
|
||||
import { ArcadiaWorkspaceItem } from "../Explorer/Controls/Arcadia/ArcadiaMenuPicker";
|
||||
import { CassandraTableKey, CassandraTableKeys, TableDataClient } from "../Explorer/Tables/TableDataClient";
|
||||
import { CommandButtonComponentProps } from "../Explorer/Controls/CommandButton/CommandButtonComponent";
|
||||
import { CommandButtonOptions } from "../Explorer/Controls/CommandButton/CommandButton";
|
||||
import { ConsoleData } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import { ExecuteSprocParam } from "../Explorer/Panes/ExecuteSprocParamsPane";
|
||||
import { GitHubClient } from "../GitHub/GitHubClient";
|
||||
import { GitHubOAuthService } from "../GitHub/GitHubOAuthService";
|
||||
import { IColumnSetting } from "../Explorer/Panes/Tables/TableColumnOptionsPane";
|
||||
import { IContentProvider } from "@nteract/core";
|
||||
import { JunoClient } from "../Juno/JunoClient";
|
||||
import { JunoClient, IGalleryItem } from "../Juno/JunoClient";
|
||||
import { Library } from "./DataModels";
|
||||
import { MostRecentActivity } from "../Explorer/MostRecentActivity/MostRecentActivity";
|
||||
import { NotebookContentItem } from "../Explorer/Notebook/NotebookContentItem";
|
||||
@@ -27,6 +24,8 @@ import { Splitter } from "../Common/Splitter";
|
||||
import { StringInputPane } from "../Explorer/Panes/StringInputPane";
|
||||
import { TextFieldProps } from "../Explorer/Controls/DialogReactComponent/DialogComponent";
|
||||
import { UploadDetails } from "../workers/upload/definitions";
|
||||
import { UploadItemsPaneAdapter } from "../Explorer/Panes/UploadItemsPaneAdapter";
|
||||
import { ReactAdapter } from "../Bindings/ReactBindingHandler";
|
||||
|
||||
export interface ExplorerOptions {
|
||||
documentClientUtility: DocumentClientUtilityBase;
|
||||
@@ -85,8 +84,10 @@ export interface Explorer {
|
||||
armEndpoint: ko.Observable<string>;
|
||||
isFeatureEnabled: (feature: string) => boolean;
|
||||
isGalleryEnabled: ko.Computed<boolean>;
|
||||
isGalleryPublishEnabled: ko.Computed<boolean>;
|
||||
isGitHubPaneEnabled: ko.Observable<boolean>;
|
||||
isGraphsEnabled: ko.Computed<boolean>;
|
||||
isPublishNotebookPaneEnabled: ko.Observable<boolean>;
|
||||
isRightPanelV2Enabled: ko.Computed<boolean>;
|
||||
canExceedMaximumValue: ko.Computed<boolean>;
|
||||
hasAutoPilotV2FeatureFlag: ko.Computed<boolean>;
|
||||
isHostedDataExplorerEnabled: ko.Computed<boolean>;
|
||||
@@ -141,6 +142,7 @@ export interface Explorer {
|
||||
executeSprocParamsPane: ExecuteSprocParamsPane;
|
||||
renewAdHocAccessPane: RenewAdHocAccessPane;
|
||||
uploadItemsPane: UploadItemsPane;
|
||||
uploadItemsPaneAdapter: UploadItemsPaneAdapter;
|
||||
loadQueryPane: LoadQueryPane;
|
||||
saveQueryPane: ContextualPane;
|
||||
browseQueriesPane: BrowseQueriesPane;
|
||||
@@ -152,6 +154,7 @@ export interface Explorer {
|
||||
libraryManagePane: ContextualPane;
|
||||
clusterLibraryPane: ContextualPane;
|
||||
gitHubReposPane: ContextualPane;
|
||||
publishNotebookPaneAdapter: ReactAdapter;
|
||||
|
||||
// Facade
|
||||
logConsoleData(data: ConsoleData): void;
|
||||
@@ -223,22 +226,17 @@ export interface Explorer {
|
||||
arcadiaWorkspaces: ko.ObservableArray<ArcadiaWorkspaceItem>;
|
||||
isNotebookTabActive: ko.Computed<boolean>;
|
||||
memoryUsageInfo: ko.Observable<DataModels.MemoryUsageInfo>;
|
||||
notebookManager?: any; // This is dynamically loaded
|
||||
openNotebook(notebookContentItem: NotebookContentItem): Promise<boolean>; // True if it was opened, false otherwise
|
||||
resetNotebookWorkspace(): void;
|
||||
importAndOpen: (path: string) => Promise<boolean>;
|
||||
importAndOpenFromGallery: (path: string, newName: string, content: any) => Promise<boolean>;
|
||||
importAndOpenFromGallery: (name: string, content: string) => Promise<boolean>;
|
||||
publishNotebook: (name: string, content: string) => void;
|
||||
openNotebookTerminal: (kind: TerminalKind) => void;
|
||||
openGallery: () => void;
|
||||
openNotebookViewer: (
|
||||
notebookUrl: string,
|
||||
notebookMetadata: DataModels.NotebookMetadata,
|
||||
onNotebookMetadataChange: (newNotebookMetadata: DataModels.NotebookMetadata) => Promise<void>,
|
||||
isLikedNotebook: boolean
|
||||
) => void;
|
||||
openGallery: (notebookUrl?: string, galleryItem?: IGalleryItem, isFavorite?: boolean) => void;
|
||||
openNotebookViewer: (notebookUrl: string) => void;
|
||||
notebookWorkspaceManager: NotebookWorkspaceManager;
|
||||
sparkClusterManager: SparkClusterManager;
|
||||
notebookContentProvider: IContentProvider;
|
||||
gitHubOAuthService: GitHubOAuthService;
|
||||
mostRecentActivity: MostRecentActivity;
|
||||
initNotebooks: (databaseAccount: DataModels.DatabaseAccount) => Promise<void>;
|
||||
deleteCluster(): void;
|
||||
@@ -337,17 +335,6 @@ export interface Button {
|
||||
isSelected?: ko.Computed<boolean>;
|
||||
}
|
||||
|
||||
export interface CommandButton {
|
||||
disabled: ko.Subscribable<boolean>;
|
||||
visible: ko.Subscribable<boolean>;
|
||||
iconSrc: string;
|
||||
commandButtonLabel: string | ko.Observable<string>;
|
||||
tooltipText: string | ko.Observable<string>;
|
||||
children: ko.ObservableArray<CommandButtonOptions>;
|
||||
|
||||
commandClickCallback: () => void;
|
||||
}
|
||||
|
||||
export interface NotificationConsole {
|
||||
filteredConsoleData: ko.ObservableArray<ConsoleData>;
|
||||
isConsoleExpanded: ko.Observable<boolean>;
|
||||
@@ -371,7 +358,6 @@ export interface TreeNode {
|
||||
id: ko.Observable<string>;
|
||||
database?: Database;
|
||||
collection?: Collection;
|
||||
contextMenu?: ContextMenu;
|
||||
|
||||
onNewQueryClick?(source: any, event: MouseEvent): void;
|
||||
onNewStoredProcedureClick?(source: Collection, event: MouseEvent): void;
|
||||
@@ -536,7 +522,7 @@ export interface StoredProcedure extends TreeNode {
|
||||
id: ko.Observable<string>;
|
||||
body: ko.Observable<string>;
|
||||
|
||||
delete(source: TreeNode, event: MouseEvent | KeyboardEvent): void;
|
||||
delete(): void;
|
||||
open: () => void;
|
||||
select(): void;
|
||||
execute(params: string[], partitionKeyValue?: string): void;
|
||||
@@ -550,7 +536,7 @@ export interface UserDefinedFunction extends TreeNode {
|
||||
id: ko.Observable<string>;
|
||||
body: ko.Observable<string>;
|
||||
|
||||
delete(source: TreeNode, event: MouseEvent | KeyboardEvent): void;
|
||||
delete(): void;
|
||||
open: () => void;
|
||||
select(): void;
|
||||
}
|
||||
@@ -565,7 +551,7 @@ export interface Trigger extends TreeNode {
|
||||
triggerType: ko.Observable<string>;
|
||||
triggerOperation: ko.Observable<string>;
|
||||
|
||||
delete(source: TreeNode, event: MouseEvent | KeyboardEvent): void;
|
||||
delete(): void;
|
||||
open: () => void;
|
||||
select(): void;
|
||||
}
|
||||
@@ -605,6 +591,16 @@ export interface GitHubReposPaneOptions extends PaneOptions {
|
||||
junoClient: JunoClient;
|
||||
}
|
||||
|
||||
export interface PublishNotebookPaneOptions extends PaneOptions {
|
||||
junoClient: JunoClient;
|
||||
}
|
||||
|
||||
export interface PublishNotebookPaneOpenOptions {
|
||||
name: string;
|
||||
author: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface AddCollectionPaneOptions extends PaneOptions {
|
||||
isPreferredApiTable: ko.Computed<boolean>;
|
||||
databaseId?: string;
|
||||
@@ -884,16 +880,16 @@ export interface TerminalTabOptions extends TabOptions {
|
||||
export interface GalleryTabOptions extends TabOptions {
|
||||
account: DatabaseAccount;
|
||||
container: Explorer;
|
||||
junoClient: JunoClient;
|
||||
notebookUrl?: string;
|
||||
galleryItem?: IGalleryItem;
|
||||
isFavorite?: boolean;
|
||||
}
|
||||
|
||||
export interface NotebookViewerTabOptions extends TabOptions {
|
||||
account: DatabaseAccount;
|
||||
container: Explorer;
|
||||
notebookUrl: string;
|
||||
notebookName: string;
|
||||
notebookMetadata: DataModels.NotebookMetadata;
|
||||
onNotebookMetadataChange: (newNotebookMetadata: DataModels.NotebookMetadata) => Promise<void>;
|
||||
isLikedNotebook: boolean;
|
||||
}
|
||||
|
||||
export interface DocumentsTabOptions extends TabOptions {
|
||||
@@ -1174,7 +1170,6 @@ export interface TriggerTab extends ScriptTab {
|
||||
}
|
||||
|
||||
export interface GraphTab extends Tab {}
|
||||
export interface NotebookTab extends Tab {}
|
||||
export interface EditorPosition {
|
||||
line: number;
|
||||
column: number;
|
||||
@@ -1218,7 +1213,7 @@ export enum CollectionTabKind {
|
||||
MongoShell = 10,
|
||||
DatabaseSettings = 11,
|
||||
Conflicts = 12,
|
||||
Notebook = 13,
|
||||
Notebook = 13 /* Deprecated */,
|
||||
Terminal = 14,
|
||||
NotebookV2 = 15,
|
||||
SparkMasterTab = 16,
|
||||
@@ -1232,17 +1227,6 @@ export enum TerminalKind {
|
||||
Cassandra = 2
|
||||
}
|
||||
|
||||
export interface ContextMenu {
|
||||
container: Explorer;
|
||||
visible: ko.Observable<boolean>;
|
||||
elementId: string;
|
||||
options: ko.ObservableArray<CommandButtonOptions>;
|
||||
tabIndex: ko.Observable<number>;
|
||||
|
||||
show(source: any, event: MouseEvent | KeyboardEvent): void;
|
||||
hide(source: any, event: MouseEvent | KeyboardEvent): void;
|
||||
}
|
||||
|
||||
export interface DataExplorerInputsFrame {
|
||||
databaseAccount: any;
|
||||
subscriptionId: string;
|
||||
|
||||
@@ -4,10 +4,6 @@ import * as ko from "knockout";
|
||||
import "./ComponentRegisterer";
|
||||
|
||||
describe("Component Registerer", () => {
|
||||
it("should register command-button component", () => {
|
||||
expect(ko.components.isRegistered("command-button")).toBe(true);
|
||||
});
|
||||
|
||||
it("should register input-typeahead component", () => {
|
||||
expect(ko.components.isRegistered("input-typeahead")).toBe(true);
|
||||
});
|
||||
@@ -64,10 +60,6 @@ describe("Component Registerer", () => {
|
||||
expect(ko.components.isRegistered("graph-tab")).toBe(true);
|
||||
});
|
||||
|
||||
it("should register notebook-tab component", () => {
|
||||
expect(ko.components.isRegistered("notebook-tab")).toBe(true);
|
||||
});
|
||||
|
||||
it("should register notebookv2-tab component", () => {
|
||||
expect(ko.components.isRegistered("notebookv2-tab")).toBe(true);
|
||||
});
|
||||
@@ -84,30 +76,6 @@ describe("Component Registerer", () => {
|
||||
expect(ko.components.isRegistered("mongo-shell-tab")).toBe(true);
|
||||
});
|
||||
|
||||
it("should register resource-tree component", () => {
|
||||
expect(ko.components.isRegistered("resource-tree")).toBe(true);
|
||||
});
|
||||
|
||||
it("should register database-node component", () => {
|
||||
expect(ko.components.isRegistered("database-node")).toBe(true);
|
||||
});
|
||||
|
||||
it("should register collection-node component", () => {
|
||||
expect(ko.components.isRegistered("collection-node")).toBe(true);
|
||||
});
|
||||
|
||||
it("should register stored-procedure-node component", () => {
|
||||
expect(ko.components.isRegistered("stored-procedure-node")).toBe(true);
|
||||
});
|
||||
|
||||
it("should register trigger-node component", () => {
|
||||
expect(ko.components.isRegistered("trigger-node")).toBe(true);
|
||||
});
|
||||
|
||||
it("should register user-defined-function-node component", () => {
|
||||
expect(ko.components.isRegistered("user-defined-function-node")).toBe(true);
|
||||
});
|
||||
|
||||
it("should registeradd-collection-pane component", () => {
|
||||
expect(ko.components.isRegistered("add-collection-pane")).toBe(true);
|
||||
});
|
||||
@@ -156,10 +124,6 @@ describe("Component Registerer", () => {
|
||||
expect(ko.components.isRegistered("manage-spark-cluster-pane")).toBe(true);
|
||||
});
|
||||
|
||||
it("should register collection-node-context-menu component", () => {
|
||||
expect(ko.components.isRegistered("collection-node-context-menu")).toBe(true);
|
||||
});
|
||||
|
||||
it("should register dynamic-list component", () => {
|
||||
expect(ko.components.isRegistered("dynamic-list")).toBe(true);
|
||||
});
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import * as ko from "knockout";
|
||||
import * as PaneComponents from "./Panes/PaneComponents";
|
||||
import * as TabComponents from "./Tabs/TabComponents";
|
||||
import * as TreeComponents from "./Tree/TreeComponents";
|
||||
import { CollapsiblePanelComponent } from "./Controls/CollapsiblePanel/CollapsiblePanelComponent";
|
||||
import { CommandButtonComponent } from "./Controls/CommandButton/CommandButton";
|
||||
import { DiffEditorComponent } from "./Controls/DiffEditor/DiffEditorComponent";
|
||||
import { DynamicListComponent } from "./Controls/DynamicList/DynamicListComponent";
|
||||
import { EditorComponent } from "./Controls/Editor/EditorComponent";
|
||||
@@ -16,7 +14,6 @@ import { ThroughputInputComponent } from "./Controls/ThroughputInput/ThroughputI
|
||||
import { ThroughputInputComponentAutoPilotV3 } from "./Controls/ThroughputInput/ThroughputInputComponentAutoPilotV3";
|
||||
import { ToolbarComponent } from "./Controls/Toolbar/Toolbar";
|
||||
|
||||
ko.components.register("command-button", CommandButtonComponent);
|
||||
ko.components.register("toolbar", new ToolbarComponent());
|
||||
ko.components.register("input-typeahead", new InputTypeaheadComponent());
|
||||
ko.components.register("new-vertex-form", NewVertexComponent);
|
||||
@@ -42,7 +39,6 @@ ko.components.register("tables-query-tab", new TabComponents.QueryTablesTab());
|
||||
ko.components.register("graph-tab", new TabComponents.GraphTab());
|
||||
ko.components.register("mongo-shell-tab", new TabComponents.MongoShellTab());
|
||||
ko.components.register("conflicts-tab", new TabComponents.ConflictsTab());
|
||||
ko.components.register("notebook-tab", new TabComponents.NotebookTab());
|
||||
ko.components.register("notebookv2-tab", new TabComponents.NotebookV2Tab());
|
||||
ko.components.register("terminal-tab", new TabComponents.TerminalTab());
|
||||
ko.components.register("spark-master-tab", new TabComponents.SparkMasterTab());
|
||||
@@ -52,14 +48,6 @@ ko.components.register("notebook-viewer-tab", new TabComponents.NotebookViewerTa
|
||||
// Database Tabs
|
||||
ko.components.register("database-settings-tab", new TabComponents.DatabaseSettingsTab());
|
||||
|
||||
// Resource Tree nodes
|
||||
ko.components.register("resource-tree", new TreeComponents.ResourceTree());
|
||||
ko.components.register("database-node", new TreeComponents.DatabaseTreeNode());
|
||||
ko.components.register("collection-node", new TreeComponents.CollectionTreeNode());
|
||||
ko.components.register("stored-procedure-node", new TreeComponents.StoredProcedureTreeNode());
|
||||
ko.components.register("trigger-node", new TreeComponents.TriggerTreeNode());
|
||||
ko.components.register("user-defined-function-node", new TreeComponents.UserDefinedFunctionTreeNode());
|
||||
|
||||
// Panes
|
||||
ko.components.register("add-database-pane", new PaneComponents.AddDatabasePaneComponent());
|
||||
ko.components.register("add-collection-pane", new PaneComponents.AddCollectionPaneComponent());
|
||||
@@ -93,6 +81,3 @@ ko.components.register("manage-spark-cluster-pane", new PaneComponents.ManageSpa
|
||||
ko.components.register("library-manage-pane", new PaneComponents.LibraryManagePaneComponent());
|
||||
ko.components.register("cluster-library-pane", new PaneComponents.ClusterLibraryPaneComponent());
|
||||
ko.components.register("github-repos-pane", new PaneComponents.GitHubReposPaneComponent());
|
||||
|
||||
// Menus
|
||||
ko.components.register("collection-node-context-menu", new TreeComponents.CollectionTreeNodeContextMenu());
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import * as ko from "knockout";
|
||||
import * as ViewModels from "../Contracts/ViewModels";
|
||||
import { CommandButtonOptions } from "./Controls/CommandButton/CommandButton";
|
||||
import { TreeNodeMenuItem } from "./Controls/TreeComponent/TreeComponent";
|
||||
import AddCollectionIcon from "../../images/AddCollection.svg";
|
||||
import AddSqlQueryIcon from "../../images/AddSqlQuery_16x16.svg";
|
||||
@@ -115,7 +114,10 @@ export class ResourceTreeContextMenuButtonFactory {
|
||||
return items;
|
||||
}
|
||||
|
||||
public static createStoreProcedureContextMenuItems(container: ViewModels.Explorer): TreeNodeMenuItem[] {
|
||||
public static createStoreProcedureContextMenuItems(
|
||||
container: ViewModels.Explorer,
|
||||
storedProcedure: ViewModels.StoredProcedure
|
||||
): TreeNodeMenuItem[] {
|
||||
if (container.isPreferredApiCassandra()) {
|
||||
return [];
|
||||
}
|
||||
@@ -123,16 +125,16 @@ export class ResourceTreeContextMenuButtonFactory {
|
||||
return [
|
||||
{
|
||||
iconSrc: DeleteSprocIcon,
|
||||
onClick: () => {
|
||||
const selectedStoreProcedure: ViewModels.StoredProcedure = container.findSelectedStoredProcedure();
|
||||
selectedStoreProcedure && selectedStoreProcedure.delete(selectedStoreProcedure, null);
|
||||
},
|
||||
onClick: () => storedProcedure.delete(),
|
||||
label: "Delete Store Procedure"
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
public static createTriggerContextMenuItems(container: ViewModels.Explorer): TreeNodeMenuItem[] {
|
||||
public static createTriggerContextMenuItems(
|
||||
container: ViewModels.Explorer,
|
||||
trigger: ViewModels.Trigger
|
||||
): TreeNodeMenuItem[] {
|
||||
if (container.isPreferredApiCassandra()) {
|
||||
return [];
|
||||
}
|
||||
@@ -140,16 +142,16 @@ export class ResourceTreeContextMenuButtonFactory {
|
||||
return [
|
||||
{
|
||||
iconSrc: DeleteTriggerIcon,
|
||||
onClick: () => {
|
||||
const selectedTrigger: ViewModels.Trigger = container.findSelectedTrigger();
|
||||
selectedTrigger && selectedTrigger.delete(selectedTrigger, null);
|
||||
},
|
||||
onClick: () => trigger.delete(),
|
||||
label: "Delete Trigger"
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
public static createUserDefinedFunctionContextMenuItems(container: ViewModels.Explorer): TreeNodeMenuItem[] {
|
||||
public static createUserDefinedFunctionContextMenuItems(
|
||||
container: ViewModels.Explorer,
|
||||
userDefinedFunction: ViewModels.UserDefinedFunction
|
||||
): TreeNodeMenuItem[] {
|
||||
if (container.isPreferredApiCassandra()) {
|
||||
return [];
|
||||
}
|
||||
@@ -157,266 +159,9 @@ export class ResourceTreeContextMenuButtonFactory {
|
||||
return [
|
||||
{
|
||||
iconSrc: DeleteUDFIcon,
|
||||
onClick: () => {
|
||||
const selectedUDF: ViewModels.UserDefinedFunction = container.findSelectedUDF();
|
||||
selectedUDF && selectedUDF.delete(selectedUDF, null);
|
||||
},
|
||||
onClick: () => userDefinedFunction.delete(),
|
||||
label: "Delete User Defined Function"
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Current resource tree (in KO)
|
||||
* TODO: Remove when switching to new resource tree
|
||||
*/
|
||||
export class ContextMenuButtonFactory {
|
||||
public static createDatabaseContextMenuButton(
|
||||
container: ViewModels.Explorer,
|
||||
btnParams: DatabaseContextMenuButtonParams
|
||||
): CommandButtonOptions[] {
|
||||
const addCollectionId = `${btnParams.databaseId}-${container.addCollectionText()}`;
|
||||
const deleteDatabaseId = `${btnParams.databaseId}-${container.deleteDatabaseText()}`;
|
||||
const newCollectionButtonOptions: CommandButtonOptions = {
|
||||
iconSrc: AddCollectionIcon,
|
||||
id: addCollectionId,
|
||||
onCommandClick: () => {
|
||||
if (container.isPreferredApiCassandra()) {
|
||||
container.cassandraAddCollectionPane.open();
|
||||
} else {
|
||||
container.addCollectionPane.open(container.selectedDatabaseId());
|
||||
}
|
||||
|
||||
const selectedDatabase: ViewModels.Database = container.findSelectedDatabase();
|
||||
selectedDatabase && selectedDatabase.contextMenu.hide(selectedDatabase, null);
|
||||
},
|
||||
commandButtonLabel: container.addCollectionText(),
|
||||
hasPopup: true
|
||||
};
|
||||
|
||||
const deleteDatabaseButtonOptions: CommandButtonOptions = {
|
||||
iconSrc: DeleteDatabaseIcon,
|
||||
id: deleteDatabaseId,
|
||||
onCommandClick: () => {
|
||||
const database: ViewModels.Database = container.findSelectedDatabase();
|
||||
database.onDeleteDatabaseContextMenuClick(database, null);
|
||||
},
|
||||
commandButtonLabel: container.deleteDatabaseText(),
|
||||
hasPopup: true,
|
||||
disabled: ko.computed<boolean>(() => container.isNoneSelected()),
|
||||
visible: ko.computed<boolean>(() => !container.isNoneSelected())
|
||||
};
|
||||
|
||||
return [newCollectionButtonOptions, deleteDatabaseButtonOptions];
|
||||
}
|
||||
|
||||
public static createCollectionContextMenuButton(
|
||||
container: ViewModels.Explorer,
|
||||
btnParams: CollectionContextMenuButtonParams
|
||||
): CommandButtonOptions[] {
|
||||
const newSqlQueryId = `${btnParams.databaseId}-${btnParams.collectionId}-newSqlQuery`;
|
||||
const newSqlQueryForGraphId = `${btnParams.databaseId}-${btnParams.collectionId}-newSqlQueryForGraph`;
|
||||
const newQueryForMongoId = `${btnParams.databaseId}-${btnParams.collectionId}-newQuery`;
|
||||
const newShellForMongoId = `${btnParams.databaseId}-${btnParams.collectionId}-newShell`;
|
||||
const newStoredProcedureId = `${btnParams.databaseId}-${btnParams.collectionId}-newStoredProcedure`;
|
||||
const udfId = `${btnParams.databaseId}-${btnParams.collectionId}-udf`;
|
||||
const newTriggerId = `${btnParams.databaseId}-${btnParams.collectionId}-newTrigger`;
|
||||
const deleteCollectionId = `${btnParams.databaseId}-${btnParams.collectionId}-${container.deleteCollectionText()}`;
|
||||
|
||||
const newSQLQueryButtonOptions: CommandButtonOptions = {
|
||||
iconSrc: AddSqlQueryIcon,
|
||||
id: newSqlQueryId,
|
||||
onCommandClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
|
||||
selectedCollection && selectedCollection.onNewQueryClick(selectedCollection, null);
|
||||
},
|
||||
commandButtonLabel: "New SQL Query",
|
||||
hasPopup: true,
|
||||
disabled: ko.computed<boolean>(
|
||||
() => container.isDatabaseNodeOrNoneSelected() && container.isPreferredApiDocumentDB()
|
||||
),
|
||||
visible: ko.computed<boolean>(
|
||||
() => !container.isDatabaseNodeOrNoneSelected() && container.isPreferredApiDocumentDB()
|
||||
)
|
||||
//TODO: Merge with add query logic below, same goes for CommandBarButtonFactory
|
||||
};
|
||||
|
||||
const newSQLQueryButtonOptionsForGraph: CommandButtonOptions = {
|
||||
iconSrc: AddSqlQueryIcon,
|
||||
id: newSqlQueryForGraphId,
|
||||
onCommandClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
|
||||
selectedCollection && selectedCollection.onNewQueryClick(selectedCollection, null);
|
||||
},
|
||||
commandButtonLabel: "New SQL Query",
|
||||
hasPopup: true,
|
||||
disabled: ko.computed<boolean>(() => container.isDatabaseNodeOrNoneSelected() && container.isPreferredApiGraph()),
|
||||
visible: ko.computed<boolean>(() => !container.isDatabaseNodeOrNoneSelected() && container.isPreferredApiGraph())
|
||||
};
|
||||
|
||||
const newMongoQueryButtonOptions: CommandButtonOptions = {
|
||||
iconSrc: AddSqlQueryIcon,
|
||||
id: newQueryForMongoId,
|
||||
onCommandClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
|
||||
selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection, null);
|
||||
},
|
||||
commandButtonLabel: "New Query",
|
||||
hasPopup: true,
|
||||
disabled: ko.computed<boolean>(
|
||||
() => container.isDatabaseNodeOrNoneSelected() && container.isPreferredApiMongoDB()
|
||||
),
|
||||
visible: ko.computed<boolean>(
|
||||
() => !container.isDatabaseNodeOrNoneSelected() && container.isPreferredApiMongoDB()
|
||||
)
|
||||
};
|
||||
|
||||
const newMongoShellButtonOptions: CommandButtonOptions = {
|
||||
iconSrc: HostedTerminalIcon,
|
||||
id: newShellForMongoId,
|
||||
onCommandClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
|
||||
selectedCollection && selectedCollection.onNewMongoShellClick();
|
||||
},
|
||||
commandButtonLabel: "New Shell",
|
||||
hasPopup: true,
|
||||
disabled: ko.computed<boolean>(
|
||||
() => container.isDatabaseNodeOrNoneSelected() && container.isPreferredApiMongoDB()
|
||||
),
|
||||
visible: ko.computed<boolean>(
|
||||
() => !container.isDatabaseNodeOrNoneSelected() && container.isPreferredApiMongoDB()
|
||||
)
|
||||
};
|
||||
|
||||
const newStoredProcedureButtonOptions: CommandButtonOptions = {
|
||||
iconSrc: AddStoredProcedureIcon,
|
||||
id: newStoredProcedureId,
|
||||
onCommandClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
|
||||
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection, null);
|
||||
},
|
||||
commandButtonLabel: "New Stored Procedure",
|
||||
hasPopup: true,
|
||||
disabled: ko.computed<boolean>(() => container.isDatabaseNodeOrNoneSelected()),
|
||||
visible: ko.computed<boolean>(
|
||||
() => !container.isDatabaseNodeOrNoneSelected() && !container.isPreferredApiCassandra()
|
||||
)
|
||||
};
|
||||
|
||||
const newUserDefinedFunctionButtonOptions: CommandButtonOptions = {
|
||||
iconSrc: AddUdfIcon,
|
||||
id: udfId,
|
||||
onCommandClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
|
||||
selectedCollection && selectedCollection.onNewUserDefinedFunctionClick(selectedCollection, null);
|
||||
},
|
||||
commandButtonLabel: "New UDF",
|
||||
hasPopup: true,
|
||||
disabled: ko.computed<boolean>(() => container.isDatabaseNodeOrNoneSelected()),
|
||||
visible: ko.computed<boolean>(
|
||||
() => !container.isDatabaseNodeOrNoneSelected() && !container.isPreferredApiCassandra()
|
||||
)
|
||||
};
|
||||
|
||||
const newTriggerButtonOptions: CommandButtonOptions = {
|
||||
iconSrc: AddTriggerIcon,
|
||||
id: newTriggerId,
|
||||
onCommandClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
|
||||
selectedCollection && selectedCollection.onNewTriggerClick(selectedCollection, null);
|
||||
},
|
||||
commandButtonLabel: "New Trigger",
|
||||
hasPopup: true,
|
||||
disabled: ko.computed<boolean>(() => container.isDatabaseNodeOrNoneSelected()),
|
||||
visible: ko.computed<boolean>(
|
||||
() => !container.isDatabaseNodeOrNoneSelected() && !container.isPreferredApiCassandra()
|
||||
)
|
||||
};
|
||||
|
||||
const deleteCollectionButtonOptions: CommandButtonOptions = {
|
||||
iconSrc: DeleteCollectionIcon,
|
||||
id: deleteCollectionId,
|
||||
onCommandClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
|
||||
selectedCollection && selectedCollection.onDeleteCollectionContextMenuClick(selectedCollection, null);
|
||||
},
|
||||
commandButtonLabel: container.deleteCollectionText(),
|
||||
hasPopup: true,
|
||||
disabled: ko.computed<boolean>(() => container.isDatabaseNodeOrNoneSelected()),
|
||||
visible: ko.computed<boolean>(() => !container.isDatabaseNodeOrNoneSelected())
|
||||
//TODO: Change to isCollectionNodeorNoneSelected and same in CommandBarButtonFactory
|
||||
};
|
||||
|
||||
return [
|
||||
newSQLQueryButtonOptions,
|
||||
newSQLQueryButtonOptionsForGraph,
|
||||
newMongoQueryButtonOptions,
|
||||
newMongoShellButtonOptions,
|
||||
newStoredProcedureButtonOptions,
|
||||
newUserDefinedFunctionButtonOptions,
|
||||
newTriggerButtonOptions,
|
||||
deleteCollectionButtonOptions
|
||||
];
|
||||
}
|
||||
|
||||
public static createStoreProcedureContextMenuButton(container: ViewModels.Explorer): CommandButtonOptions[] {
|
||||
const deleteStoredProcedureId = "Context Menu - Delete Stored Procedure";
|
||||
const deleteStoreProcedureButtonOptions: CommandButtonOptions = {
|
||||
iconSrc: DeleteSprocIcon,
|
||||
id: deleteStoredProcedureId,
|
||||
onCommandClick: () => {
|
||||
const selectedStoreProcedure: ViewModels.StoredProcedure = container.findSelectedStoredProcedure();
|
||||
selectedStoreProcedure && selectedStoreProcedure.delete(selectedStoreProcedure, null);
|
||||
},
|
||||
commandButtonLabel: "Delete Stored Procedure",
|
||||
hasPopup: false,
|
||||
disabled: ko.computed<boolean>(() => container.isDatabaseNodeOrNoneSelected()),
|
||||
visible: ko.computed<boolean>(
|
||||
() => !container.isDatabaseNodeOrNoneSelected() && !container.isPreferredApiCassandra()
|
||||
)
|
||||
};
|
||||
|
||||
return [deleteStoreProcedureButtonOptions];
|
||||
}
|
||||
|
||||
public static createTriggerContextMenuButton(container: ViewModels.Explorer): CommandButtonOptions[] {
|
||||
const deleteTriggerId = "Context Menu - Delete Trigger";
|
||||
const deleteTriggerButtonOptions: CommandButtonOptions = {
|
||||
iconSrc: DeleteTriggerIcon,
|
||||
id: deleteTriggerId,
|
||||
onCommandClick: () => {
|
||||
const selectedTrigger: ViewModels.Trigger = container.findSelectedTrigger();
|
||||
selectedTrigger && selectedTrigger.delete(selectedTrigger, null);
|
||||
},
|
||||
commandButtonLabel: "Delete Trigger",
|
||||
hasPopup: false,
|
||||
disabled: ko.computed<boolean>(() => container.isDatabaseNodeOrNoneSelected()),
|
||||
visible: ko.computed<boolean>(
|
||||
() => !container.isDatabaseNodeOrNoneSelected() && !container.isPreferredApiCassandra()
|
||||
)
|
||||
};
|
||||
|
||||
return [deleteTriggerButtonOptions];
|
||||
}
|
||||
|
||||
public static createUserDefinedFunctionContextMenuButton(container: ViewModels.Explorer): CommandButtonOptions[] {
|
||||
const deleteUserDefinedFunctionId = "Context Menu - Delete User Defined Function";
|
||||
const deleteUserDefinedFunctionButtonOptions: CommandButtonOptions = {
|
||||
iconSrc: DeleteUDFIcon,
|
||||
id: deleteUserDefinedFunctionId,
|
||||
onCommandClick: () => {
|
||||
const selectedUDF: ViewModels.UserDefinedFunction = container.findSelectedUDF();
|
||||
selectedUDF && selectedUDF.delete(selectedUDF, null);
|
||||
},
|
||||
commandButtonLabel: "Delete User Defined Function",
|
||||
hasPopup: false,
|
||||
disabled: ko.computed<boolean>(() => container.isDatabaseNodeOrNoneSelected()),
|
||||
visible: ko.computed<boolean>(
|
||||
() => !container.isDatabaseNodeOrNoneSelected() && !container.isPreferredApiCassandra()
|
||||
)
|
||||
};
|
||||
|
||||
return [deleteUserDefinedFunctionButtonOptions];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
IContextualMenuProps,
|
||||
ContextualMenuItemType
|
||||
} from "office-ui-fabric-react/lib/ContextualMenu";
|
||||
import { Logger } from "../../../Common/Logger";
|
||||
import * as Logger from "../../../Common/Logger";
|
||||
|
||||
export interface ArcadiaMenuPickerProps {
|
||||
selectText?: string;
|
||||
|
||||
@@ -1,200 +0,0 @@
|
||||
@import "../../../../less/Common/Constants";
|
||||
|
||||
@ButtonIconSize: 18px;
|
||||
|
||||
.commandBar {
|
||||
padding-left: @DefaultSpace;
|
||||
border-bottom: @ButtonBorderWidth solid @BaseMedium;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
height: @topcommandbarheight;
|
||||
|
||||
.staticCommands {
|
||||
list-style: none;
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.overflowCommands {
|
||||
display:flex;
|
||||
flex: 1 0 auto;
|
||||
|
||||
.visibleCommands {
|
||||
display: inline-flex;
|
||||
list-style: none;
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.partialSplitterContainer {
|
||||
padding: @SmallSpace @DefaultSpace @SmallSpace @SmallSpace;
|
||||
.flex-display();
|
||||
}
|
||||
}
|
||||
|
||||
.commandExpand {
|
||||
border: none;
|
||||
padding: 0px;
|
||||
direction: rtl;
|
||||
|
||||
&:hover {
|
||||
.hover();
|
||||
cursor: pointer;
|
||||
& > .commandDropdownContainer {
|
||||
display: block !important; // TODO: Remove after reusing KO mouseover and mouseout event handlers
|
||||
}
|
||||
}
|
||||
|
||||
&:focus {
|
||||
.focus();
|
||||
}
|
||||
|
||||
.commandDropdownLauncher {
|
||||
direction: ltr;
|
||||
padding-top: @SmallSpace;
|
||||
|
||||
.commandIcon {
|
||||
vertical-align: text-top;
|
||||
}
|
||||
|
||||
.commandBarEllipses {
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.hiddenCommandsContainer > .commandDropdownLauncher {
|
||||
padding: 0px @DefaultSpace;
|
||||
}
|
||||
|
||||
.commandDropdownContainer {
|
||||
display: none;
|
||||
z-index: 1000;
|
||||
direction: ltr;
|
||||
position: absolute;
|
||||
width: fit-content;
|
||||
padding: 0px;
|
||||
background-color: @BaseLight;
|
||||
box-shadow: 1px 2px 6px @BaseMediumHigh, -2px 2px 6px @BaseMediumHigh;
|
||||
|
||||
.commandDropdown {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.feedbackButton {
|
||||
margin-right: @LargeSpace;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
command-button,
|
||||
.commandButtonReact {
|
||||
display: inline-flex;
|
||||
.commandButtonComponent {
|
||||
width: 100%;
|
||||
color: @BaseHigh;
|
||||
background-color: transparent;
|
||||
text-decoration: none;
|
||||
border: @ButtonBorderWidth solid transparent;
|
||||
.flex-display();
|
||||
|
||||
&:hover:not(.commandDisabled) {
|
||||
cursor: pointer;
|
||||
.hover();
|
||||
}
|
||||
|
||||
&:active:not(.commandDisabled) {
|
||||
border: @ButtonBorderWidth dashed @AccentMedium;
|
||||
.active();
|
||||
}
|
||||
|
||||
&:focus:not(.commandDisabled) {
|
||||
border: @ButtonBorderWidth dashed @AccentMedium;
|
||||
}
|
||||
|
||||
.commandContent {
|
||||
padding: @DefaultSpace @DefaultSpace @DefaultSpace;
|
||||
flex: 0 0 auto;
|
||||
|
||||
.commandIcon {
|
||||
margin: 0 @SmallSpace 0 0;
|
||||
vertical-align: text-top;
|
||||
width: @ButtonIconSize;
|
||||
height: @ButtonIconSize;
|
||||
}
|
||||
|
||||
.commandLabel {
|
||||
padding: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.commandContent .hasHiddenItems {
|
||||
padding-right: @SmallSpace;
|
||||
}
|
||||
}
|
||||
|
||||
.commandButtonComponent.commandDisabled {
|
||||
color: @BaseMediumHigh;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.commandExpand {
|
||||
padding-top: @SmallSpace;
|
||||
padding-bottom: @SmallSpace;
|
||||
&:hover {
|
||||
.hover();
|
||||
& > .commandDropdownContainer {
|
||||
display: block !important; // TODO: Remove after reusing KO mouseover and mouseout event handlers
|
||||
}
|
||||
}
|
||||
|
||||
&:focus {
|
||||
.focus();
|
||||
}
|
||||
|
||||
.commandDropdownLauncher {
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
|
||||
.commandButtonComponent {
|
||||
padding: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.expandDropdown {
|
||||
padding: @SmallSpace;
|
||||
|
||||
img {
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
|
||||
.partialSplitter {
|
||||
margin: @SmallSpace 0px 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.commandButtonComponent[tabindex]:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.selectedButton {
|
||||
background-color: @AccentLow;
|
||||
outline: none
|
||||
}
|
||||
}
|
||||
|
||||
.partialSplitter {
|
||||
border-left: @ButtonBorderWidth solid @BaseMediumHigh;
|
||||
}
|
||||
|
||||
.commandDropdown .commandButtonComponent {
|
||||
padding-left: 0px;
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
import * as ko from "knockout";
|
||||
import { CommandButtonComponent, CommandButtonOptions } from "./CommandButton";
|
||||
|
||||
const mockLabel = "Some Label";
|
||||
const id = "Some id";
|
||||
|
||||
function buildComponent(buttonOptions: any) {
|
||||
document.body.innerHTML = CommandButtonComponent.template as any;
|
||||
const vm = new CommandButtonComponent.viewModel(buttonOptions);
|
||||
ko.applyBindings(vm);
|
||||
}
|
||||
|
||||
describe("Command Button Component", () => {
|
||||
function buildButtonOptions(
|
||||
onClick: () => void,
|
||||
id?: string,
|
||||
label?: string,
|
||||
disabled?: ko.Observable<boolean>,
|
||||
visible?: ko.Observable<boolean>,
|
||||
tooltipText?: string
|
||||
): { buttonProps: CommandButtonOptions } {
|
||||
return {
|
||||
buttonProps: {
|
||||
iconSrc: "images/AddCollection.svg",
|
||||
id: id,
|
||||
commandButtonLabel: label || mockLabel,
|
||||
disabled: disabled,
|
||||
visible: visible,
|
||||
tooltipText: tooltipText,
|
||||
hasPopup: false,
|
||||
onCommandClick: onClick
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function buildSplitterButtonOptions(
|
||||
onClick: () => void,
|
||||
id?: string,
|
||||
label?: string,
|
||||
disabled?: ko.Observable<boolean>,
|
||||
visible?: ko.Observable<boolean>,
|
||||
tooltipText?: string
|
||||
): { buttonProps: CommandButtonOptions } {
|
||||
const child: CommandButtonOptions = {
|
||||
iconSrc: "images/settings_15x15.svg",
|
||||
id: id,
|
||||
commandButtonLabel: label || mockLabel,
|
||||
disabled: disabled,
|
||||
visible: visible,
|
||||
tooltipText: tooltipText,
|
||||
hasPopup: false,
|
||||
onCommandClick: onClick
|
||||
};
|
||||
|
||||
return {
|
||||
buttonProps: {
|
||||
iconSrc: "images/AddCollection.svg",
|
||||
id: id,
|
||||
commandButtonLabel: label || mockLabel,
|
||||
disabled: disabled,
|
||||
visible: visible,
|
||||
tooltipText: tooltipText,
|
||||
hasPopup: false,
|
||||
onCommandClick: onClick,
|
||||
children: [child]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
ko.cleanNode(document);
|
||||
document.body.innerHTML = "";
|
||||
});
|
||||
|
||||
describe("Rendering", () => {
|
||||
it("should display button label", () => {
|
||||
const buttonOptions = buildButtonOptions(() => {
|
||||
/** do nothing **/
|
||||
}, mockLabel);
|
||||
buildComponent(buttonOptions);
|
||||
expect(document.getElementsByClassName("commandButtonComponent").item(0).textContent).toContain(mockLabel);
|
||||
});
|
||||
|
||||
it("should display button icon", () => {
|
||||
const buttonOptions = buildButtonOptions(() => {
|
||||
/** do nothing **/
|
||||
});
|
||||
buildComponent(buttonOptions);
|
||||
expect(
|
||||
document
|
||||
.getElementsByTagName("img")
|
||||
.item(0)
|
||||
.getAttribute("src")
|
||||
).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Behavior", () => {
|
||||
let clickSpy: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
clickSpy = jasmine.createSpy("Command button click spy");
|
||||
});
|
||||
|
||||
it("should trigger the click handler when the command button is clicked", () => {
|
||||
const buttonOptions = buildButtonOptions(() => clickSpy());
|
||||
buildComponent(buttonOptions);
|
||||
document
|
||||
.getElementsByClassName("commandButtonComponent")
|
||||
.item(0)
|
||||
.dispatchEvent(new Event("click"));
|
||||
expect(clickSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not trigger the click handler when command button is disabled", () => {
|
||||
const buttonOptions = buildButtonOptions(() => clickSpy(), id, mockLabel, ko.observable(true));
|
||||
buildComponent(buttonOptions);
|
||||
document
|
||||
.getElementsByClassName("commandButtonComponent")
|
||||
.item(0)
|
||||
.dispatchEvent(new Event("click"));
|
||||
expect(clickSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not have a dropdown if it has no child", () => {
|
||||
const buttonOptions = buildButtonOptions(() => clickSpy(), id, mockLabel, ko.observable(true));
|
||||
buildComponent(buttonOptions);
|
||||
const dropdownSize = document.getElementsByClassName("commandExpand").length;
|
||||
expect(dropdownSize).toBe(0);
|
||||
});
|
||||
|
||||
it("should have a dropdown if it has a child", () => {
|
||||
const buttonOptions = buildSplitterButtonOptions(() => clickSpy(), id, mockLabel, ko.observable(true));
|
||||
buildComponent(buttonOptions);
|
||||
const dropdownSize = document.getElementsByClassName("commandExpand").length;
|
||||
expect(dropdownSize).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,191 +0,0 @@
|
||||
/**
|
||||
* How to use this component:
|
||||
*
|
||||
* In your html markup, use:
|
||||
* <command-button params="{
|
||||
* iconSrc: '/icon/example/src/',
|
||||
* onCommandClick: () => { doSomething },
|
||||
* commandButtonLabel: 'Some Label'
|
||||
* disabled: true/false
|
||||
* }">
|
||||
* </command-button>
|
||||
*
|
||||
*/
|
||||
|
||||
import * as ko from "knockout";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import { WaitsForTemplateViewModel } from "../../WaitsForTemplateViewModel";
|
||||
import { KeyCodes } from "../../../Common/Constants";
|
||||
|
||||
import TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import template from "./command-button.html";
|
||||
|
||||
/**
|
||||
* Options for this component
|
||||
*/
|
||||
export interface CommandButtonOptions {
|
||||
/**
|
||||
* image source for the button icon
|
||||
*/
|
||||
iconSrc: string;
|
||||
|
||||
/**
|
||||
* Id for the button icon
|
||||
*/
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* Click handler for command button click
|
||||
*/
|
||||
onCommandClick: () => void;
|
||||
|
||||
/**
|
||||
* Label for the button
|
||||
*/
|
||||
commandButtonLabel: string | ko.Observable<string>;
|
||||
|
||||
/**
|
||||
* True if this button opens a tab or pane, false otherwise.
|
||||
*/
|
||||
hasPopup: boolean;
|
||||
|
||||
/**
|
||||
* Enabled/disabled state of command button
|
||||
*/
|
||||
disabled?: ko.Subscribable<boolean>;
|
||||
|
||||
/**
|
||||
* Visibility/Invisibility of the button
|
||||
*/
|
||||
visible?: ko.Subscribable<boolean>;
|
||||
|
||||
/**
|
||||
* Whether or not the button should have the 'selectedButton' styling
|
||||
*/
|
||||
isSelected?: ko.Observable<boolean>;
|
||||
|
||||
/**
|
||||
* Text to displayed in the tooltip on hover
|
||||
*/
|
||||
tooltipText?: string | ko.Observable<string>;
|
||||
|
||||
/**
|
||||
* Callback triggered when the template is bound to the component
|
||||
*/
|
||||
onTemplateReady?: () => void;
|
||||
|
||||
/**
|
||||
* tabindex for the command button
|
||||
*/
|
||||
tabIndex?: ko.Observable<number>;
|
||||
|
||||
/**
|
||||
* Childrens command buttons to hide in the dropdown
|
||||
*/
|
||||
children?: CommandButtonOptions[];
|
||||
}
|
||||
|
||||
export class CommandButtonViewModel extends WaitsForTemplateViewModel implements ViewModels.CommandButton {
|
||||
public commandClickCallback: () => void;
|
||||
public commandButtonId: string;
|
||||
public disabled: ko.Subscribable<boolean>;
|
||||
public visible: ko.Subscribable<boolean>;
|
||||
public isSelected: ko.Observable<boolean>;
|
||||
public iconSrc: string;
|
||||
public commandButtonLabel: ko.Observable<string>;
|
||||
public tooltipText: ko.Observable<string>;
|
||||
public tabIndex: ko.Observable<number>;
|
||||
public isTemplateReady: ko.Observable<boolean>;
|
||||
public hasPopup: boolean;
|
||||
public children: ko.ObservableArray<CommandButtonOptions>;
|
||||
|
||||
public constructor(options: { buttonProps: CommandButtonOptions }) {
|
||||
super();
|
||||
const props = options.buttonProps;
|
||||
const commandButtonLabel = props.commandButtonLabel;
|
||||
const tooltipText = props.tooltipText;
|
||||
this.commandButtonLabel =
|
||||
typeof commandButtonLabel === "string" ? ko.observable<string>(commandButtonLabel) : commandButtonLabel;
|
||||
this.commandButtonId = props.id;
|
||||
this.disabled = props.disabled || ko.observable(false);
|
||||
this.visible = props.visible || ko.observable(true);
|
||||
this.isSelected = props.isSelected || ko.observable(false);
|
||||
this.iconSrc = props.iconSrc;
|
||||
this.tabIndex = props.tabIndex || ko.observable(0);
|
||||
this.hasPopup = props.hasPopup;
|
||||
this.children = ko.observableArray(props.children);
|
||||
|
||||
super.onTemplateReady((isTemplateReady: boolean) => {
|
||||
if (isTemplateReady && props.onTemplateReady) {
|
||||
props.onTemplateReady();
|
||||
}
|
||||
});
|
||||
|
||||
if (tooltipText && typeof tooltipText === "string") {
|
||||
this.tooltipText = ko.observable<string>(tooltipText);
|
||||
} else if (tooltipText && typeof tooltipText === "function") {
|
||||
this.tooltipText = tooltipText;
|
||||
} else {
|
||||
this.tooltipText = this.commandButtonLabel;
|
||||
}
|
||||
|
||||
this.commandClickCallback = () => {
|
||||
if (this.disabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const el = document.querySelector(".commandDropdownContainer") as HTMLElement;
|
||||
if (el) {
|
||||
el.style.display = "none";
|
||||
}
|
||||
props.onCommandClick();
|
||||
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {
|
||||
commandButtonClicked: this.commandButtonLabel
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
public onKeyPress(source: any, event: KeyboardEvent): boolean {
|
||||
if (event.keyCode === KeyCodes.Space || event.keyCode === KeyCodes.Enter) {
|
||||
this.commandClickCallback && this.commandClickCallback();
|
||||
event.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public onLauncherKeyDown(source: any, event: KeyboardEvent): boolean {
|
||||
// TODO: Convert JQuery code into Knockout
|
||||
if (event.keyCode === KeyCodes.DownArrow) {
|
||||
$(event.target)
|
||||
.parent()
|
||||
.siblings()
|
||||
.children(".commandExpand")
|
||||
.children(".commandDropdownContainer")
|
||||
.hide();
|
||||
$(event.target)
|
||||
.children(".commandDropdownContainer")
|
||||
.show()
|
||||
.focus();
|
||||
event.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
if (event.keyCode === KeyCodes.UpArrow) {
|
||||
$(event.target)
|
||||
.children(".commandDropdownContainer")
|
||||
.hide();
|
||||
event.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper class for ko component registration
|
||||
*/
|
||||
export const CommandButtonComponent = {
|
||||
viewModel: CommandButtonViewModel,
|
||||
template
|
||||
};
|
||||
@@ -15,15 +15,20 @@ import { ArcadiaMenuPickerProps } from "../Arcadia/ArcadiaMenuPicker";
|
||||
* Options for this component
|
||||
*/
|
||||
export interface CommandButtonComponentProps {
|
||||
/**
|
||||
* font icon name for the button
|
||||
*/
|
||||
iconName?: string;
|
||||
|
||||
/**
|
||||
* image source for the button icon
|
||||
*/
|
||||
iconSrc: string;
|
||||
iconSrc?: string;
|
||||
|
||||
/**
|
||||
* image alt for accessibility
|
||||
*/
|
||||
iconAlt: string;
|
||||
iconAlt?: string;
|
||||
|
||||
/**
|
||||
* Click handler for command button click
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
<span
|
||||
class="commandButtonComponent"
|
||||
role="menuitem"
|
||||
tabindex="0"
|
||||
data-bind="setTemplateReady: true,
|
||||
css: {
|
||||
commandDisabled: disabled,
|
||||
selectedButton: isSelected
|
||||
},
|
||||
event: {
|
||||
keypress: onKeyPress
|
||||
},
|
||||
attr: {
|
||||
title: tooltipText,
|
||||
id: commandButtonId,
|
||||
tabindex: tabIndex ,
|
||||
'aria-disabled': disabled,
|
||||
'aria-haspopup': hasPopup
|
||||
},
|
||||
click: commandClickCallback,
|
||||
visible: visible"
|
||||
>
|
||||
<div class="commandContent" data-bind="css: { hasHiddenItems: children().length > 0 }">
|
||||
<img class="commandIcon" data-bind="attr: {src: iconSrc, alt: commandButtonLabel}" />
|
||||
<span class="commandLabel" data-bind="text: commandButtonLabel"></span>
|
||||
</div>
|
||||
</span>
|
||||
<!-- ko if: children().length > 0 -->
|
||||
<div class="commandExpand" tabindex="0" data-bind="visible: visible, event: { keydown: onLauncherKeyDown }">
|
||||
<div class="commandDropdownLauncher">
|
||||
<span class="partialSplitter"></span>
|
||||
<span class="expandDropdown"> <img src="/QueryBuilder/CollapseChevronDown_16x.png" /> </span>
|
||||
</div>
|
||||
<div class="commandDropdownContainer">
|
||||
<div class="commandDropdown" data-bind="foreach: children">
|
||||
<command-button params="{buttonProps: $data}"></command-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /ko -->
|
||||
@@ -0,0 +1,20 @@
|
||||
.featurePanelComponentContainer {
|
||||
width: 800px;
|
||||
|
||||
.urlContainer {
|
||||
padding: 10px;
|
||||
margin-bottom: 10px;
|
||||
white-space: nowrap;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.options {
|
||||
padding: 10px;
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.checkboxRow {
|
||||
width: 390px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import React from "react";
|
||||
import { shallow } from "enzyme";
|
||||
|
||||
import { FeaturePanelComponent } from "./FeaturePanelComponent";
|
||||
|
||||
describe("Feature panel", () => {
|
||||
it("renders all flags", () => {
|
||||
const wrapper = shallow(<FeaturePanelComponent />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
253
src/Explorer/Controls/FeaturePanel/FeaturePanelComponent.tsx
Normal file
253
src/Explorer/Controls/FeaturePanel/FeaturePanelComponent.tsx
Normal file
@@ -0,0 +1,253 @@
|
||||
import * as React from "react";
|
||||
import { Stack } from "office-ui-fabric-react/lib/Stack";
|
||||
import { Dropdown, IDropdownOption, IDropdownStyles } from "office-ui-fabric-react/lib/Dropdown";
|
||||
import { Checkbox } from "office-ui-fabric-react/lib/Checkbox";
|
||||
import { TextField, ITextFieldStyles } from "office-ui-fabric-react/lib/TextField";
|
||||
import { DefaultButton } from "office-ui-fabric-react";
|
||||
import "./FeaturePanelComponent.less";
|
||||
|
||||
export const FeaturePanelComponent: React.FunctionComponent = () => {
|
||||
// Initial conditions
|
||||
const originalParams = new URLSearchParams(window.location.search);
|
||||
const urlParams = new Map(); // Params with lowercase keys
|
||||
originalParams.forEach((value: string, key: string) => urlParams.set(key.toLocaleLowerCase(), value));
|
||||
|
||||
const baseUrlOptions = [
|
||||
{ key: "https://localhost:1234/explorer.html", text: "localhost:1234" },
|
||||
{ key: "https://cosmos.azure.com/explorer.html", text: "cosmos.azure.com" },
|
||||
{ key: "https://portal.azure.com", text: "portal" }
|
||||
];
|
||||
|
||||
const platformOptions = [
|
||||
{ key: "Hosted", text: "Hosted" },
|
||||
{ key: "Portal", text: "Portal" },
|
||||
{ key: "Emulator", text: "Emulator" },
|
||||
{ key: "", text: "None" }
|
||||
];
|
||||
|
||||
// React hooks to keep state
|
||||
const [baseUrl, setBaseUrl] = React.useState<IDropdownOption>(
|
||||
baseUrlOptions.find(o => o.key === window.location.origin + window.location.pathname) || baseUrlOptions[0]
|
||||
);
|
||||
const [platform, setPlatform] = React.useState<IDropdownOption>(
|
||||
urlParams.has("platform")
|
||||
? platformOptions.find(o => o.key === urlParams.get("platform")) || platformOptions[0]
|
||||
: platformOptions[0]
|
||||
);
|
||||
|
||||
const booleanFeatures: {
|
||||
key: string;
|
||||
label: string;
|
||||
value: string;
|
||||
disabled?: () => boolean;
|
||||
reactState?: [boolean, React.Dispatch<React.SetStateAction<boolean>>];
|
||||
onChange?: (_?: React.FormEvent<HTMLElement | HTMLInputElement>, checked?: boolean) => void;
|
||||
}[] = [
|
||||
{ key: "feature.enablechangefeedpolicy", label: "Enable change feed policy", value: "true" },
|
||||
{ key: "feature.enablerupm", label: "Enable RUPM", value: "true" },
|
||||
{ key: "feature.dataexplorerexecutesproc", label: "Execute stored procedure", value: "true" },
|
||||
{ key: "feature.hosteddataexplorerenabled", label: "Hosted Data Explorer (deprecated?)", value: "true" },
|
||||
{ key: "feature.enablettl", label: "Enable TTL", value: "true" },
|
||||
{ key: "feature.enablegallery", label: "Enable Notebook Gallery", value: "true" },
|
||||
{ key: "feature.enablegallerypublish", label: "Enable Notebook Gallery Publishing", value: "true" },
|
||||
{ key: "feature.canexceedmaximumvalue", label: "Can exceed max value", value: "true" },
|
||||
{
|
||||
key: "feature.enablefixedcollectionwithsharedthroughput",
|
||||
label: "Enable fixed collection with shared throughput",
|
||||
value: "true"
|
||||
},
|
||||
{ key: "feature.ttl90days", label: "TTL 90 days", value: "true" },
|
||||
{ key: "feature.enablenotebooks", label: "Enable notebooks", value: "true" },
|
||||
{
|
||||
key: "feature.customportal",
|
||||
label: "Force Production portal (portal only)",
|
||||
value: "false",
|
||||
disabled: (): boolean => baseUrl.key !== "https://portal.azure.com"
|
||||
},
|
||||
{ key: "feature.enablespark", label: "Enable Synapse", value: "true" },
|
||||
{ key: "feature.enableautopilotv2", label: "Enable Auto-pilot V2", value: "true" }
|
||||
];
|
||||
|
||||
const stringFeatures: {
|
||||
key: string;
|
||||
label: string;
|
||||
placeholder: string;
|
||||
disabled?: () => boolean;
|
||||
reactState?: [string, React.Dispatch<React.SetStateAction<string>>];
|
||||
onChange?: (_: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>, newValue?: string) => void;
|
||||
}[] = [
|
||||
{ key: "feature.notebookserverurl", label: "Notebook server URL", placeholder: "https://notebookserver" },
|
||||
{ key: "feature.notebookservertoken", label: "Notebook server token", placeholder: "" },
|
||||
{ key: "feature.notebookbasepath", label: "Notebook base path", placeholder: "" },
|
||||
{ key: "key", label: "Auth key", placeholder: "" },
|
||||
{
|
||||
key: "dataExplorerSource",
|
||||
label: "Data Explorer Source (portal only)",
|
||||
placeholder: "https://localhost:1234/explorer.html",
|
||||
disabled: (): boolean => baseUrl.key !== "https://portal.azure.com"
|
||||
},
|
||||
{ key: "feature.livyendpoint", label: "Livy endpoint", placeholder: "" }
|
||||
];
|
||||
|
||||
booleanFeatures.forEach(
|
||||
f => (f.reactState = React.useState<boolean>(urlParams.has(f.key) ? urlParams.get(f.key) === "true" : false))
|
||||
);
|
||||
stringFeatures.forEach(
|
||||
f => (f.reactState = React.useState<string>(urlParams.has(f.key) ? urlParams.get(f.key) : undefined))
|
||||
);
|
||||
|
||||
const buildUrl = (): string => {
|
||||
const fragments = (platform.key === "" ? [] : [`platform=${platform.key}`])
|
||||
.concat(booleanFeatures.map(f => (f.reactState[0] ? `${f.key}=${f.value}` : "")))
|
||||
.concat(stringFeatures.map(f => (f.reactState[0] ? `${f.key}=${encodeURIComponent(f.reactState[0])}` : "")))
|
||||
.filter(v => v && v.length > 0);
|
||||
|
||||
const paramString = fragments.length < 1 ? "" : `?${fragments.join("&")}`;
|
||||
return `${baseUrl.key}${paramString}`;
|
||||
};
|
||||
|
||||
const onChangeBaseUrl = (event: React.FormEvent<HTMLDivElement>, option?: IDropdownOption): void => {
|
||||
setBaseUrl(option);
|
||||
};
|
||||
|
||||
const onChangePlatform = (event: React.FormEvent<HTMLDivElement>, option?: IDropdownOption): void => {
|
||||
setPlatform(option);
|
||||
};
|
||||
|
||||
booleanFeatures.forEach(
|
||||
f =>
|
||||
(f.onChange = (ev?: React.FormEvent<HTMLElement | HTMLInputElement>, checked?: boolean): void => {
|
||||
f.reactState[1](checked);
|
||||
})
|
||||
);
|
||||
|
||||
stringFeatures.forEach(
|
||||
f =>
|
||||
(f.onChange = (event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>, newValue?: string): void => {
|
||||
f.reactState[1](newValue);
|
||||
})
|
||||
);
|
||||
|
||||
const onNotebookShortcut = (): void => {
|
||||
booleanFeatures.find(f => f.key === "feature.enablenotebooks").reactState[1](true);
|
||||
stringFeatures
|
||||
.find(f => f.key === "feature.notebookserverurl")
|
||||
.reactState[1]("https://localhost:10001/12345/notebook/");
|
||||
stringFeatures.find(f => f.key === "feature.notebookservertoken").reactState[1]("token");
|
||||
stringFeatures.find(f => f.key === "feature.notebookbasepath").reactState[1]("./notebooks");
|
||||
setPlatform(platformOptions.find(o => o.key === "Hosted"));
|
||||
};
|
||||
|
||||
const onPortalLocalDEShortcut = (): void => {
|
||||
setBaseUrl(baseUrlOptions.find(o => o.key === "https://portal.azure.com"));
|
||||
setPlatform(platformOptions.find(o => o.key === "Portal"));
|
||||
stringFeatures.find(f => f.key === "dataExplorerSource").reactState[1]("https://localhost:1234/explorer.html");
|
||||
};
|
||||
|
||||
const onReset = (): void => {
|
||||
booleanFeatures.forEach(f => f.reactState[1](false));
|
||||
stringFeatures.forEach(f => f.reactState[1](""));
|
||||
};
|
||||
|
||||
const stackTokens = { childrenGap: 10 };
|
||||
const dropdownStyles: Partial<IDropdownStyles> = { dropdown: { width: 200 } };
|
||||
const textFieldStyles: Partial<ITextFieldStyles> = { fieldGroup: { width: 300 } };
|
||||
|
||||
// Show in 2 columns to keep it compact
|
||||
let halfSize = Math.ceil(booleanFeatures.length / 2);
|
||||
const leftBooleanFeatures = booleanFeatures.slice(0, halfSize);
|
||||
const rightBooleanFeatures = booleanFeatures.slice(halfSize, booleanFeatures.length);
|
||||
|
||||
halfSize = Math.ceil(stringFeatures.length / 2);
|
||||
const leftStringFeatures = stringFeatures.slice(0, halfSize);
|
||||
const rightStringFeatures = stringFeatures.slice(halfSize, stringFeatures.length);
|
||||
|
||||
const anchorOptions = {
|
||||
href: buildUrl(),
|
||||
target: "_blank",
|
||||
rel: "noopener"
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="featurePanelComponentContainer">
|
||||
<div className="urlContainer">
|
||||
<a {...anchorOptions}>{buildUrl()}</a>
|
||||
</div>
|
||||
<Stack className="options" tokens={stackTokens}>
|
||||
<Stack horizontal horizontalAlign="space-between" tokens={stackTokens}>
|
||||
<DefaultButton onClick={onNotebookShortcut}>Notebooks on localhost</DefaultButton>
|
||||
<DefaultButton onClick={onPortalLocalDEShortcut}>Portal points to local DE</DefaultButton>
|
||||
<DefaultButton onClick={onReset}>Reset</DefaultButton>
|
||||
</Stack>
|
||||
<Stack horizontal horizontalAlign="start" tokens={stackTokens}>
|
||||
<Dropdown
|
||||
selectedKey={baseUrl.key}
|
||||
options={baseUrlOptions}
|
||||
onChange={onChangeBaseUrl}
|
||||
label="Base Url"
|
||||
styles={dropdownStyles}
|
||||
/>
|
||||
<Dropdown
|
||||
label="Platform"
|
||||
selectedKey={platform.key}
|
||||
onChange={onChangePlatform}
|
||||
options={platformOptions}
|
||||
styles={dropdownStyles}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack horizontal>
|
||||
<Stack className="checkboxRow" horizontalAlign="space-between">
|
||||
{leftBooleanFeatures.map(f => (
|
||||
<Checkbox
|
||||
key={f.key}
|
||||
label={f.label}
|
||||
checked={f.reactState[0]}
|
||||
onChange={f.onChange}
|
||||
disabled={f.disabled && f.disabled()}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
<Stack className="checkboxRow" horizontalAlign="space-between">
|
||||
{rightBooleanFeatures.map(f => (
|
||||
<Checkbox
|
||||
key={f.key}
|
||||
label={f.label}
|
||||
checked={f.reactState[0]}
|
||||
onChange={f.onChange}
|
||||
disabled={f.disabled && f.disabled()}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack horizontal tokens={stackTokens}>
|
||||
<Stack horizontalAlign="space-between">
|
||||
{leftStringFeatures.map(f => (
|
||||
<TextField
|
||||
key={f.key}
|
||||
value={f.reactState[0]}
|
||||
label={f.label}
|
||||
onChange={f.onChange}
|
||||
styles={textFieldStyles}
|
||||
placeholder={f.placeholder}
|
||||
disabled={f.disabled && f.disabled()}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
<Stack horizontalAlign="space-between">
|
||||
{rightStringFeatures.map(f => (
|
||||
<TextField
|
||||
key={f.key}
|
||||
value={f.reactState[0]}
|
||||
label={f.label}
|
||||
onChange={f.onChange}
|
||||
styles={textFieldStyles}
|
||||
placeholder={f.placeholder}
|
||||
disabled={f.disabled && f.disabled()}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
18
src/Explorer/Controls/FeaturePanel/FeaturePanelLauncher.less
Normal file
18
src/Explorer/Controls/FeaturePanel/FeaturePanelLauncher.less
Normal file
@@ -0,0 +1,18 @@
|
||||
.featurePanelLauncherContainer {
|
||||
.featurePanelLauncherModal {
|
||||
overflow-y: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.ms-Dialog-main {
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.activePatch {
|
||||
position: absolute;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
top: 20px;
|
||||
left: -20px;
|
||||
}
|
||||
86
src/Explorer/Controls/FeaturePanel/FeaturePanelLauncher.tsx
Normal file
86
src/Explorer/Controls/FeaturePanel/FeaturePanelLauncher.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import * as React from "react";
|
||||
import { FeaturePanelComponent } from "./FeaturePanelComponent";
|
||||
import { getTheme, mergeStyleSets, FontWeights, Modal, IconButton, IIconProps } from "office-ui-fabric-react";
|
||||
import "./FeaturePanelLauncher.less";
|
||||
|
||||
// Modal wrapper
|
||||
export const FeaturePanelLauncher: React.FunctionComponent = (): JSX.Element => {
|
||||
const [isModalOpen, showModal] = React.useState<boolean>(false);
|
||||
|
||||
const onActivate = (event: React.MouseEvent<HTMLSpanElement>): void => {
|
||||
if (!event.shiftKey || !event.ctrlKey) {
|
||||
return;
|
||||
}
|
||||
event.stopPropagation();
|
||||
showModal(true);
|
||||
};
|
||||
|
||||
const theme = getTheme();
|
||||
const contentStyles = mergeStyleSets({
|
||||
container: {
|
||||
display: "flex",
|
||||
flexFlow: "column nowrap",
|
||||
alignItems: "stretch"
|
||||
},
|
||||
header: [
|
||||
// tslint:disable-next-line:deprecation
|
||||
theme.fonts.xLargePlus,
|
||||
{
|
||||
flex: "1 1 auto",
|
||||
borderTop: `4px solid ${theme.palette.themePrimary}`,
|
||||
color: theme.palette.neutralPrimary,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
fontWeight: FontWeights.semibold,
|
||||
padding: "12px 12px 14px 24px"
|
||||
}
|
||||
],
|
||||
body: {
|
||||
flex: "4 4 auto",
|
||||
overflowY: "hidden",
|
||||
marginBottom: 40,
|
||||
height: "100%",
|
||||
display: "flex"
|
||||
}
|
||||
});
|
||||
|
||||
const iconButtonStyles = {
|
||||
root: {
|
||||
color: theme.palette.neutralPrimary,
|
||||
marginLeft: "auto",
|
||||
marginTop: "4px",
|
||||
marginRight: "2px"
|
||||
},
|
||||
rootHovered: {
|
||||
color: theme.palette.neutralDark
|
||||
}
|
||||
};
|
||||
const cancelIcon: IIconProps = { iconName: "Cancel" };
|
||||
const hideModal = (): void => showModal(false);
|
||||
|
||||
return (
|
||||
<span className="activePatch" onDoubleClick={onActivate}>
|
||||
<Modal
|
||||
className="featurePanelLauncherContainer"
|
||||
titleAriaId="Features"
|
||||
isOpen={isModalOpen}
|
||||
onDismiss={hideModal}
|
||||
isBlocking={false}
|
||||
scrollableContentClassName="featurePanelLauncherModal"
|
||||
>
|
||||
<div className={contentStyles.header}>
|
||||
<span>Data Explorer Launcher</span>
|
||||
<IconButton
|
||||
styles={iconButtonStyles}
|
||||
iconProps={cancelIcon}
|
||||
ariaLabel="Close popup modal"
|
||||
onClick={hideModal}
|
||||
/>
|
||||
</div>
|
||||
<div className={contentStyles.body}>
|
||||
<FeaturePanelComponent />
|
||||
</div>
|
||||
</Modal>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,318 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Feature panel renders all flags 1`] = `
|
||||
<div
|
||||
className="featurePanelComponentContainer"
|
||||
>
|
||||
<div
|
||||
className="urlContainer"
|
||||
>
|
||||
<a
|
||||
href="https://localhost:1234/explorer.html?platform=Hosted"
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
>
|
||||
https://localhost:1234/explorer.html?platform=Hosted
|
||||
</a>
|
||||
</div>
|
||||
<Stack
|
||||
className="options"
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
horizontalAlign="space-between"
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<CustomizedDefaultButton
|
||||
onClick={[Function]}
|
||||
>
|
||||
Notebooks on localhost
|
||||
</CustomizedDefaultButton>
|
||||
<CustomizedDefaultButton
|
||||
onClick={[Function]}
|
||||
>
|
||||
Portal points to local DE
|
||||
</CustomizedDefaultButton>
|
||||
<CustomizedDefaultButton
|
||||
onClick={[Function]}
|
||||
>
|
||||
Reset
|
||||
</CustomizedDefaultButton>
|
||||
</Stack>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
horizontalAlign="start"
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<StyledWithResponsiveMode
|
||||
label="Base Url"
|
||||
onChange={[Function]}
|
||||
options={
|
||||
Array [
|
||||
Object {
|
||||
"key": "https://localhost:1234/explorer.html",
|
||||
"text": "localhost:1234",
|
||||
},
|
||||
Object {
|
||||
"key": "https://cosmos.azure.com/explorer.html",
|
||||
"text": "cosmos.azure.com",
|
||||
},
|
||||
Object {
|
||||
"key": "https://portal.azure.com",
|
||||
"text": "portal",
|
||||
},
|
||||
]
|
||||
}
|
||||
selectedKey="https://localhost:1234/explorer.html"
|
||||
styles={
|
||||
Object {
|
||||
"dropdown": Object {
|
||||
"width": 200,
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
<StyledWithResponsiveMode
|
||||
label="Platform"
|
||||
onChange={[Function]}
|
||||
options={
|
||||
Array [
|
||||
Object {
|
||||
"key": "Hosted",
|
||||
"text": "Hosted",
|
||||
},
|
||||
Object {
|
||||
"key": "Portal",
|
||||
"text": "Portal",
|
||||
},
|
||||
Object {
|
||||
"key": "Emulator",
|
||||
"text": "Emulator",
|
||||
},
|
||||
Object {
|
||||
"key": "",
|
||||
"text": "None",
|
||||
},
|
||||
]
|
||||
}
|
||||
selectedKey="Hosted"
|
||||
styles={
|
||||
Object {
|
||||
"dropdown": Object {
|
||||
"width": 200,
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
>
|
||||
<Stack
|
||||
className="checkboxRow"
|
||||
horizontalAlign="space-between"
|
||||
>
|
||||
<StyledCheckboxBase
|
||||
checked={false}
|
||||
key="feature.enablechangefeedpolicy"
|
||||
label="Enable change feed policy"
|
||||
onChange={[Function]}
|
||||
/>
|
||||
<StyledCheckboxBase
|
||||
checked={false}
|
||||
key="feature.enablerupm"
|
||||
label="Enable RUPM"
|
||||
onChange={[Function]}
|
||||
/>
|
||||
<StyledCheckboxBase
|
||||
checked={false}
|
||||
key="feature.dataexplorerexecutesproc"
|
||||
label="Execute stored procedure"
|
||||
onChange={[Function]}
|
||||
/>
|
||||
<StyledCheckboxBase
|
||||
checked={false}
|
||||
key="feature.hosteddataexplorerenabled"
|
||||
label="Hosted Data Explorer (deprecated?)"
|
||||
onChange={[Function]}
|
||||
/>
|
||||
<StyledCheckboxBase
|
||||
checked={false}
|
||||
key="feature.enablettl"
|
||||
label="Enable TTL"
|
||||
onChange={[Function]}
|
||||
/>
|
||||
<StyledCheckboxBase
|
||||
checked={false}
|
||||
key="feature.enablegallery"
|
||||
label="Enable Notebook Gallery"
|
||||
onChange={[Function]}
|
||||
/>
|
||||
<StyledCheckboxBase
|
||||
checked={false}
|
||||
key="feature.enablegallerypublish"
|
||||
label="Enable Notebook Gallery Publishing"
|
||||
onChange={[Function]}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack
|
||||
className="checkboxRow"
|
||||
horizontalAlign="space-between"
|
||||
>
|
||||
<StyledCheckboxBase
|
||||
checked={false}
|
||||
key="feature.canexceedmaximumvalue"
|
||||
label="Can exceed max value"
|
||||
onChange={[Function]}
|
||||
/>
|
||||
<StyledCheckboxBase
|
||||
checked={false}
|
||||
key="feature.enablefixedcollectionwithsharedthroughput"
|
||||
label="Enable fixed collection with shared throughput"
|
||||
onChange={[Function]}
|
||||
/>
|
||||
<StyledCheckboxBase
|
||||
checked={false}
|
||||
key="feature.ttl90days"
|
||||
label="TTL 90 days"
|
||||
onChange={[Function]}
|
||||
/>
|
||||
<StyledCheckboxBase
|
||||
checked={false}
|
||||
key="feature.enablenotebooks"
|
||||
label="Enable notebooks"
|
||||
onChange={[Function]}
|
||||
/>
|
||||
<StyledCheckboxBase
|
||||
checked={false}
|
||||
disabled={true}
|
||||
key="feature.customportal"
|
||||
label="Force Production portal (portal only)"
|
||||
onChange={[Function]}
|
||||
/>
|
||||
<StyledCheckboxBase
|
||||
checked={false}
|
||||
key="feature.enablespark"
|
||||
label="Enable Synapse"
|
||||
onChange={[Function]}
|
||||
/>
|
||||
<StyledCheckboxBase
|
||||
checked={false}
|
||||
key="feature.enableautopilotv2"
|
||||
label="Enable Auto-pilot V2"
|
||||
onChange={[Function]}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Stack
|
||||
horizontalAlign="space-between"
|
||||
>
|
||||
<StyledTextFieldBase
|
||||
key="feature.notebookserverurl"
|
||||
label="Notebook server URL"
|
||||
onChange={[Function]}
|
||||
placeholder="https://notebookserver"
|
||||
styles={
|
||||
Object {
|
||||
"fieldGroup": Object {
|
||||
"width": 300,
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
<StyledTextFieldBase
|
||||
key="feature.notebookservertoken"
|
||||
label="Notebook server token"
|
||||
onChange={[Function]}
|
||||
placeholder=""
|
||||
styles={
|
||||
Object {
|
||||
"fieldGroup": Object {
|
||||
"width": 300,
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
<StyledTextFieldBase
|
||||
key="feature.notebookbasepath"
|
||||
label="Notebook base path"
|
||||
onChange={[Function]}
|
||||
placeholder=""
|
||||
styles={
|
||||
Object {
|
||||
"fieldGroup": Object {
|
||||
"width": 300,
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack
|
||||
horizontalAlign="space-between"
|
||||
>
|
||||
<StyledTextFieldBase
|
||||
key="key"
|
||||
label="Auth key"
|
||||
onChange={[Function]}
|
||||
placeholder=""
|
||||
styles={
|
||||
Object {
|
||||
"fieldGroup": Object {
|
||||
"width": 300,
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
<StyledTextFieldBase
|
||||
disabled={true}
|
||||
key="dataExplorerSource"
|
||||
label="Data Explorer Source (portal only)"
|
||||
onChange={[Function]}
|
||||
placeholder="https://localhost:1234/explorer.html"
|
||||
styles={
|
||||
Object {
|
||||
"fieldGroup": Object {
|
||||
"width": 300,
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
<StyledTextFieldBase
|
||||
key="feature.livyendpoint"
|
||||
label="Livy endpoint"
|
||||
onChange={[Function]}
|
||||
placeholder=""
|
||||
styles={
|
||||
Object {
|
||||
"fieldGroup": Object {
|
||||
"width": 300,
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</div>
|
||||
`;
|
||||
@@ -5,7 +5,7 @@ import * as Constants from "../../../Common/Constants";
|
||||
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import { RepoListItem } from "./GitHubReposComponent";
|
||||
import { ChildrenMargin } from "./GitHubStyleConstants";
|
||||
import { GitHubUtils } from "../../../Utils/GitHubUtils";
|
||||
import * as GitHubUtils from "../../../Utils/GitHubUtils";
|
||||
import { IGitHubRepo } from "../../../GitHub/GitHubClient";
|
||||
import TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import UrlUtility from "../../../Common/UrlUtility";
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
} from "office-ui-fabric-react";
|
||||
import * as React from "react";
|
||||
import { IGitHubBranch, IGitHubPageInfo } from "../../../GitHub/GitHubClient";
|
||||
import { GitHubUtils } from "../../../Utils/GitHubUtils";
|
||||
import * as GitHubUtils from "../../../Utils/GitHubUtils";
|
||||
import { RepoListItem } from "./GitHubReposComponent";
|
||||
import {
|
||||
BranchesDropdownCheckboxStyles,
|
||||
|
||||
@@ -4,4 +4,21 @@
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
line-height: 1;
|
||||
font-size: 14px;
|
||||
padding: 6px 12px;
|
||||
background: #fff;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 2px 0 0 2px;
|
||||
min-height: 25px;
|
||||
resize: vertical;
|
||||
|
||||
&:focus {
|
||||
border-color: #66afe9;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import React from "react";
|
||||
import { shallow } from "enzyme";
|
||||
import { InputTypeaheadComponent, InputTypeaheadComponentProps } from "./InputTypeaheadComponent";
|
||||
import "../../../../externals/jquery.typeahead.min.js";
|
||||
|
||||
describe("inputTypeahead", () => {
|
||||
it("renders <input />", () => {
|
||||
const props: InputTypeaheadComponentProps = {
|
||||
choices: [
|
||||
{ caption: "item1", value: "value1" },
|
||||
{ caption: "item2", value: "value2" }
|
||||
],
|
||||
placeholder: "placeholder",
|
||||
useTextarea: false
|
||||
};
|
||||
|
||||
const wrapper = shallow(<InputTypeaheadComponent {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders <textarea />", () => {
|
||||
const props: InputTypeaheadComponentProps = {
|
||||
choices: [
|
||||
{ caption: "item1", value: "value1" },
|
||||
{ caption: "item2", value: "value2" }
|
||||
],
|
||||
placeholder: "placeholder",
|
||||
useTextarea: true
|
||||
};
|
||||
|
||||
const wrapper = shallow(<InputTypeaheadComponent {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -7,6 +7,7 @@
|
||||
*
|
||||
*/
|
||||
import * as React from "react";
|
||||
import "./InputTypeahead.less";
|
||||
import { KeyCodes } from "../../../Common/Constants";
|
||||
|
||||
export interface Item {
|
||||
@@ -17,7 +18,7 @@ export interface Item {
|
||||
/**
|
||||
* Parameters for this component
|
||||
*/
|
||||
interface InputTypeaheadComponentProps {
|
||||
export interface InputTypeaheadComponentProps {
|
||||
/**
|
||||
* List of choices available in the dropdown.
|
||||
*/
|
||||
@@ -66,6 +67,11 @@ interface InputTypeaheadComponentProps {
|
||||
* true: show (X) button that clears the text inside the textbox when typing
|
||||
*/
|
||||
showCancelButton?: boolean;
|
||||
|
||||
/**
|
||||
* true: use <textarea /> instead of <input />
|
||||
*/
|
||||
useTextarea?: boolean;
|
||||
}
|
||||
|
||||
interface OnClickItem {
|
||||
@@ -135,6 +141,16 @@ export class InputTypeaheadComponent extends React.Component<
|
||||
<div className="typeahead__container" ref={input => (this.containerElt = input)}>
|
||||
<div className="typeahead__field">
|
||||
<span className="typeahead__query">
|
||||
{this.props.useTextarea ? (
|
||||
<textarea
|
||||
rows={1}
|
||||
name="q"
|
||||
autoComplete="off"
|
||||
aria-label="Input query"
|
||||
ref={input => (this.inputElt = input)}
|
||||
defaultValue={this.props.defaultValue}
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
name="q"
|
||||
type="search"
|
||||
@@ -143,6 +159,7 @@ export class InputTypeaheadComponent extends React.Component<
|
||||
ref={input => (this.inputElt = input)}
|
||||
defaultValue={this.props.defaultValue}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
{this.props.showSearchButton && (
|
||||
<span className="typeahead__button">
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`inputTypeahead renders <input /> 1`] = `
|
||||
<span
|
||||
className="input-typeahead-container"
|
||||
>
|
||||
<div
|
||||
className="input-typehead"
|
||||
onKeyDown={[Function]}
|
||||
>
|
||||
<div
|
||||
className="typeahead__container"
|
||||
>
|
||||
<div
|
||||
className="typeahead__field"
|
||||
>
|
||||
<span
|
||||
className="typeahead__query"
|
||||
>
|
||||
<input
|
||||
aria-label="Input query"
|
||||
autoComplete="off"
|
||||
name="q"
|
||||
type="search"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
`;
|
||||
|
||||
exports[`inputTypeahead renders <textarea /> 1`] = `
|
||||
<span
|
||||
className="input-typeahead-container"
|
||||
>
|
||||
<div
|
||||
className="input-typehead"
|
||||
onKeyDown={[Function]}
|
||||
>
|
||||
<div
|
||||
className="typeahead__container"
|
||||
>
|
||||
<div
|
||||
className="typeahead__field"
|
||||
>
|
||||
<span
|
||||
className="typeahead__query"
|
||||
>
|
||||
<textarea
|
||||
aria-label="Input query"
|
||||
autoComplete="off"
|
||||
name="q"
|
||||
rows={1}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
`;
|
||||
@@ -1,80 +0,0 @@
|
||||
/**
|
||||
* Message handler to communicate with NotebookApp iframe
|
||||
*/
|
||||
import Q from "q";
|
||||
import * as _ from "underscore";
|
||||
|
||||
import { HashMap } from "../../../Common/HashMap";
|
||||
import { CachedDataPromise } from "../../../Common/MessageHandler";
|
||||
import * as Constants from "../../../Common/Constants";
|
||||
import {
|
||||
MessageTypes,
|
||||
FromNotebookMessage,
|
||||
FromNotebookResponseMessage,
|
||||
FromDataExplorerMessage
|
||||
} from "../../../Terminal/NotebookAppContracts";
|
||||
|
||||
export class NotebookAppMessageHandler {
|
||||
private requestMap: HashMap<CachedDataPromise<any>>;
|
||||
|
||||
constructor(private targetIFrameWindow: Window) {
|
||||
this.requestMap = new HashMap();
|
||||
}
|
||||
|
||||
public handleCachedDataMessage(message: FromNotebookMessage): void {
|
||||
const messageContent = message && (message.message as FromNotebookResponseMessage);
|
||||
if (
|
||||
message == null ||
|
||||
messageContent == null ||
|
||||
messageContent.id == null ||
|
||||
!this.requestMap.has(messageContent.id)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cachedDataPromise = this.requestMap.get(messageContent.id);
|
||||
if (messageContent.error != null) {
|
||||
cachedDataPromise.deferred.reject(messageContent.error);
|
||||
} else {
|
||||
cachedDataPromise.deferred.resolve(messageContent.data);
|
||||
}
|
||||
this.runGarbageCollector();
|
||||
}
|
||||
|
||||
public sendCachedDataMessage<TResponseDataModel>(
|
||||
messageType: MessageTypes,
|
||||
params?: any
|
||||
): Q.Promise<TResponseDataModel> {
|
||||
let cachedDataPromise: CachedDataPromise<TResponseDataModel> = {
|
||||
deferred: Q.defer<TResponseDataModel>(),
|
||||
startTime: new Date(),
|
||||
id: _.uniqueId()
|
||||
};
|
||||
this.requestMap.set(cachedDataPromise.id, cachedDataPromise);
|
||||
this.sendMessage({ type: messageType, params: params, id: cachedDataPromise.id });
|
||||
|
||||
//TODO: Use telemetry to measure optimal time to resolve/reject promises
|
||||
return cachedDataPromise.deferred.promise.timeout(
|
||||
Constants.ClientDefaults.requestTimeoutMs,
|
||||
"Timed out while waiting for response from portal"
|
||||
);
|
||||
}
|
||||
|
||||
public sendMessage(data: FromDataExplorerMessage): void {
|
||||
if (!this.targetIFrameWindow) {
|
||||
console.error("targetIFrame not defined. This is not expected");
|
||||
return;
|
||||
}
|
||||
|
||||
this.targetIFrameWindow.postMessage(data, window.location.href || window.document.referrer);
|
||||
}
|
||||
|
||||
protected runGarbageCollector() {
|
||||
this.requestMap.keys().forEach((key: string) => {
|
||||
const promise: Q.Promise<any> = this.requestMap.get(key).deferred.promise;
|
||||
if (promise.isFulfilled() || promise.isRejected()) {
|
||||
this.requestMap.delete(key);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import * as React from "react";
|
||||
import * as DataModels from "../../../Contracts/DataModels";
|
||||
import { Logger } from "../../../Common/Logger";
|
||||
import * as Logger from "../../../Common/Logger";
|
||||
import { NotificationConsoleUtils } from "../../../Utils/NotificationConsoleUtils";
|
||||
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import { StringUtils } from "../../../Utils/StringUtils";
|
||||
|
||||
@@ -1,15 +1,32 @@
|
||||
import React from "react";
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import { GalleryCardComponent, GalleryCardComponentProps } from "./GalleryCardComponent";
|
||||
|
||||
describe("GalleryCardComponent", () => {
|
||||
it("renders", () => {
|
||||
const props: GalleryCardComponentProps = {
|
||||
name: "mycard",
|
||||
url: "url",
|
||||
notebookMetadata: undefined,
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
onClick: () => {}
|
||||
data: {
|
||||
id: "id",
|
||||
name: "name",
|
||||
description: "description",
|
||||
author: "author",
|
||||
thumbnailUrl: "thumbnailUrl",
|
||||
created: "created",
|
||||
gitSha: "gitSha",
|
||||
tags: ["tag"],
|
||||
isSample: false,
|
||||
downloads: 0,
|
||||
favorites: 0,
|
||||
views: 0
|
||||
},
|
||||
isFavorite: false,
|
||||
showDelete: true,
|
||||
onClick: undefined,
|
||||
onTagClick: undefined,
|
||||
onFavoriteClick: undefined,
|
||||
onUnfavoriteClick: undefined,
|
||||
onDownloadClick: undefined,
|
||||
onDeleteClick: undefined
|
||||
};
|
||||
|
||||
const wrapper = shallow(<GalleryCardComponent {...props} />);
|
||||
|
||||
@@ -1,65 +1,199 @@
|
||||
import * as React from "react";
|
||||
import * as DataModels from "../../../../Contracts/DataModels";
|
||||
import { Card, ICardTokens, ICardSectionTokens } from "@uifabric/react-cards";
|
||||
import { Icon, Image, Persona, Text } from "office-ui-fabric-react";
|
||||
import { Card, ICardTokens } from "@uifabric/react-cards";
|
||||
import {
|
||||
siteTextStyles,
|
||||
descriptionTextStyles,
|
||||
helpfulTextStyles,
|
||||
subtleHelpfulTextStyles,
|
||||
subtleIconStyles
|
||||
} from "./CardStyleConstants";
|
||||
FontWeights,
|
||||
Icon,
|
||||
IconButton,
|
||||
Image,
|
||||
ImageFit,
|
||||
Persona,
|
||||
Text,
|
||||
Link,
|
||||
BaseButton,
|
||||
Button,
|
||||
LinkBase,
|
||||
Separator,
|
||||
TooltipHost
|
||||
} from "office-ui-fabric-react";
|
||||
import * as React from "react";
|
||||
import { IGalleryItem } from "../../../../Juno/JunoClient";
|
||||
import { FileSystemUtil } from "../../../Notebook/FileSystemUtil";
|
||||
|
||||
export interface GalleryCardComponentProps {
|
||||
name: string;
|
||||
url: string;
|
||||
notebookMetadata: DataModels.NotebookMetadata;
|
||||
data: IGalleryItem;
|
||||
isFavorite: boolean;
|
||||
showDelete: boolean;
|
||||
onClick: () => void;
|
||||
onTagClick: (tag: string) => void;
|
||||
onFavoriteClick: () => void;
|
||||
onUnfavoriteClick: () => void;
|
||||
onDownloadClick: () => void;
|
||||
onDeleteClick: () => void;
|
||||
}
|
||||
|
||||
export class GalleryCardComponent extends React.Component<GalleryCardComponentProps> {
|
||||
private cardTokens: ICardTokens = { childrenMargin: 12 };
|
||||
private attendantsCardSectionTokens: ICardSectionTokens = { childrenGap: 6 };
|
||||
public static readonly CARD_HEIGHT = 384;
|
||||
public static readonly CARD_WIDTH = 256;
|
||||
|
||||
private static readonly cardImageHeight = 144;
|
||||
private static readonly cardDescriptionMaxChars = 88;
|
||||
private static readonly cardTokens: ICardTokens = {
|
||||
width: GalleryCardComponent.CARD_WIDTH,
|
||||
height: GalleryCardComponent.CARD_HEIGHT,
|
||||
childrenGap: 8,
|
||||
childrenMargin: 10
|
||||
};
|
||||
|
||||
public render(): JSX.Element {
|
||||
return this.props.notebookMetadata != null ? (
|
||||
<Card aria-label="Notebook Card" onClick={this.props.onClick} tokens={this.cardTokens}>
|
||||
const options: Intl.DateTimeFormatOptions = {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric"
|
||||
};
|
||||
|
||||
const dateString = new Date(this.props.data.created).toLocaleString("default", options);
|
||||
|
||||
return (
|
||||
<Card aria-label="Notebook Card" tokens={GalleryCardComponent.cardTokens} onClick={this.props.onClick}>
|
||||
<Card.Item>
|
||||
<Persona text={this.props.notebookMetadata.author} secondaryText={this.props.notebookMetadata.date} />
|
||||
<Persona text={this.props.data.author} secondaryText={dateString} />
|
||||
</Card.Item>
|
||||
|
||||
<Card.Item fill>
|
||||
<Image src={this.props.notebookMetadata.imageUrl} width="100%" alt="Notebook display image" />
|
||||
<Image
|
||||
src={
|
||||
this.props.data.thumbnailUrl ||
|
||||
`https://placehold.it/${GalleryCardComponent.CARD_WIDTH}x${GalleryCardComponent.cardImageHeight}`
|
||||
}
|
||||
width={GalleryCardComponent.CARD_WIDTH}
|
||||
height={GalleryCardComponent.cardImageHeight}
|
||||
imageFit={ImageFit.cover}
|
||||
alt="Notebook cover image"
|
||||
/>
|
||||
</Card.Item>
|
||||
|
||||
<Card.Section>
|
||||
<Text variant="small" styles={siteTextStyles}>
|
||||
{this.props.notebookMetadata.tags.join(", ")}
|
||||
<Text variant="small" nowrap>
|
||||
{this.props.data.tags?.map((tag, index, array) => (
|
||||
<span key={tag}>
|
||||
<Link onClick={(event): void => this.onTagClick(event, tag)}>{tag}</Link>
|
||||
{index === array.length - 1 ? <></> : ", "}
|
||||
</span>
|
||||
))}
|
||||
</Text>
|
||||
<Text styles={descriptionTextStyles}>{this.props.name}</Text>
|
||||
<Text variant="small" styles={helpfulTextStyles}>
|
||||
{this.props.notebookMetadata.description}
|
||||
<Text styles={{ root: { fontWeight: FontWeights.semibold } }} nowrap>
|
||||
{FileSystemUtil.stripExtension(this.props.data.name, "ipynb")}
|
||||
</Text>
|
||||
<Text variant="small" styles={{ root: { height: 36 } }}>
|
||||
{this.props.data.description.substr(0, GalleryCardComponent.cardDescriptionMaxChars)}
|
||||
</Text>
|
||||
</Card.Section>
|
||||
<Card.Section horizontal tokens={this.attendantsCardSectionTokens}>
|
||||
<Icon iconName="RedEye" styles={subtleIconStyles} />
|
||||
<Text variant="small" styles={subtleHelpfulTextStyles}>
|
||||
{this.props.notebookMetadata.views}
|
||||
|
||||
<Card.Section horizontal styles={{ root: { alignItems: "flex-end" } }}>
|
||||
<Text variant="tiny" styles={{ root: { color: "#ccc" } }}>
|
||||
<Icon iconName="RedEye" styles={{ root: { verticalAlign: "middle" } }} /> {this.props.data.views}
|
||||
</Text>
|
||||
<Icon iconName="Download" styles={subtleIconStyles} />
|
||||
<Text variant="small" styles={subtleHelpfulTextStyles}>
|
||||
{this.props.notebookMetadata.downloads}
|
||||
<Text variant="tiny" styles={{ root: { color: "#ccc" } }}>
|
||||
<Icon iconName="Download" styles={{ root: { verticalAlign: "middle" } }} /> {this.props.data.downloads}
|
||||
</Text>
|
||||
<Icon iconName="Heart" styles={subtleIconStyles} />
|
||||
<Text variant="small" styles={subtleHelpfulTextStyles}>
|
||||
{this.props.notebookMetadata.likes}
|
||||
<Text variant="tiny" styles={{ root: { color: "#ccc" } }}>
|
||||
<Icon iconName="Heart" styles={{ root: { verticalAlign: "middle" } }} /> {this.props.data.favorites}
|
||||
</Text>
|
||||
</Card.Section>
|
||||
</Card>
|
||||
) : (
|
||||
<Card aria-label="Notebook Card" onClick={this.props.onClick} tokens={this.cardTokens}>
|
||||
<Card.Section>
|
||||
<Text styles={descriptionTextStyles}>{this.props.name}</Text>
|
||||
|
||||
<Card.Item>
|
||||
<Separator styles={{ root: { padding: 0, height: 1 } }} />
|
||||
</Card.Item>
|
||||
|
||||
<Card.Section horizontal styles={{ root: { marginTop: 0 } }}>
|
||||
{this.generateIconButtonWithTooltip(
|
||||
this.props.isFavorite ? "HeartFill" : "Heart",
|
||||
this.props.isFavorite ? "Unlike" : "Like",
|
||||
this.props.isFavorite ? this.onUnfavoriteClick : this.onFavoriteClick
|
||||
)}
|
||||
|
||||
{this.generateIconButtonWithTooltip("Download", "Download", this.onDownloadClick)}
|
||||
|
||||
{this.props.showDelete && (
|
||||
<div style={{ width: "100%", textAlign: "right" }}>
|
||||
{this.generateIconButtonWithTooltip("Delete", "Remove", this.props.onDeleteClick)}
|
||||
</div>
|
||||
)}
|
||||
</Card.Section>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
* Fluent UI doesn't support tooltips on IconButtons out of the box. In the meantime the recommendation is
|
||||
* to do the following (from https://developer.microsoft.com/en-us/fluentui#/controls/web/button)
|
||||
*/
|
||||
private generateIconButtonWithTooltip = (
|
||||
iconName: string,
|
||||
title: string,
|
||||
onClick: (
|
||||
event: React.MouseEvent<
|
||||
HTMLAnchorElement | HTMLButtonElement | HTMLDivElement | BaseButton | Button | HTMLSpanElement,
|
||||
MouseEvent
|
||||
>
|
||||
) => void
|
||||
): JSX.Element => {
|
||||
return (
|
||||
<TooltipHost
|
||||
content={title}
|
||||
id={`TooltipHost-IconButton-${iconName}`}
|
||||
calloutProps={{ gapSpace: 0 }}
|
||||
styles={{ root: { display: "inline-block" } }}
|
||||
>
|
||||
<IconButton iconProps={{ iconName }} title={title} ariaLabel={title} onClick={onClick} />
|
||||
</TooltipHost>
|
||||
);
|
||||
};
|
||||
|
||||
private onTagClick = (
|
||||
event: React.MouseEvent<HTMLElement | HTMLAnchorElement | HTMLButtonElement | LinkBase, MouseEvent>,
|
||||
tag: string
|
||||
): void => {
|
||||
event.stopPropagation();
|
||||
this.props.onTagClick(tag);
|
||||
};
|
||||
|
||||
private onFavoriteClick = (
|
||||
event: React.MouseEvent<
|
||||
HTMLAnchorElement | HTMLButtonElement | HTMLDivElement | BaseButton | Button | HTMLSpanElement,
|
||||
MouseEvent
|
||||
>
|
||||
): void => {
|
||||
event.stopPropagation();
|
||||
this.props.onFavoriteClick();
|
||||
};
|
||||
|
||||
private onUnfavoriteClick = (
|
||||
event: React.MouseEvent<
|
||||
HTMLAnchorElement | HTMLButtonElement | HTMLDivElement | BaseButton | Button | HTMLSpanElement,
|
||||
MouseEvent
|
||||
>
|
||||
): void => {
|
||||
event.stopPropagation();
|
||||
this.props.onUnfavoriteClick();
|
||||
};
|
||||
|
||||
private onDownloadClick = (
|
||||
event: React.MouseEvent<
|
||||
HTMLAnchorElement | HTMLButtonElement | HTMLDivElement | BaseButton | Button | HTMLSpanElement,
|
||||
MouseEvent
|
||||
>
|
||||
): void => {
|
||||
event.stopPropagation();
|
||||
this.props.onDownloadClick();
|
||||
};
|
||||
|
||||
private onDeleteClick = (
|
||||
event: React.MouseEvent<
|
||||
HTMLAnchorElement | HTMLButtonElement | HTMLDivElement | BaseButton | Button | HTMLSpanElement,
|
||||
MouseEvent
|
||||
>
|
||||
): void => {
|
||||
event.stopPropagation();
|
||||
this.props.onDeleteClick();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,26 +3,263 @@
|
||||
exports[`GalleryCardComponent renders 1`] = `
|
||||
<Card
|
||||
aria-label="Notebook Card"
|
||||
onClick={[Function]}
|
||||
tokens={
|
||||
Object {
|
||||
"childrenMargin": 12,
|
||||
"childrenGap": 8,
|
||||
"childrenMargin": 10,
|
||||
"height": 384,
|
||||
"width": 256,
|
||||
}
|
||||
}
|
||||
>
|
||||
<CardItem>
|
||||
<StyledPersonaBase
|
||||
secondaryText="Invalid Date"
|
||||
text="author"
|
||||
/>
|
||||
</CardItem>
|
||||
<CardItem
|
||||
fill={true}
|
||||
>
|
||||
<StyledImageBase
|
||||
alt="Notebook cover image"
|
||||
height={144}
|
||||
imageFit={2}
|
||||
src="thumbnailUrl"
|
||||
width={256}
|
||||
/>
|
||||
</CardItem>
|
||||
<CardSection>
|
||||
<Text
|
||||
nowrap={true}
|
||||
variant="small"
|
||||
>
|
||||
<span
|
||||
key="tag"
|
||||
>
|
||||
<StyledLinkBase
|
||||
onClick={[Function]}
|
||||
>
|
||||
tag
|
||||
</StyledLinkBase>
|
||||
</span>
|
||||
</Text>
|
||||
<Text
|
||||
nowrap={true}
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "#333333",
|
||||
"fontWeight": 600,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
mycard
|
||||
name
|
||||
</Text>
|
||||
<Text
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"height": 36,
|
||||
},
|
||||
}
|
||||
}
|
||||
variant="small"
|
||||
>
|
||||
description
|
||||
</Text>
|
||||
</CardSection>
|
||||
<CardSection
|
||||
horizontal={true}
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"alignItems": "flex-end",
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "#ccc",
|
||||
},
|
||||
}
|
||||
}
|
||||
variant="tiny"
|
||||
>
|
||||
<StyledIconBase
|
||||
iconName="RedEye"
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"verticalAlign": "middle",
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
|
||||
0
|
||||
</Text>
|
||||
<Text
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "#ccc",
|
||||
},
|
||||
}
|
||||
}
|
||||
variant="tiny"
|
||||
>
|
||||
<StyledIconBase
|
||||
iconName="Download"
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"verticalAlign": "middle",
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
|
||||
0
|
||||
</Text>
|
||||
<Text
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "#ccc",
|
||||
},
|
||||
}
|
||||
}
|
||||
variant="tiny"
|
||||
>
|
||||
<StyledIconBase
|
||||
iconName="Heart"
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"verticalAlign": "middle",
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
|
||||
0
|
||||
</Text>
|
||||
</CardSection>
|
||||
<CardItem>
|
||||
<Styled
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"height": 1,
|
||||
"padding": 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
</CardItem>
|
||||
<CardSection
|
||||
horizontal={true}
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"marginTop": 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<StyledTooltipHostBase
|
||||
calloutProps={
|
||||
Object {
|
||||
"gapSpace": 0,
|
||||
}
|
||||
}
|
||||
content="Like"
|
||||
id="TooltipHost-IconButton-Heart"
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"display": "inline-block",
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<CustomizedIconButton
|
||||
ariaLabel="Like"
|
||||
iconProps={
|
||||
Object {
|
||||
"iconName": "Heart",
|
||||
}
|
||||
}
|
||||
onClick={[Function]}
|
||||
title="Like"
|
||||
/>
|
||||
</StyledTooltipHostBase>
|
||||
<StyledTooltipHostBase
|
||||
calloutProps={
|
||||
Object {
|
||||
"gapSpace": 0,
|
||||
}
|
||||
}
|
||||
content="Download"
|
||||
id="TooltipHost-IconButton-Download"
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"display": "inline-block",
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<CustomizedIconButton
|
||||
ariaLabel="Download"
|
||||
iconProps={
|
||||
Object {
|
||||
"iconName": "Download",
|
||||
}
|
||||
}
|
||||
onClick={[Function]}
|
||||
title="Download"
|
||||
/>
|
||||
</StyledTooltipHostBase>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"textAlign": "right",
|
||||
"width": "100%",
|
||||
}
|
||||
}
|
||||
>
|
||||
<StyledTooltipHostBase
|
||||
calloutProps={
|
||||
Object {
|
||||
"gapSpace": 0,
|
||||
}
|
||||
}
|
||||
content="Remove"
|
||||
id="TooltipHost-IconButton-Delete"
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"display": "inline-block",
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<CustomizedIconButton
|
||||
ariaLabel="Remove"
|
||||
iconProps={
|
||||
Object {
|
||||
"iconName": "Delete",
|
||||
}
|
||||
}
|
||||
title="Remove"
|
||||
/>
|
||||
</StyledTooltipHostBase>
|
||||
</div>
|
||||
</CardSection>
|
||||
</Card>
|
||||
`;
|
||||
|
||||
@@ -1,74 +1,17 @@
|
||||
import React from "react";
|
||||
import { shallow } from "enzyme";
|
||||
import {
|
||||
GalleryViewerContainerComponent,
|
||||
GalleryViewerContainerComponentProps,
|
||||
FullWidthTabs,
|
||||
FullWidthTabsProps,
|
||||
GalleryCardsComponent,
|
||||
GalleryCardsComponentProps,
|
||||
GalleryViewerComponent,
|
||||
GalleryViewerComponentProps
|
||||
} from "./GalleryViewerComponent";
|
||||
import * as DataModels from "../../../Contracts/DataModels";
|
||||
import React from "react";
|
||||
import { GalleryViewerComponent, GalleryViewerComponentProps, GalleryTab, SortBy } from "./GalleryViewerComponent";
|
||||
|
||||
describe("GalleryCardsComponent", () => {
|
||||
it("renders", () => {
|
||||
// TODO Mock this
|
||||
const props: GalleryCardsComponentProps = {
|
||||
data: [],
|
||||
userMetadata: undefined,
|
||||
onNotebookMetadataChange: (officialSamplesIndex: number, notebookMetadata: DataModels.NotebookMetadata) =>
|
||||
Promise.resolve(),
|
||||
onClick: (
|
||||
url: string,
|
||||
notebookMetadata: DataModels.NotebookMetadata,
|
||||
onNotebookMetadataChange: (newNotebookMetadata: DataModels.NotebookMetadata) => Promise<void>,
|
||||
isLikedNotebook: boolean
|
||||
) => Promise.resolve()
|
||||
};
|
||||
|
||||
const wrapper = shallow(<GalleryCardsComponent {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("FullWidthTabs", () => {
|
||||
it("renders", () => {
|
||||
const props: FullWidthTabsProps = {
|
||||
officialSamplesContent: [],
|
||||
likedNotebooksContent: [],
|
||||
userMetadata: undefined,
|
||||
onClick: (
|
||||
url: string,
|
||||
notebookMetadata: DataModels.NotebookMetadata,
|
||||
onNotebookMetadataChange: (newNotebookMetadata: DataModels.NotebookMetadata) => Promise<void>,
|
||||
isLikedNotebook: boolean
|
||||
) => Promise.resolve()
|
||||
};
|
||||
|
||||
const wrapper = shallow(<FullWidthTabs {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("GalleryViewerContainerComponent", () => {
|
||||
it("renders", () => {
|
||||
const props: GalleryViewerContainerComponentProps = {
|
||||
container: undefined
|
||||
};
|
||||
|
||||
const wrapper = shallow(<GalleryViewerContainerComponent {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("GalleryCardComponent", () => {
|
||||
describe("GalleryViewerComponent", () => {
|
||||
it("renders", () => {
|
||||
const props: GalleryViewerComponentProps = {
|
||||
container: undefined,
|
||||
officialSamplesData: [],
|
||||
likedNotebookData: undefined
|
||||
junoClient: undefined,
|
||||
selectedTab: GalleryTab.OfficialSamples,
|
||||
sortBy: SortBy.MostViewed,
|
||||
searchText: undefined,
|
||||
onSelectedTabChange: undefined,
|
||||
onSortByChange: undefined,
|
||||
onSearchTextChange: undefined
|
||||
};
|
||||
|
||||
const wrapper = shallow(<GalleryViewerComponent {...props} />);
|
||||
|
||||
@@ -1,356 +1,513 @@
|
||||
/**
|
||||
* Gallery Viewer
|
||||
*/
|
||||
|
||||
import {
|
||||
Dropdown,
|
||||
FocusZone,
|
||||
IDropdownOption,
|
||||
IPageSpecification,
|
||||
IPivotItemProps,
|
||||
IPivotProps,
|
||||
IRectangle,
|
||||
Label,
|
||||
List,
|
||||
Pivot,
|
||||
PivotItem,
|
||||
SearchBox,
|
||||
Stack
|
||||
} from "office-ui-fabric-react";
|
||||
import * as React from "react";
|
||||
import * as DataModels from "../../../Contracts/DataModels";
|
||||
import * as Logger from "../../../Common/Logger";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { GalleryCardComponent } from "./Cards/GalleryCardComponent";
|
||||
import { Stack, IStackTokens } from "office-ui-fabric-react";
|
||||
import { JunoUtils } from "../../../Utils/JunoUtils";
|
||||
import { CosmosClient } from "../../../Common/CosmosClient";
|
||||
import { config } from "../../../Config";
|
||||
import path from "path";
|
||||
import { SessionStorageUtility, StorageKey } from "../../../Shared/StorageUtility";
|
||||
import { IGalleryItem, JunoClient } from "../../../Juno/JunoClient";
|
||||
import * as GalleryUtils from "../../../Utils/GalleryUtils";
|
||||
import { NotificationConsoleUtils } from "../../../Utils/NotificationConsoleUtils";
|
||||
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import * as TabComponent from "../Tabs/TabComponent";
|
||||
|
||||
import { DialogComponent, DialogProps } from "../DialogReactComponent/DialogComponent";
|
||||
import { GalleryCardComponent, GalleryCardComponentProps } from "./Cards/GalleryCardComponent";
|
||||
import "./GalleryViewerComponent.less";
|
||||
import { HttpStatusCodes } from "../../../Common/Constants";
|
||||
|
||||
export interface GalleryCardsComponentProps {
|
||||
data: DataModels.GitHubInfoJunoResponse[];
|
||||
userMetadata: DataModels.UserMetadata;
|
||||
onNotebookMetadataChange: (
|
||||
officialSamplesIndex: number,
|
||||
notebookMetadata: DataModels.NotebookMetadata
|
||||
) => Promise<void>;
|
||||
onClick: (
|
||||
url: string,
|
||||
notebookMetadata: DataModels.NotebookMetadata,
|
||||
onNotebookMetadataChange: (newNotebookMetadata: DataModels.NotebookMetadata) => Promise<void>,
|
||||
isLikedNotebook: boolean
|
||||
) => Promise<void>;
|
||||
export interface GalleryViewerComponentProps {
|
||||
container?: ViewModels.Explorer;
|
||||
junoClient: JunoClient;
|
||||
selectedTab: GalleryTab;
|
||||
sortBy: SortBy;
|
||||
searchText: string;
|
||||
onSelectedTabChange: (newTab: GalleryTab) => void;
|
||||
onSortByChange: (sortBy: SortBy) => void;
|
||||
onSearchTextChange: (searchText: string) => void;
|
||||
}
|
||||
|
||||
export class GalleryCardsComponent extends React.Component<GalleryCardsComponentProps> {
|
||||
private sectionStackTokens: IStackTokens = { childrenGap: 30 };
|
||||
export enum GalleryTab {
|
||||
OfficialSamples,
|
||||
PublicGallery,
|
||||
Favorites,
|
||||
Published
|
||||
}
|
||||
|
||||
export enum SortBy {
|
||||
MostViewed,
|
||||
MostDownloaded,
|
||||
MostFavorited,
|
||||
MostRecent
|
||||
}
|
||||
|
||||
interface GalleryViewerComponentState {
|
||||
sampleNotebooks: IGalleryItem[];
|
||||
publicNotebooks: IGalleryItem[];
|
||||
favoriteNotebooks: IGalleryItem[];
|
||||
publishedNotebooks: IGalleryItem[];
|
||||
selectedTab: GalleryTab;
|
||||
sortBy: SortBy;
|
||||
searchText: string;
|
||||
dialogProps: DialogProps;
|
||||
}
|
||||
|
||||
interface GalleryTabInfo {
|
||||
tab: GalleryTab;
|
||||
content: JSX.Element;
|
||||
}
|
||||
|
||||
export class GalleryViewerComponent extends React.Component<GalleryViewerComponentProps, GalleryViewerComponentState>
|
||||
implements GalleryUtils.DialogEnabledComponent {
|
||||
public static readonly OfficialSamplesTitle = "Official samples";
|
||||
public static readonly PublicGalleryTitle = "Public gallery";
|
||||
public static readonly FavoritesTitle = "Liked";
|
||||
public static readonly PublishedTitle = "Your published work";
|
||||
|
||||
private static readonly mostViewedText = "Most viewed";
|
||||
private static readonly mostDownloadedText = "Most downloaded";
|
||||
private static readonly mostFavoritedText = "Most favorited";
|
||||
private static readonly mostRecentText = "Most recent";
|
||||
|
||||
private static readonly sortingOptions: IDropdownOption[] = [
|
||||
{
|
||||
key: SortBy.MostViewed,
|
||||
text: GalleryViewerComponent.mostViewedText
|
||||
},
|
||||
{
|
||||
key: SortBy.MostDownloaded,
|
||||
text: GalleryViewerComponent.mostDownloadedText
|
||||
},
|
||||
{
|
||||
key: SortBy.MostFavorited,
|
||||
text: GalleryViewerComponent.mostFavoritedText
|
||||
},
|
||||
{
|
||||
key: SortBy.MostRecent,
|
||||
text: GalleryViewerComponent.mostRecentText
|
||||
}
|
||||
];
|
||||
|
||||
private sampleNotebooks: IGalleryItem[];
|
||||
private publicNotebooks: IGalleryItem[];
|
||||
private favoriteNotebooks: IGalleryItem[];
|
||||
private publishedNotebooks: IGalleryItem[];
|
||||
private columnCount: number;
|
||||
private rowCount: number;
|
||||
|
||||
constructor(props: GalleryViewerComponentProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
sampleNotebooks: undefined,
|
||||
publicNotebooks: undefined,
|
||||
favoriteNotebooks: undefined,
|
||||
publishedNotebooks: undefined,
|
||||
selectedTab: props.selectedTab,
|
||||
sortBy: props.sortBy,
|
||||
searchText: props.searchText,
|
||||
dialogProps: undefined
|
||||
};
|
||||
|
||||
this.loadTabContent(this.state.selectedTab, this.state.searchText, this.state.sortBy, false);
|
||||
if (this.props.container) {
|
||||
this.loadFavoriteNotebooks(this.state.searchText, this.state.sortBy, false); // Need this to show correct favorite button state
|
||||
}
|
||||
}
|
||||
|
||||
setDialogProps = (dialogProps: DialogProps): void => {
|
||||
this.setState({ dialogProps });
|
||||
};
|
||||
|
||||
public render(): JSX.Element {
|
||||
return (
|
||||
<Stack horizontal wrap tokens={this.sectionStackTokens}>
|
||||
{this.props.data.map((githubInfo: DataModels.GitHubInfoJunoResponse, index: any) => {
|
||||
const name = githubInfo.name;
|
||||
const url = githubInfo.downloadUrl;
|
||||
const notebookMetadata = githubInfo.metadata || {
|
||||
date: "2008-12-01",
|
||||
description: "Great notebook",
|
||||
tags: ["favorite", "sample"],
|
||||
author: "Laurent Nguyen",
|
||||
views: 432,
|
||||
likes: 123,
|
||||
downloads: 56,
|
||||
imageUrl:
|
||||
"https://media.magazine.ferrari.com/images/2019/02/27/170304506-c1bcf028-b513-45f6-9f27-0cadac619c3d.jpg"
|
||||
const tabs: GalleryTabInfo[] = [this.createTab(GalleryTab.OfficialSamples, this.state.sampleNotebooks)];
|
||||
|
||||
if (this.props.container) {
|
||||
if (this.props.container.isGalleryPublishEnabled()) {
|
||||
tabs.push(this.createTab(GalleryTab.PublicGallery, this.state.publicNotebooks));
|
||||
}
|
||||
|
||||
tabs.push(this.createTab(GalleryTab.Favorites, this.state.favoriteNotebooks));
|
||||
|
||||
if (this.props.container.isGalleryPublishEnabled()) {
|
||||
tabs.push(this.createTab(GalleryTab.Published, this.state.publishedNotebooks));
|
||||
}
|
||||
}
|
||||
|
||||
const pivotProps: IPivotProps = {
|
||||
onLinkClick: this.onPivotChange,
|
||||
selectedKey: GalleryTab[this.state.selectedTab]
|
||||
};
|
||||
|
||||
const pivotItems = tabs.map(tab => {
|
||||
const pivotItemProps: IPivotItemProps = {
|
||||
itemKey: GalleryTab[tab.tab],
|
||||
style: { marginTop: 20 },
|
||||
headerText: GalleryUtils.getTabTitle(tab.tab)
|
||||
};
|
||||
const officialSamplesIndex = githubInfo.officialSamplesIndex;
|
||||
const isLikedNotebook = githubInfo.isLikedNotebook;
|
||||
const updateTabsStatePerNotebook = this.props.onNotebookMetadataChange
|
||||
? (notebookMetadata: DataModels.NotebookMetadata) =>
|
||||
this.props.onNotebookMetadataChange(officialSamplesIndex, notebookMetadata)
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
name !== ".gitignore" &&
|
||||
url && (
|
||||
<GalleryCardComponent
|
||||
key={url}
|
||||
name={name}
|
||||
url={url}
|
||||
notebookMetadata={notebookMetadata}
|
||||
onClick={() => this.props.onClick(url, notebookMetadata, updateTabsStatePerNotebook, isLikedNotebook)}
|
||||
/>
|
||||
)
|
||||
<PivotItem key={pivotItemProps.itemKey} {...pivotItemProps}>
|
||||
{tab.content}
|
||||
</PivotItem>
|
||||
);
|
||||
})}
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="galleryContainer">
|
||||
<Pivot {...pivotProps}>{pivotItems}</Pivot>
|
||||
|
||||
{this.state.dialogProps && <DialogComponent {...this.state.dialogProps} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private createTab(tab: GalleryTab, data: IGalleryItem[]): GalleryTabInfo {
|
||||
return {
|
||||
tab,
|
||||
content: this.createTabContent(data)
|
||||
};
|
||||
}
|
||||
|
||||
private createTabContent(data: IGalleryItem[]): JSX.Element {
|
||||
return (
|
||||
<Stack tokens={{ childrenGap: 20 }}>
|
||||
<Stack horizontal tokens={{ childrenGap: 20 }}>
|
||||
<Stack.Item grow>
|
||||
<SearchBox value={this.state.searchText} placeholder="Search" onChange={this.onSearchBoxChange} />
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Label>Sort by</Label>
|
||||
</Stack.Item>
|
||||
<Stack.Item styles={{ root: { minWidth: 200 } }}>
|
||||
<Dropdown
|
||||
options={GalleryViewerComponent.sortingOptions}
|
||||
selectedKey={this.state.sortBy}
|
||||
onChange={this.onDropdownChange}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
|
||||
{data && this.createCardsTabContent(data)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export interface FullWidthTabsProps {
|
||||
officialSamplesContent: DataModels.GitHubInfoJunoResponse[];
|
||||
likedNotebooksContent: DataModels.GitHubInfoJunoResponse[];
|
||||
userMetadata: DataModels.UserMetadata;
|
||||
onClick: (
|
||||
url: string,
|
||||
notebookMetadata: DataModels.NotebookMetadata,
|
||||
onNotebookMetadataChange: (newNotebookMetadata: DataModels.NotebookMetadata) => Promise<void>,
|
||||
isLikedNotebook: boolean
|
||||
) => Promise<void>;
|
||||
}
|
||||
|
||||
interface FullWidthTabsState {
|
||||
activeTabIndex: number;
|
||||
officialSamplesContent: DataModels.GitHubInfoJunoResponse[];
|
||||
likedNotebooksContent: DataModels.GitHubInfoJunoResponse[];
|
||||
userMetadata: DataModels.UserMetadata;
|
||||
}
|
||||
|
||||
export class FullWidthTabs extends React.Component<FullWidthTabsProps, FullWidthTabsState> {
|
||||
private authorizationToken = CosmosClient.authorizationToken();
|
||||
private appTabs: TabComponent.Tab[];
|
||||
|
||||
constructor(props: FullWidthTabsProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
activeTabIndex: 0,
|
||||
officialSamplesContent: this.props.officialSamplesContent,
|
||||
likedNotebooksContent: this.props.likedNotebooksContent,
|
||||
userMetadata: this.props.userMetadata
|
||||
};
|
||||
|
||||
this.appTabs = [
|
||||
{
|
||||
title: "Official Samples",
|
||||
content: {
|
||||
className: "",
|
||||
render: () => (
|
||||
<GalleryCardsComponent
|
||||
data={this.state.officialSamplesContent}
|
||||
onClick={this.props.onClick}
|
||||
userMetadata={this.state.userMetadata}
|
||||
onNotebookMetadataChange={this.updateTabsState}
|
||||
/>
|
||||
)
|
||||
},
|
||||
isVisible: () => true
|
||||
},
|
||||
{
|
||||
title: "Liked Notebooks",
|
||||
content: {
|
||||
className: "",
|
||||
render: () => (
|
||||
<GalleryCardsComponent
|
||||
data={this.state.likedNotebooksContent}
|
||||
onClick={this.props.onClick}
|
||||
userMetadata={this.state.userMetadata}
|
||||
onNotebookMetadataChange={this.updateTabsState}
|
||||
/>
|
||||
)
|
||||
},
|
||||
isVisible: () => true
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
public updateTabsState = async (officialSamplesIndex: number, notebookMetadata: DataModels.NotebookMetadata) => {
|
||||
let currentLikedNotebooksContent = [...this.state.likedNotebooksContent];
|
||||
let currentUserMetadata = { ...this.state.userMetadata };
|
||||
let currentLikedNotebooks = [...currentUserMetadata.likedNotebooks];
|
||||
|
||||
const currentOfficialSamplesContent = [...this.state.officialSamplesContent];
|
||||
const currentOfficialSamplesObject = { ...currentOfficialSamplesContent[officialSamplesIndex] };
|
||||
const metadata = { ...currentOfficialSamplesObject.metadata };
|
||||
const metadataLikesUpdates = metadata.likes - notebookMetadata.likes;
|
||||
|
||||
metadata.views = notebookMetadata.views;
|
||||
metadata.downloads = notebookMetadata.downloads;
|
||||
metadata.likes = notebookMetadata.likes;
|
||||
currentOfficialSamplesObject.metadata = metadata;
|
||||
|
||||
// Notebook has been liked. Add To likedNotebooksContent, update isLikedNotebook flag
|
||||
if (metadataLikesUpdates < 0) {
|
||||
currentOfficialSamplesObject.isLikedNotebook = true;
|
||||
currentLikedNotebooksContent = currentLikedNotebooksContent.concat(currentOfficialSamplesObject);
|
||||
currentLikedNotebooks = currentLikedNotebooks.concat(currentOfficialSamplesObject.path);
|
||||
currentUserMetadata = { likedNotebooks: currentLikedNotebooks };
|
||||
} else if (metadataLikesUpdates > 0) {
|
||||
// Notebook has been unliked. Remove from likedNotebooksContent after matching the path, update isLikedNotebook flag
|
||||
|
||||
currentOfficialSamplesObject.isLikedNotebook = false;
|
||||
const likedNotebookIndex = currentLikedNotebooks.findIndex((path: string) => {
|
||||
return path === currentOfficialSamplesObject.path;
|
||||
});
|
||||
currentLikedNotebooksContent.splice(likedNotebookIndex, 1);
|
||||
currentLikedNotebooks.splice(likedNotebookIndex, 1);
|
||||
currentUserMetadata = { likedNotebooks: currentLikedNotebooks };
|
||||
}
|
||||
|
||||
currentOfficialSamplesContent[officialSamplesIndex] = currentOfficialSamplesObject;
|
||||
|
||||
this.setState({
|
||||
activeTabIndex: 0,
|
||||
userMetadata: currentUserMetadata,
|
||||
likedNotebooksContent: currentLikedNotebooksContent,
|
||||
officialSamplesContent: currentOfficialSamplesContent
|
||||
});
|
||||
|
||||
JunoUtils.updateNotebookMetadata(this.authorizationToken, notebookMetadata).then(
|
||||
async returnedNotebookMetadata => {
|
||||
if (metadataLikesUpdates !== 0) {
|
||||
JunoUtils.updateUserMetadata(this.authorizationToken, currentUserMetadata);
|
||||
// TODO: update state here?
|
||||
}
|
||||
},
|
||||
error => {
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Error updating notebook metadata: ${JSON.stringify(error)}`
|
||||
);
|
||||
// TODO add telemetry
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
private onTabIndexChange = (activeTabIndex: number) => this.setState({ activeTabIndex });
|
||||
|
||||
public render() {
|
||||
private createCardsTabContent(data: IGalleryItem[]): JSX.Element {
|
||||
return (
|
||||
<TabComponent.TabComponent
|
||||
tabs={this.appTabs}
|
||||
onTabIndexChange={this.onTabIndexChange.bind(this)}
|
||||
currentTabIndex={this.state.activeTabIndex}
|
||||
hideHeader={false}
|
||||
<FocusZone>
|
||||
<List
|
||||
items={data}
|
||||
getPageSpecification={this.getPageSpecification}
|
||||
renderedWindowsAhead={3}
|
||||
onRenderCell={this.onRenderCell}
|
||||
/>
|
||||
</FocusZone>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export interface GalleryViewerContainerComponentProps {
|
||||
container: ViewModels.Explorer;
|
||||
}
|
||||
private loadTabContent(tab: GalleryTab, searchText: string, sortBy: SortBy, offline: boolean): void {
|
||||
switch (tab) {
|
||||
case GalleryTab.OfficialSamples:
|
||||
this.loadSampleNotebooks(searchText, sortBy, offline);
|
||||
break;
|
||||
|
||||
interface GalleryViewerContainerComponentState {
|
||||
officialSamplesData: DataModels.GitHubInfoJunoResponse[];
|
||||
likedNotebooksData: DataModels.LikedNotebooksJunoResponse;
|
||||
}
|
||||
case GalleryTab.PublicGallery:
|
||||
this.loadPublicNotebooks(searchText, sortBy, offline);
|
||||
break;
|
||||
|
||||
export class GalleryViewerContainerComponent extends React.Component<
|
||||
GalleryViewerContainerComponentProps,
|
||||
GalleryViewerContainerComponentState
|
||||
> {
|
||||
constructor(props: GalleryViewerContainerComponentProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
officialSamplesData: undefined,
|
||||
likedNotebooksData: undefined
|
||||
};
|
||||
case GalleryTab.Favorites:
|
||||
this.loadFavoriteNotebooks(searchText, sortBy, offline);
|
||||
break;
|
||||
|
||||
case GalleryTab.Published:
|
||||
this.loadPublishedNotebooks(searchText, sortBy, offline);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown tab ${tab}`);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const authToken = CosmosClient.authorizationToken();
|
||||
JunoUtils.getOfficialSampleNotebooks(authToken).then(
|
||||
(data1: DataModels.GitHubInfoJunoResponse[]) => {
|
||||
const officialSamplesData = data1;
|
||||
|
||||
JunoUtils.getLikedNotebooks(authToken).then(
|
||||
(data2: DataModels.LikedNotebooksJunoResponse) => {
|
||||
const likedNotebooksData = data2;
|
||||
|
||||
officialSamplesData.map((value: DataModels.GitHubInfoJunoResponse, index: number) => {
|
||||
value.officialSamplesIndex = index;
|
||||
value.isLikedNotebook = likedNotebooksData.userMetadata.likedNotebooks.includes(value.path);
|
||||
});
|
||||
|
||||
likedNotebooksData.likedNotebooksContent.map((value: DataModels.GitHubInfoJunoResponse) => {
|
||||
value.isLikedNotebook = true;
|
||||
value.officialSamplesIndex = officialSamplesData.findIndex(
|
||||
(officialSample: DataModels.GitHubInfoJunoResponse) => {
|
||||
return officialSample.path === value.path;
|
||||
private async loadSampleNotebooks(searchText: string, sortBy: SortBy, offline: boolean): Promise<void> {
|
||||
if (!offline) {
|
||||
try {
|
||||
const response = await this.props.junoClient.getSampleNotebooks();
|
||||
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
|
||||
throw new Error(`Received HTTP ${response.status} when loading sample notebooks`);
|
||||
}
|
||||
|
||||
this.sampleNotebooks = response.data;
|
||||
} catch (error) {
|
||||
const message = `Failed to load sample notebooks: ${error}`;
|
||||
Logger.logError(message, "GalleryViewerComponent/loadSampleNotebooks");
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
this.setState({
|
||||
officialSamplesData: officialSamplesData,
|
||||
likedNotebooksData: likedNotebooksData
|
||||
sampleNotebooks: this.sampleNotebooks && [...this.sort(sortBy, this.search(searchText, this.sampleNotebooks))]
|
||||
});
|
||||
},
|
||||
error => {
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Error fetching liked notebooks: ${JSON.stringify(error)}`
|
||||
);
|
||||
// TODO Add telemetry
|
||||
}
|
||||
);
|
||||
},
|
||||
error => {
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Error fetching sample notebooks: ${JSON.stringify(error)}`
|
||||
);
|
||||
// TODO Add telemetry
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
return this.state.officialSamplesData && this.state.likedNotebooksData ? (
|
||||
<GalleryViewerComponent
|
||||
container={this.props.container}
|
||||
officialSamplesData={this.state.officialSamplesData}
|
||||
likedNotebookData={this.state.likedNotebooksData}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
private async loadPublicNotebooks(searchText: string, sortBy: SortBy, offline: boolean): Promise<void> {
|
||||
if (!offline) {
|
||||
try {
|
||||
const response = await this.props.junoClient.getPublicNotebooks();
|
||||
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
|
||||
throw new Error(`Received HTTP ${response.status} when loading public notebooks`);
|
||||
}
|
||||
}
|
||||
|
||||
export interface GalleryViewerComponentProps {
|
||||
container: ViewModels.Explorer;
|
||||
officialSamplesData: DataModels.GitHubInfoJunoResponse[];
|
||||
likedNotebookData: DataModels.LikedNotebooksJunoResponse;
|
||||
}
|
||||
this.publicNotebooks = response.data;
|
||||
} catch (error) {
|
||||
const message = `Failed to load public notebooks: ${error}`;
|
||||
Logger.logError(message, "GalleryViewerComponent/loadPublicNotebooks");
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message);
|
||||
}
|
||||
}
|
||||
|
||||
export class GalleryViewerComponent extends React.Component<GalleryViewerComponentProps> {
|
||||
public render(): JSX.Element {
|
||||
return this.props.container ? (
|
||||
<div className="galleryContainer">
|
||||
<FullWidthTabs
|
||||
officialSamplesContent={this.props.officialSamplesData}
|
||||
likedNotebooksContent={this.props.likedNotebookData.likedNotebooksContent}
|
||||
userMetadata={this.props.likedNotebookData.userMetadata}
|
||||
onClick={this.openNotebookViewer}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="galleryContainer">
|
||||
<GalleryCardsComponent
|
||||
data={this.props.officialSamplesData}
|
||||
onClick={this.openNotebookViewer}
|
||||
userMetadata={undefined}
|
||||
onNotebookMetadataChange={undefined}
|
||||
/>
|
||||
this.setState({
|
||||
publicNotebooks: this.publicNotebooks && [...this.sort(sortBy, this.search(searchText, this.publicNotebooks))]
|
||||
});
|
||||
}
|
||||
|
||||
private async loadFavoriteNotebooks(searchText: string, sortBy: SortBy, offline: boolean): Promise<void> {
|
||||
if (!offline) {
|
||||
try {
|
||||
const response = await this.props.junoClient.getFavoriteNotebooks();
|
||||
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
|
||||
throw new Error(`Received HTTP ${response.status} when loading favorite notebooks`);
|
||||
}
|
||||
|
||||
this.favoriteNotebooks = response.data;
|
||||
} catch (error) {
|
||||
const message = `Failed to load favorite notebooks: ${error}`;
|
||||
Logger.logError(message, "GalleryViewerComponent/loadFavoriteNotebooks");
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message);
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
favoriteNotebooks: this.favoriteNotebooks && [
|
||||
...this.sort(sortBy, this.search(searchText, this.favoriteNotebooks))
|
||||
]
|
||||
});
|
||||
|
||||
// Refresh favorite button state
|
||||
if (this.state.selectedTab !== GalleryTab.Favorites) {
|
||||
this.refreshSelectedTab();
|
||||
}
|
||||
}
|
||||
|
||||
private async loadPublishedNotebooks(searchText: string, sortBy: SortBy, offline: boolean): Promise<void> {
|
||||
if (!offline) {
|
||||
try {
|
||||
const response = await this.props.junoClient.getPublishedNotebooks();
|
||||
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
|
||||
throw new Error(`Received HTTP ${response.status} when loading published notebooks`);
|
||||
}
|
||||
|
||||
this.publishedNotebooks = response.data;
|
||||
} catch (error) {
|
||||
const message = `Failed to load published notebooks: ${error}`;
|
||||
Logger.logError(message, "GalleryViewerComponent/loadPublishedNotebooks");
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message);
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
publishedNotebooks: this.publishedNotebooks && [
|
||||
...this.sort(sortBy, this.search(searchText, this.publishedNotebooks))
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
private search(searchText: string, data: IGalleryItem[]): IGalleryItem[] {
|
||||
if (searchText) {
|
||||
return data?.filter(item => this.isGalleryItemPresent(searchText, item));
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
private isGalleryItemPresent(searchText: string, item: IGalleryItem): boolean {
|
||||
const toSearch = searchText.trim().toUpperCase();
|
||||
const searchData: string[] = [
|
||||
item.author.toUpperCase(),
|
||||
item.description.toUpperCase(),
|
||||
item.name.toUpperCase(),
|
||||
...item.tags?.map(tag => tag.toUpperCase())
|
||||
];
|
||||
|
||||
for (const data of searchData) {
|
||||
if (data?.indexOf(toSearch) !== -1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private sort(sortBy: SortBy, data: IGalleryItem[]): IGalleryItem[] {
|
||||
return data?.sort((a, b) => {
|
||||
switch (sortBy) {
|
||||
case SortBy.MostViewed:
|
||||
return b.views - a.views;
|
||||
case SortBy.MostDownloaded:
|
||||
return b.downloads - a.downloads;
|
||||
case SortBy.MostFavorited:
|
||||
return b.favorites - a.favorites;
|
||||
case SortBy.MostRecent:
|
||||
return Date.parse(b.created) - Date.parse(a.created);
|
||||
default:
|
||||
throw new Error(`Unknown sorting condition ${sortBy}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private refreshSelectedTab(item?: IGalleryItem): void {
|
||||
if (item) {
|
||||
this.updateGalleryItem(item);
|
||||
}
|
||||
this.loadTabContent(this.state.selectedTab, this.state.searchText, this.state.sortBy, true);
|
||||
}
|
||||
|
||||
private updateGalleryItem(updatedItem: IGalleryItem): void {
|
||||
this.replaceGalleryItem(updatedItem, this.sampleNotebooks);
|
||||
this.replaceGalleryItem(updatedItem, this.publicNotebooks);
|
||||
this.replaceGalleryItem(updatedItem, this.favoriteNotebooks);
|
||||
this.replaceGalleryItem(updatedItem, this.publishedNotebooks);
|
||||
}
|
||||
|
||||
private replaceGalleryItem(item: IGalleryItem, items?: IGalleryItem[]): void {
|
||||
const index = items?.findIndex(value => value.id === item.id);
|
||||
if (index !== -1) {
|
||||
items?.splice(index, 1, item);
|
||||
}
|
||||
}
|
||||
|
||||
private getPageSpecification = (itemIndex?: number, visibleRect?: IRectangle): IPageSpecification => {
|
||||
this.columnCount = Math.floor(visibleRect.width / GalleryCardComponent.CARD_WIDTH);
|
||||
this.rowCount = Math.floor(visibleRect.height / GalleryCardComponent.CARD_HEIGHT);
|
||||
|
||||
return {
|
||||
height: visibleRect.height,
|
||||
itemCount: this.columnCount * this.rowCount
|
||||
};
|
||||
};
|
||||
|
||||
private onRenderCell = (data?: IGalleryItem): JSX.Element => {
|
||||
const isFavorite = this.favoriteNotebooks?.find(item => item.id === data.id) !== undefined;
|
||||
const props: GalleryCardComponentProps = {
|
||||
data,
|
||||
isFavorite,
|
||||
showDelete: this.state.selectedTab === GalleryTab.Published,
|
||||
onClick: () => this.openNotebook(data, isFavorite),
|
||||
onTagClick: this.loadTaggedItems,
|
||||
onFavoriteClick: () => this.favoriteItem(data),
|
||||
onUnfavoriteClick: () => this.unfavoriteItem(data),
|
||||
onDownloadClick: () => this.downloadItem(data),
|
||||
onDeleteClick: () => this.deleteItem(data)
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ float: "left", padding: 10 }}>
|
||||
<GalleryCardComponent {...props} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
public getOfficialSamplesData(): DataModels.GitHubInfoJunoResponse[] {
|
||||
return this.props.officialSamplesData;
|
||||
}
|
||||
|
||||
public getLikedNotebookData(): DataModels.LikedNotebooksJunoResponse {
|
||||
return this.props.likedNotebookData;
|
||||
}
|
||||
|
||||
public openNotebookViewer = async (
|
||||
url: string,
|
||||
notebookMetadata: DataModels.NotebookMetadata,
|
||||
onNotebookMetadataChange: (newNotebookMetadata: DataModels.NotebookMetadata) => Promise<void>,
|
||||
isLikedNotebook: boolean
|
||||
) => {
|
||||
if (!this.props.container) {
|
||||
SessionStorageUtility.setEntryString(
|
||||
StorageKey.NotebookMetadata,
|
||||
notebookMetadata ? JSON.stringify(notebookMetadata) : null
|
||||
);
|
||||
SessionStorageUtility.setEntryString(StorageKey.NotebookName, path.basename(url));
|
||||
window.open(`${config.hostedExplorerURL}notebookViewer.html?notebookurl=${url}`, "_blank");
|
||||
private openNotebook = (data: IGalleryItem, isFavorite: boolean): void => {
|
||||
if (this.props.container && this.props.junoClient) {
|
||||
this.props.container.openGallery(this.props.junoClient.getNotebookContentUrl(data.id), data, isFavorite);
|
||||
} else {
|
||||
this.props.container.openNotebookViewer(url, notebookMetadata, onNotebookMetadataChange, isLikedNotebook);
|
||||
const params = new URLSearchParams({
|
||||
[GalleryUtils.NotebookViewerParams.NotebookUrl]: this.props.junoClient.getNotebookContentUrl(data.id),
|
||||
[GalleryUtils.NotebookViewerParams.GalleryItemId]: data.id
|
||||
});
|
||||
|
||||
window.open(`/notebookViewer.html?${params.toString()}`);
|
||||
}
|
||||
};
|
||||
|
||||
private loadTaggedItems = (tag: string): void => {
|
||||
const searchText = tag;
|
||||
this.setState({
|
||||
searchText
|
||||
});
|
||||
|
||||
this.loadTabContent(this.state.selectedTab, searchText, this.state.sortBy, true);
|
||||
this.props.onSearchTextChange && this.props.onSearchTextChange(searchText);
|
||||
};
|
||||
|
||||
private favoriteItem = async (data: IGalleryItem): Promise<void> => {
|
||||
GalleryUtils.favoriteItem(this.props.container, this.props.junoClient, data, (item: IGalleryItem) => {
|
||||
if (this.favoriteNotebooks) {
|
||||
this.favoriteNotebooks.push(item);
|
||||
} else {
|
||||
this.favoriteNotebooks = [item];
|
||||
}
|
||||
this.refreshSelectedTab(item);
|
||||
});
|
||||
};
|
||||
|
||||
private unfavoriteItem = async (data: IGalleryItem): Promise<void> => {
|
||||
GalleryUtils.unfavoriteItem(this.props.container, this.props.junoClient, data, (item: IGalleryItem) => {
|
||||
this.favoriteNotebooks = this.favoriteNotebooks?.filter(value => value.id !== item.id);
|
||||
this.refreshSelectedTab(item);
|
||||
});
|
||||
};
|
||||
|
||||
private downloadItem = async (data: IGalleryItem): Promise<void> => {
|
||||
GalleryUtils.downloadItem(this, this.props.container, this.props.junoClient, data, item =>
|
||||
this.refreshSelectedTab(item)
|
||||
);
|
||||
};
|
||||
|
||||
private deleteItem = async (data: IGalleryItem): Promise<void> => {
|
||||
GalleryUtils.deleteItem(this.props.container, this.props.junoClient, data, item => {
|
||||
this.publishedNotebooks = this.publishedNotebooks.filter(notebook => item.id !== notebook.id);
|
||||
this.refreshSelectedTab(item);
|
||||
});
|
||||
};
|
||||
|
||||
private onPivotChange = (item: PivotItem): void => {
|
||||
const selectedTab = GalleryTab[item.props.itemKey as keyof typeof GalleryTab];
|
||||
const searchText: string = undefined;
|
||||
this.setState({
|
||||
selectedTab,
|
||||
searchText
|
||||
});
|
||||
|
||||
this.loadTabContent(selectedTab, searchText, this.state.sortBy, false);
|
||||
this.props.onSelectedTabChange && this.props.onSelectedTabChange(selectedTab);
|
||||
};
|
||||
|
||||
private onSearchBoxChange = (event?: React.ChangeEvent<HTMLInputElement>, newValue?: string): void => {
|
||||
const searchText = newValue;
|
||||
this.setState({
|
||||
searchText
|
||||
});
|
||||
|
||||
this.loadTabContent(this.state.selectedTab, searchText, this.state.sortBy, true);
|
||||
this.props.onSearchTextChange && this.props.onSearchTextChange(searchText);
|
||||
};
|
||||
|
||||
private onDropdownChange = (event: React.FormEvent<HTMLDivElement>, option?: IDropdownOption): void => {
|
||||
const sortBy = option.key as SortBy;
|
||||
this.setState({
|
||||
sortBy
|
||||
});
|
||||
|
||||
this.loadTabContent(this.state.selectedTab, this.state.searchText, sortBy, true);
|
||||
this.props.onSortByChange && this.props.onSortByChange(sortBy);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,54 +1,88 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`FullWidthTabs renders 1`] = `
|
||||
<TabComponent
|
||||
currentTabIndex={0}
|
||||
hideHeader={false}
|
||||
onTabIndexChange={[Function]}
|
||||
tabs={
|
||||
Array [
|
||||
Object {
|
||||
"content": Object {
|
||||
"className": "",
|
||||
"render": [Function],
|
||||
},
|
||||
"isVisible": [Function],
|
||||
"title": "Official Samples",
|
||||
},
|
||||
Object {
|
||||
"content": Object {
|
||||
"className": "",
|
||||
"render": [Function],
|
||||
},
|
||||
"isVisible": [Function],
|
||||
"title": "Liked Notebooks",
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`GalleryCardComponent renders 1`] = `
|
||||
exports[`GalleryViewerComponent renders 1`] = `
|
||||
<div
|
||||
className="galleryContainer"
|
||||
>
|
||||
<GalleryCardsComponent
|
||||
data={Array []}
|
||||
onClick={[Function]}
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`GalleryCardsComponent renders 1`] = `
|
||||
<Stack
|
||||
<StyledPivotBase
|
||||
onLinkClick={[Function]}
|
||||
selectedKey="OfficialSamples"
|
||||
>
|
||||
<PivotItem
|
||||
headerText="Official samples"
|
||||
itemKey="OfficialSamples"
|
||||
key="OfficialSamples"
|
||||
style={
|
||||
Object {
|
||||
"marginTop": 20,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Stack
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 20,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 30,
|
||||
"childrenGap": 20,
|
||||
}
|
||||
}
|
||||
wrap={true}
|
||||
/>
|
||||
>
|
||||
<StackItem
|
||||
grow={true}
|
||||
>
|
||||
<StyledSearchBoxBase
|
||||
onChange={[Function]}
|
||||
placeholder="Search"
|
||||
/>
|
||||
</StackItem>
|
||||
<StackItem>
|
||||
<StyledLabelBase>
|
||||
Sort by
|
||||
</StyledLabelBase>
|
||||
</StackItem>
|
||||
<StackItem
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"minWidth": 200,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<StyledWithResponsiveMode
|
||||
onChange={[Function]}
|
||||
options={
|
||||
Array [
|
||||
Object {
|
||||
"key": 0,
|
||||
"text": "Most viewed",
|
||||
},
|
||||
Object {
|
||||
"key": 1,
|
||||
"text": "Most downloaded",
|
||||
},
|
||||
Object {
|
||||
"key": 2,
|
||||
"text": "Most favorited",
|
||||
},
|
||||
Object {
|
||||
"key": 3,
|
||||
"text": "Most recent",
|
||||
},
|
||||
]
|
||||
}
|
||||
selectedKey={0}
|
||||
/>
|
||||
</StackItem>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</PivotItem>
|
||||
</StyledPivotBase>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`GalleryViewerContainerComponent renders 1`] = `<Fragment />`;
|
||||
|
||||
@@ -1,17 +1,30 @@
|
||||
import React from "react";
|
||||
import { shallow } from "enzyme";
|
||||
import { NotebookMetadataComponentProps, NotebookMetadataComponent } from "./NotebookMetadataComponent";
|
||||
import { NotebookMetadata } from "../../../Contracts/DataModels";
|
||||
import React from "react";
|
||||
import { NotebookMetadataComponent, NotebookMetadataComponentProps } from "./NotebookMetadataComponent";
|
||||
|
||||
describe("NotebookMetadataComponent", () => {
|
||||
it("renders un-liked notebook", () => {
|
||||
const props: NotebookMetadataComponentProps = {
|
||||
notebookName: "My notebook",
|
||||
container: undefined,
|
||||
notebookMetadata: undefined,
|
||||
notebookContent: {},
|
||||
onNotebookMetadataChange: (newNotebookMetadata: NotebookMetadata) => Promise.resolve(),
|
||||
isLikedNotebook: false
|
||||
data: {
|
||||
id: "id",
|
||||
name: "name",
|
||||
description: "description",
|
||||
author: "author",
|
||||
thumbnailUrl: "thumbnailUrl",
|
||||
created: "created",
|
||||
gitSha: "gitSha",
|
||||
tags: ["tag"],
|
||||
isSample: false,
|
||||
downloads: 0,
|
||||
favorites: 0,
|
||||
views: 0
|
||||
},
|
||||
isFavorite: false,
|
||||
downloadButtonText: "Download",
|
||||
onTagClick: undefined,
|
||||
onDownloadClick: undefined,
|
||||
onFavoriteClick: undefined,
|
||||
onUnfavoriteClick: undefined
|
||||
};
|
||||
|
||||
const wrapper = shallow(<NotebookMetadataComponent {...props} />);
|
||||
@@ -20,12 +33,26 @@ describe("NotebookMetadataComponent", () => {
|
||||
|
||||
it("renders liked notebook", () => {
|
||||
const props: NotebookMetadataComponentProps = {
|
||||
notebookName: "My notebook",
|
||||
container: undefined,
|
||||
notebookMetadata: undefined,
|
||||
notebookContent: {},
|
||||
onNotebookMetadataChange: (newNotebookMetadata: NotebookMetadata) => Promise.resolve(),
|
||||
isLikedNotebook: true
|
||||
data: {
|
||||
id: "id",
|
||||
name: "name",
|
||||
description: "description",
|
||||
author: "author",
|
||||
thumbnailUrl: "thumbnailUrl",
|
||||
created: "created",
|
||||
gitSha: "gitSha",
|
||||
tags: ["tag"],
|
||||
isSample: false,
|
||||
downloads: 0,
|
||||
favorites: 0,
|
||||
views: 0
|
||||
},
|
||||
isFavorite: true,
|
||||
downloadButtonText: "Download",
|
||||
onTagClick: undefined,
|
||||
onDownloadClick: undefined,
|
||||
onFavoriteClick: undefined,
|
||||
onUnfavoriteClick: undefined
|
||||
};
|
||||
|
||||
const wrapper = shallow(<NotebookMetadataComponent {...props} />);
|
||||
|
||||
@@ -1,189 +1,85 @@
|
||||
/**
|
||||
* Wrapper around Notebook metadata
|
||||
*/
|
||||
|
||||
import * as React from "react";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { NotebookMetadata } from "../../../Contracts/DataModels";
|
||||
import { initializeIcons } from "office-ui-fabric-react/lib/Icons";
|
||||
import { Icon, Persona, Text, IconButton } from "office-ui-fabric-react";
|
||||
import {
|
||||
siteTextStyles,
|
||||
subtleIconStyles,
|
||||
iconStyles,
|
||||
iconButtonStyles,
|
||||
mainHelpfulTextStyles,
|
||||
subtleHelpfulTextStyles,
|
||||
helpfulTextStyles
|
||||
} from "../NotebookGallery/Cards/CardStyleConstants";
|
||||
|
||||
FontWeights,
|
||||
Icon,
|
||||
IconButton,
|
||||
Link,
|
||||
Persona,
|
||||
PersonaSize,
|
||||
PrimaryButton,
|
||||
Stack,
|
||||
Text
|
||||
} from "office-ui-fabric-react";
|
||||
import * as React from "react";
|
||||
import { IGalleryItem } from "../../../Juno/JunoClient";
|
||||
import { FileSystemUtil } from "../../Notebook/FileSystemUtil";
|
||||
import "./NotebookViewerComponent.less";
|
||||
|
||||
initializeIcons();
|
||||
|
||||
export interface NotebookMetadataComponentProps {
|
||||
notebookName: string;
|
||||
container: ViewModels.Explorer;
|
||||
notebookMetadata: NotebookMetadata;
|
||||
notebookContent: any;
|
||||
onNotebookMetadataChange: (newNotebookMetadata: NotebookMetadata) => Promise<void>;
|
||||
isLikedNotebook: boolean;
|
||||
data: IGalleryItem;
|
||||
isFavorite: boolean;
|
||||
downloadButtonText: string;
|
||||
onTagClick: (tag: string) => void;
|
||||
onFavoriteClick: () => void;
|
||||
onUnfavoriteClick: () => void;
|
||||
onDownloadClick: () => void;
|
||||
}
|
||||
|
||||
interface NotebookMetadatComponentState {
|
||||
liked: boolean;
|
||||
notebookMetadata: NotebookMetadata;
|
||||
}
|
||||
|
||||
export class NotebookMetadataComponent extends React.Component<
|
||||
NotebookMetadataComponentProps,
|
||||
NotebookMetadatComponentState
|
||||
> {
|
||||
constructor(props: NotebookMetadataComponentProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
liked: this.props.isLikedNotebook,
|
||||
notebookMetadata: this.props.notebookMetadata
|
||||
};
|
||||
}
|
||||
|
||||
private onDownloadClick = (newNotebookName: string) => {
|
||||
this.props.container
|
||||
.importAndOpenFromGallery(this.props.notebookName, newNotebookName, JSON.stringify(this.props.notebookContent))
|
||||
.then(() => {
|
||||
if (this.props.notebookMetadata) {
|
||||
if (this.props.onNotebookMetadataChange) {
|
||||
const notebookMetadata = { ...this.state.notebookMetadata };
|
||||
notebookMetadata.downloads += 1;
|
||||
this.props.onNotebookMetadataChange(notebookMetadata).then(() => {
|
||||
this.setState({ notebookMetadata: notebookMetadata });
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.onNotebookMetadataChange) {
|
||||
const notebookMetadata = { ...this.state.notebookMetadata };
|
||||
if (this.props.notebookMetadata) {
|
||||
notebookMetadata.views += 1;
|
||||
this.props.onNotebookMetadataChange(notebookMetadata).then(() => {
|
||||
this.setState({ notebookMetadata: notebookMetadata });
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private onLike = (): void => {
|
||||
if (this.props.onNotebookMetadataChange) {
|
||||
const notebookMetadata = { ...this.state.notebookMetadata };
|
||||
let liked: boolean;
|
||||
if (this.state.liked) {
|
||||
liked = false;
|
||||
notebookMetadata.likes -= 1;
|
||||
} else {
|
||||
liked = true;
|
||||
notebookMetadata.likes += 1;
|
||||
}
|
||||
|
||||
this.props.onNotebookMetadataChange(notebookMetadata).then(() => {
|
||||
this.setState({ liked: liked, notebookMetadata: notebookMetadata });
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private onDownload = (): void => {
|
||||
const promptForNotebookName = () => {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
let newNotebookName = this.props.notebookName;
|
||||
this.props.container.showOkCancelTextFieldModalDialog(
|
||||
"Save notebook as",
|
||||
undefined,
|
||||
"Ok",
|
||||
() => resolve(newNotebookName),
|
||||
"Cancel",
|
||||
() => reject(new Error("New notebook name dialog canceled")),
|
||||
{
|
||||
label: "New notebook name:",
|
||||
autoAdjustHeight: true,
|
||||
multiline: true,
|
||||
rows: 3,
|
||||
defaultValue: this.props.notebookName,
|
||||
onChange: (_, newValue: string) => {
|
||||
newNotebookName = newValue;
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
promptForNotebookName().then((newNotebookName: string) => {
|
||||
this.onDownloadClick(newNotebookName);
|
||||
});
|
||||
};
|
||||
|
||||
export class NotebookMetadataComponent extends React.Component<NotebookMetadataComponentProps> {
|
||||
public render(): JSX.Element {
|
||||
const options: Intl.DateTimeFormatOptions = {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric"
|
||||
};
|
||||
|
||||
const dateString = new Date(this.props.data.created).toLocaleString("default", options);
|
||||
|
||||
return (
|
||||
<div className="notebookViewerMetadataContainer">
|
||||
<h3 className="title">{this.props.notebookName}</h3>
|
||||
|
||||
{this.props.notebookMetadata && (
|
||||
<div className="decoration">
|
||||
{this.props.container ? (
|
||||
<Stack tokens={{ childrenGap: 10 }}>
|
||||
<Stack horizontal verticalAlign="center" tokens={{ childrenGap: 30 }}>
|
||||
<Text variant="xxLarge" nowrap>
|
||||
{FileSystemUtil.stripExtension(this.props.data.name, "ipynb")}
|
||||
</Text>
|
||||
<Text>
|
||||
<IconButton
|
||||
iconProps={{ iconName: this.state.liked ? "HeartFill" : "Heart" }}
|
||||
styles={iconButtonStyles}
|
||||
onClick={this.onLike}
|
||||
iconProps={{ iconName: this.props.isFavorite ? "HeartFill" : "Heart" }}
|
||||
onClick={this.props.isFavorite ? this.props.onUnfavoriteClick : this.props.onFavoriteClick}
|
||||
/>
|
||||
) : (
|
||||
<Icon iconName="Heart" styles={iconStyles} />
|
||||
)}
|
||||
<Text variant="large" styles={mainHelpfulTextStyles}>
|
||||
{this.state.notebookMetadata.likes} likes
|
||||
{this.props.data.favorites} likes
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
<PrimaryButton text={this.props.downloadButtonText} onClick={this.props.onDownloadClick} />
|
||||
</Stack>
|
||||
|
||||
{this.props.container && (
|
||||
<button aria-label="downloadButton" className="downloadButton" onClick={this.onDownload}>
|
||||
Download Notebook
|
||||
</button>
|
||||
)}
|
||||
<Stack horizontal verticalAlign="center" tokens={{ childrenGap: 10 }}>
|
||||
<Persona text={this.props.data.author} size={PersonaSize.size32} />
|
||||
<Text>{dateString}</Text>
|
||||
<Text>
|
||||
<Icon iconName="RedEye" /> {this.props.data.views}
|
||||
</Text>
|
||||
<Text>
|
||||
<Icon iconName="Download" />
|
||||
{this.props.data.downloads}
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
{this.props.notebookMetadata && (
|
||||
<>
|
||||
<div>
|
||||
<Persona
|
||||
className="persona"
|
||||
text={this.props.notebookMetadata.author}
|
||||
secondaryText={this.props.notebookMetadata.date}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="extras">
|
||||
<Icon iconName="RedEye" styles={subtleIconStyles} />
|
||||
<Text variant="small" styles={subtleHelpfulTextStyles}>
|
||||
{this.state.notebookMetadata.views}
|
||||
<Text nowrap>
|
||||
{this.props.data.tags?.map((tag, index, array) => (
|
||||
<span key={tag}>
|
||||
<Link onClick={(): void => this.props.onTagClick(tag)}>{tag}</Link>
|
||||
{index === array.length - 1 ? <></> : ", "}
|
||||
</span>
|
||||
))}
|
||||
</Text>
|
||||
<Icon iconName="Download" styles={subtleIconStyles} />
|
||||
<Text variant="small" styles={subtleHelpfulTextStyles}>
|
||||
{this.state.notebookMetadata.downloads}
|
||||
|
||||
<Text variant="large" styles={{ root: { fontWeight: FontWeights.semibold } }}>
|
||||
Description
|
||||
</Text>
|
||||
</div>
|
||||
<Text variant="small" styles={siteTextStyles}>
|
||||
{this.props.notebookMetadata.tags.join(", ")}
|
||||
</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text variant="small" styles={helpfulTextStyles}>
|
||||
<b>Description:</b>
|
||||
<p>{this.props.notebookMetadata.description}</p>
|
||||
</Text>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Text>{this.props.data.description}</Text>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@import "../../../../less/Common/Constants";
|
||||
|
||||
.notebookViewerContainer {
|
||||
padding: @DefaultSpace;
|
||||
padding: 30px;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow-y: auto;
|
||||
|
||||
@@ -1,36 +1,44 @@
|
||||
/**
|
||||
* Wrapper around Notebook Viewer Read only content
|
||||
*/
|
||||
|
||||
import { Notebook } from "@nteract/commutable";
|
||||
import { createContentRef } from "@nteract/core";
|
||||
import { Icon, Link } from "office-ui-fabric-react";
|
||||
import * as React from "react";
|
||||
import { contents } from "rx-jupyter";
|
||||
import * as Logger from "../../../Common/Logger";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { IGalleryItem, JunoClient } from "../../../Juno/JunoClient";
|
||||
import * as GalleryUtils from "../../../Utils/GalleryUtils";
|
||||
import { NotificationConsoleUtils } from "../../../Utils/NotificationConsoleUtils";
|
||||
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import { NotebookClientV2 } from "../../Notebook/NotebookClientV2";
|
||||
import { NotebookComponentBootstrapper } from "../../Notebook/NotebookComponent/NotebookComponentBootstrapper";
|
||||
import { createContentRef } from "@nteract/core";
|
||||
import NotebookReadOnlyRenderer from "../../Notebook/NotebookRenderer/NotebookReadOnlyRenderer";
|
||||
import { contents } from "rx-jupyter";
|
||||
import { NotebookMetadata } from "../../../Contracts/DataModels";
|
||||
import { DialogComponent, DialogProps } from "../DialogReactComponent/DialogComponent";
|
||||
import { NotebookMetadataComponent } from "./NotebookMetadataComponent";
|
||||
import "./NotebookViewerComponent.less";
|
||||
|
||||
export interface NotebookViewerComponentProps {
|
||||
notebookName: string;
|
||||
notebookUrl: string;
|
||||
container?: ViewModels.Explorer;
|
||||
notebookMetadata: NotebookMetadata;
|
||||
onNotebookMetadataChange?: (newNotebookMetadata: NotebookMetadata) => Promise<void>;
|
||||
isLikedNotebook?: boolean;
|
||||
hideInputs?: boolean;
|
||||
junoClient?: JunoClient;
|
||||
notebookUrl: string;
|
||||
galleryItem?: IGalleryItem;
|
||||
isFavorite?: boolean;
|
||||
backNavigationText: string;
|
||||
onBackClick: () => void;
|
||||
onTagClick: (tag: string) => void;
|
||||
}
|
||||
|
||||
interface NotebookViewerComponentState {
|
||||
content: any;
|
||||
content: Notebook;
|
||||
galleryItem?: IGalleryItem;
|
||||
isFavorite?: boolean;
|
||||
dialogProps: DialogProps;
|
||||
}
|
||||
|
||||
export class NotebookViewerComponent extends React.Component<
|
||||
NotebookViewerComponentProps,
|
||||
NotebookViewerComponentState
|
||||
> {
|
||||
export class NotebookViewerComponent extends React.Component<NotebookViewerComponentProps, NotebookViewerComponentState>
|
||||
implements GalleryUtils.DialogEnabledComponent {
|
||||
private clientManager: NotebookClientV2;
|
||||
private notebookComponentBootstrapper: NotebookComponentBootstrapper;
|
||||
|
||||
@@ -52,40 +60,118 @@ export class NotebookViewerComponent extends React.Component<
|
||||
contentRef: createContentRef()
|
||||
});
|
||||
|
||||
this.state = { content: undefined };
|
||||
this.state = {
|
||||
content: undefined,
|
||||
galleryItem: props.galleryItem,
|
||||
isFavorite: props.isFavorite,
|
||||
dialogProps: undefined
|
||||
};
|
||||
|
||||
this.loadNotebookContent();
|
||||
}
|
||||
|
||||
private async getJsonNotebookContent(): Promise<any> {
|
||||
const response: Response = await fetch(this.props.notebookUrl);
|
||||
if (response.ok) {
|
||||
return await response.json();
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
setDialogProps = (dialogProps: DialogProps): void => {
|
||||
this.setState({ dialogProps });
|
||||
};
|
||||
|
||||
private async loadNotebookContent(): Promise<void> {
|
||||
try {
|
||||
const response = await fetch(this.props.notebookUrl);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Received HTTP ${response.status} while fetching ${this.props.notebookUrl}`);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.getJsonNotebookContent().then((jsonContent: any) => {
|
||||
this.notebookComponentBootstrapper.setContent("json", jsonContent);
|
||||
this.setState({ content: jsonContent });
|
||||
});
|
||||
const notebook: Notebook = await response.json();
|
||||
this.notebookComponentBootstrapper.setContent("json", notebook);
|
||||
this.setState({ content: notebook });
|
||||
|
||||
if (this.props.galleryItem) {
|
||||
const response = await this.props.junoClient.increaseNotebookViews(this.props.galleryItem.id);
|
||||
if (!response.data) {
|
||||
throw new Error(`Received HTTP ${response.status} while increasing notebook views`);
|
||||
}
|
||||
|
||||
this.setState({ galleryItem: response.data });
|
||||
}
|
||||
} catch (error) {
|
||||
const message = `Failed to load notebook content: ${error}`;
|
||||
Logger.logError(message, "NotebookViewerComponent/loadNotebookContent");
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message);
|
||||
}
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
return (
|
||||
<div className="notebookViewerContainer">
|
||||
{this.props.backNavigationText ? (
|
||||
<Link onClick={this.props.onBackClick}>
|
||||
<Icon iconName="Back" /> {this.props.backNavigationText}
|
||||
</Link>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
|
||||
{this.state.galleryItem ? (
|
||||
<div style={{ margin: 10 }}>
|
||||
<NotebookMetadataComponent
|
||||
notebookMetadata={this.props.notebookMetadata}
|
||||
notebookName={this.props.notebookName}
|
||||
container={this.props.container}
|
||||
notebookContent={this.state.content}
|
||||
onNotebookMetadataChange={this.props.onNotebookMetadataChange}
|
||||
isLikedNotebook={this.props.isLikedNotebook}
|
||||
data={this.state.galleryItem}
|
||||
isFavorite={this.state.isFavorite}
|
||||
downloadButtonText={
|
||||
this.props.container ? "Download to my notebooks" : "Edit/Run in Cosmos DB data explorer"
|
||||
}
|
||||
onTagClick={this.props.onTagClick}
|
||||
onFavoriteClick={this.favoriteItem}
|
||||
onUnfavoriteClick={this.unfavoriteItem}
|
||||
onDownloadClick={this.downloadItem}
|
||||
/>
|
||||
{this.notebookComponentBootstrapper.renderComponent(NotebookReadOnlyRenderer, {
|
||||
hideInputs: this.props.hideInputs
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
|
||||
{this.notebookComponentBootstrapper.renderComponent(NotebookReadOnlyRenderer, { hideInputs: true })}
|
||||
|
||||
{this.state.dialogProps && <DialogComponent {...this.state.dialogProps} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
public static getDerivedStateFromProps(
|
||||
props: NotebookViewerComponentProps,
|
||||
state: NotebookViewerComponentState
|
||||
): Partial<NotebookViewerComponentState> {
|
||||
let galleryItem = props.galleryItem;
|
||||
let isFavorite = props.isFavorite;
|
||||
|
||||
if (state.galleryItem !== undefined) {
|
||||
galleryItem = state.galleryItem;
|
||||
}
|
||||
|
||||
if (state.isFavorite !== undefined) {
|
||||
isFavorite = state.isFavorite;
|
||||
}
|
||||
|
||||
return {
|
||||
galleryItem,
|
||||
isFavorite
|
||||
};
|
||||
}
|
||||
|
||||
private favoriteItem = async (): Promise<void> => {
|
||||
GalleryUtils.favoriteItem(this.props.container, this.props.junoClient, this.state.galleryItem, item =>
|
||||
this.setState({ galleryItem: item, isFavorite: true })
|
||||
);
|
||||
};
|
||||
|
||||
private unfavoriteItem = async (): Promise<void> => {
|
||||
GalleryUtils.unfavoriteItem(this.props.container, this.props.junoClient, this.state.galleryItem, item =>
|
||||
this.setState({ galleryItem: item, isFavorite: false })
|
||||
);
|
||||
};
|
||||
|
||||
private downloadItem = async (): Promise<void> => {
|
||||
GalleryUtils.downloadItem(this, this.props.container, this.props.junoClient, this.state.galleryItem, item =>
|
||||
this.setState({ galleryItem: item })
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,25 +1,199 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`NotebookMetadataComponent renders liked notebook 1`] = `
|
||||
<div
|
||||
className="notebookViewerMetadataContainer"
|
||||
<Stack
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<h3
|
||||
className="title"
|
||||
<Stack
|
||||
horizontal={true}
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 30,
|
||||
}
|
||||
}
|
||||
verticalAlign="center"
|
||||
>
|
||||
My notebook
|
||||
</h3>
|
||||
</div>
|
||||
<Text
|
||||
nowrap={true}
|
||||
variant="xxLarge"
|
||||
>
|
||||
name
|
||||
</Text>
|
||||
<Text>
|
||||
<CustomizedIconButton
|
||||
iconProps={
|
||||
Object {
|
||||
"iconName": "HeartFill",
|
||||
}
|
||||
}
|
||||
/>
|
||||
0
|
||||
likes
|
||||
</Text>
|
||||
<CustomizedPrimaryButton
|
||||
text="Download"
|
||||
/>
|
||||
</Stack>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 10,
|
||||
}
|
||||
}
|
||||
verticalAlign="center"
|
||||
>
|
||||
<StyledPersonaBase
|
||||
size={11}
|
||||
text="author"
|
||||
/>
|
||||
<Text>
|
||||
Invalid Date
|
||||
</Text>
|
||||
<Text>
|
||||
<StyledIconBase
|
||||
iconName="RedEye"
|
||||
/>
|
||||
|
||||
0
|
||||
</Text>
|
||||
<Text>
|
||||
<StyledIconBase
|
||||
iconName="Download"
|
||||
/>
|
||||
0
|
||||
</Text>
|
||||
</Stack>
|
||||
<Text
|
||||
nowrap={true}
|
||||
>
|
||||
<span
|
||||
key="tag"
|
||||
>
|
||||
<StyledLinkBase
|
||||
onClick={[Function]}
|
||||
>
|
||||
tag
|
||||
</StyledLinkBase>
|
||||
</span>
|
||||
</Text>
|
||||
<Text
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"fontWeight": 600,
|
||||
},
|
||||
}
|
||||
}
|
||||
variant="large"
|
||||
>
|
||||
Description
|
||||
</Text>
|
||||
<Text>
|
||||
description
|
||||
</Text>
|
||||
</Stack>
|
||||
`;
|
||||
|
||||
exports[`NotebookMetadataComponent renders un-liked notebook 1`] = `
|
||||
<div
|
||||
className="notebookViewerMetadataContainer"
|
||||
<Stack
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<h3
|
||||
className="title"
|
||||
<Stack
|
||||
horizontal={true}
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 30,
|
||||
}
|
||||
}
|
||||
verticalAlign="center"
|
||||
>
|
||||
My notebook
|
||||
</h3>
|
||||
</div>
|
||||
<Text
|
||||
nowrap={true}
|
||||
variant="xxLarge"
|
||||
>
|
||||
name
|
||||
</Text>
|
||||
<Text>
|
||||
<CustomizedIconButton
|
||||
iconProps={
|
||||
Object {
|
||||
"iconName": "Heart",
|
||||
}
|
||||
}
|
||||
/>
|
||||
0
|
||||
likes
|
||||
</Text>
|
||||
<CustomizedPrimaryButton
|
||||
text="Download"
|
||||
/>
|
||||
</Stack>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 10,
|
||||
}
|
||||
}
|
||||
verticalAlign="center"
|
||||
>
|
||||
<StyledPersonaBase
|
||||
size={11}
|
||||
text="author"
|
||||
/>
|
||||
<Text>
|
||||
Invalid Date
|
||||
</Text>
|
||||
<Text>
|
||||
<StyledIconBase
|
||||
iconName="RedEye"
|
||||
/>
|
||||
|
||||
0
|
||||
</Text>
|
||||
<Text>
|
||||
<StyledIconBase
|
||||
iconName="Download"
|
||||
/>
|
||||
0
|
||||
</Text>
|
||||
</Stack>
|
||||
<Text
|
||||
nowrap={true}
|
||||
>
|
||||
<span
|
||||
key="tag"
|
||||
>
|
||||
<StyledLinkBase
|
||||
onClick={[Function]}
|
||||
>
|
||||
tag
|
||||
</StyledLinkBase>
|
||||
</span>
|
||||
</Text>
|
||||
<Text
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"fontWeight": 600,
|
||||
},
|
||||
}
|
||||
}
|
||||
variant="large"
|
||||
>
|
||||
Description
|
||||
</Text>
|
||||
<Text>
|
||||
description
|
||||
</Text>
|
||||
</Stack>
|
||||
`;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
@import "../../../../less/Common/Constants";
|
||||
|
||||
.tabComponentContainer {
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
.flex-display();
|
||||
.flex-direction();
|
||||
|
||||
@@ -6,10 +6,9 @@
|
||||
*/
|
||||
|
||||
import * as React from "react";
|
||||
import * as ReactDOM from "react-dom";
|
||||
import * as Constants from "../../../Common/Constants";
|
||||
import AnimateHeight from "react-animate-height";
|
||||
import { IconButton } from "office-ui-fabric-react/lib/Button";
|
||||
import { IconButton, IButtonStyles } from "office-ui-fabric-react/lib/Button";
|
||||
import {
|
||||
DirectionalHint,
|
||||
IContextualMenuItemProps,
|
||||
@@ -227,6 +226,10 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
|
||||
|
||||
private renderContextMenuButton(node: TreeNode): JSX.Element {
|
||||
const menuItemLabel = "More";
|
||||
const buttonStyles: Partial<IButtonStyles> = {
|
||||
rootFocused: { outline: `1px dashed ${Constants.StyleConstants.FocusColor}` }
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={this.contextMenuRef} onContextMenu={this.onRightClick} onKeyPress={this.onMoreButtonKeyPress}>
|
||||
<IconButton
|
||||
@@ -264,6 +267,7 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
|
||||
onRenderIcon: (props: any) => <img src={menuItem.iconSrc} alt="" />
|
||||
}))
|
||||
}}
|
||||
styles={buttonStyles}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -191,6 +191,13 @@ exports[`TreeNodeComponent renders a simple node (sorted children, expanded) 1`]
|
||||
}
|
||||
}
|
||||
name="More"
|
||||
styles={
|
||||
Object {
|
||||
"rootFocused": Object {
|
||||
"outline": "1px dashed undefined",
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -314,6 +321,13 @@ exports[`TreeNodeComponent renders sorted children, expanded, leaves and parents
|
||||
}
|
||||
}
|
||||
name="More"
|
||||
styles={
|
||||
Object {
|
||||
"rootFocused": Object {
|
||||
"outline": "1px dashed undefined",
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -21,7 +21,6 @@ import EnvironmentUtility from "../Common/EnvironmentUtility";
|
||||
import GraphStylingPane from "./Panes/GraphStylingPane";
|
||||
import hasher from "hasher";
|
||||
import NewVertexPane from "./Panes/NewVertexPane";
|
||||
import NotebookTab from "./Tabs/NotebookTab";
|
||||
import NotebookV2Tab from "./Tabs/NotebookV2Tab";
|
||||
import Q from "q";
|
||||
import ResourceTokenCollection from "./Tree/ResourceTokenCollection";
|
||||
@@ -50,19 +49,15 @@ import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane";
|
||||
import { ExplorerMetrics } from "../Common/Constants";
|
||||
import { ExplorerSettings } from "../Shared/ExplorerSettings";
|
||||
import { FileSystemUtil } from "./Notebook/FileSystemUtil";
|
||||
import { GitHubOAuthService } from "../GitHub/GitHubOAuthService";
|
||||
import { GitHubReposPane } from "./Panes/GitHubReposPane";
|
||||
import { handleOpenAction } from "./OpenActions";
|
||||
import { IContentProvider } from "@nteract/core";
|
||||
import { isInvalidParentFrameOrigin } from "../Utils/MessageValidation";
|
||||
import { JunoClient } from "../Juno/JunoClient";
|
||||
import { IGalleryItem } from "../Juno/JunoClient";
|
||||
import { LibraryManagePane } from "./Panes/LibraryManagePane";
|
||||
import { LoadQueryPane } from "./Panes/LoadQueryPane";
|
||||
import { Logger } from "../Common/Logger";
|
||||
import * as Logger from "../Common/Logger";
|
||||
import { ManageSparkClusterPane } from "./Panes/ManageSparkClusterPane";
|
||||
import { MessageHandler } from "../Common/MessageHandler";
|
||||
import { NotebookContentItem, NotebookContentItemType } from "./Notebook/NotebookContentItem";
|
||||
import { NotebookContentProvider } from "./Notebook/NotebookComponent/NotebookContentProvider";
|
||||
import { NotebookUtil } from "./Notebook/NotebookUtil";
|
||||
import { NotebookWorkspaceManager } from "../NotebookWorkspaceManager/NotebookWorkspaceManager";
|
||||
import { NotificationConsoleComponentAdapter } from "./Menus/NotificationConsole/NotificationConsoleComponentAdapter";
|
||||
@@ -86,6 +81,8 @@ import { StringInputPane } from "./Panes/StringInputPane";
|
||||
import { TableColumnOptionsPane } from "./Panes/Tables/TableColumnOptionsPane";
|
||||
import { UploadFilePane } from "./Panes/UploadFilePane";
|
||||
import { UploadItemsPane } from "./Panes/UploadItemsPane";
|
||||
import { UploadItemsPaneAdapter } from "./Panes/UploadItemsPaneAdapter";
|
||||
import { ReactAdapter } from "../Bindings/ReactBindingHandler";
|
||||
|
||||
BindingHandlersRegisterer.registerBindingHandlers();
|
||||
// Hold a reference to ComponentRegisterer to prevent transpiler to ignore import
|
||||
@@ -154,7 +151,6 @@ export default class Explorer implements ViewModels.Explorer {
|
||||
public selectedNode: ko.Observable<ViewModels.TreeNode>;
|
||||
public isRefreshingExplorer: ko.Observable<boolean>;
|
||||
private resourceTree: ResourceTreeAdapter;
|
||||
private enableLegacyResourceTree: ko.Observable<boolean>;
|
||||
|
||||
// Resource Token
|
||||
public resourceTokenDatabaseId: ko.Observable<string>;
|
||||
@@ -188,6 +184,7 @@ export default class Explorer implements ViewModels.Explorer {
|
||||
public executeSprocParamsPane: ViewModels.ExecuteSprocParamsPane;
|
||||
public renewAdHocAccessPane: ViewModels.RenewAdHocAccessPane;
|
||||
public uploadItemsPane: ViewModels.UploadItemsPane;
|
||||
public uploadItemsPaneAdapter: UploadItemsPaneAdapter;
|
||||
public loadQueryPane: ViewModels.LoadQueryPane;
|
||||
public saveQueryPane: ViewModels.ContextualPane;
|
||||
public browseQueriesPane: ViewModels.BrowseQueriesPane;
|
||||
@@ -199,12 +196,15 @@ export default class Explorer implements ViewModels.Explorer {
|
||||
public libraryManagePane: ViewModels.ContextualPane;
|
||||
public clusterLibraryPane: ViewModels.ContextualPane;
|
||||
public gitHubReposPane: ViewModels.ContextualPane;
|
||||
public publishNotebookPaneAdapter: ReactAdapter;
|
||||
|
||||
// features
|
||||
public isGalleryEnabled: ko.Computed<boolean>;
|
||||
public isGalleryPublishEnabled: ko.Computed<boolean>;
|
||||
public isGitHubPaneEnabled: ko.Observable<boolean>;
|
||||
public isGraphsEnabled: ko.Computed<boolean>;
|
||||
public isPublishNotebookPaneEnabled: ko.Observable<boolean>;
|
||||
public isHostedDataExplorerEnabled: ko.Computed<boolean>;
|
||||
public isRightPanelV2Enabled: ko.Computed<boolean>;
|
||||
public canExceedMaximumValue: ko.Computed<boolean>;
|
||||
public hasAutoPilotV2FeatureFlag: ko.Computed<boolean>;
|
||||
|
||||
@@ -223,11 +223,7 @@ export default class Explorer implements ViewModels.Explorer {
|
||||
// Notebooks
|
||||
public isNotebookEnabled: ko.Observable<boolean>;
|
||||
public isNotebooksEnabledForAccount: ko.Observable<boolean>;
|
||||
private notebookClient: ViewModels.INotebookContainerClient;
|
||||
private notebookContentClient: ViewModels.INotebookContentClient;
|
||||
public notebookServerInfo: ko.Observable<DataModels.NotebookWorkspaceConnectionInfo>;
|
||||
public notebookContentProvider: IContentProvider;
|
||||
public gitHubOAuthService: GitHubOAuthService;
|
||||
public notebookWorkspaceManager: ViewModels.NotebookWorkspaceManager;
|
||||
public sparkClusterManager: ViewModels.SparkClusterManager;
|
||||
public sparkClusterConnectionInfo: ko.Observable<DataModels.SparkClusterConnectionInfo>;
|
||||
@@ -239,6 +235,7 @@ export default class Explorer implements ViewModels.Explorer {
|
||||
public isSynapseLinkUpdating: ko.Observable<boolean>;
|
||||
public isNotebookTabActive: ko.Computed<boolean>;
|
||||
public memoryUsageInfo: ko.Observable<DataModels.MemoryUsageInfo>;
|
||||
public notebookManager?: any; // This is dynamically loaded
|
||||
|
||||
private _panes: ViewModels.ContextualPane[] = [];
|
||||
private _importExplorerConfigComplete: boolean = false;
|
||||
@@ -303,7 +300,7 @@ export default class Explorer implements ViewModels.Explorer {
|
||||
this.openedTabs &&
|
||||
this.openedTabs().forEach(tab => {
|
||||
if (tab.tabKind === ViewModels.CollectionTabKind.Notebook) {
|
||||
(tab as NotebookTab).reconfigureServiceEndpoints();
|
||||
throw new Error("NotebookTab is deprecated. Use NotebookV2Tab");
|
||||
} else if (tab.tabKind === ViewModels.CollectionTabKind.NotebookV2) {
|
||||
(tab as NotebookV2Tab).reconfigureServiceEndpoints();
|
||||
}
|
||||
@@ -381,7 +378,6 @@ export default class Explorer implements ViewModels.Explorer {
|
||||
this.armEndpoint = ko.observable<string>(undefined);
|
||||
this.queriesClient = new QueriesClient(this);
|
||||
this.isTryCosmosDBSubscription = ko.observable<boolean>(false);
|
||||
this.enableLegacyResourceTree = ko.observable<boolean>(false);
|
||||
|
||||
this.resourceTokenDatabaseId = ko.observable<string>();
|
||||
this.resourceTokenCollectionId = ko.observable<string>();
|
||||
@@ -410,10 +406,11 @@ export default class Explorer implements ViewModels.Explorer {
|
||||
this.shouldShowDataAccessExpiryDialog = ko.observable<boolean>(false);
|
||||
this.shouldShowContextSwitchPrompt = ko.observable<boolean>(false);
|
||||
this.isGalleryEnabled = ko.computed<boolean>(() => this.isFeatureEnabled(Constants.Features.enableGallery));
|
||||
this.isGalleryPublishEnabled = ko.computed<boolean>(() =>
|
||||
this.isFeatureEnabled(Constants.Features.enableGalleryPublish)
|
||||
);
|
||||
this.isGitHubPaneEnabled = ko.observable<boolean>(false);
|
||||
this.isGraphsEnabled = ko.computed<boolean>(() => {
|
||||
return this.isFeatureEnabled(Constants.Features.graphs);
|
||||
});
|
||||
this.isPublishNotebookPaneEnabled = ko.observable<boolean>(false);
|
||||
|
||||
this.canExceedMaximumValue = ko.computed<boolean>(() =>
|
||||
this.isFeatureEnabled(Constants.Features.canExceedMaximumValue)
|
||||
@@ -551,6 +548,9 @@ export default class Explorer implements ViewModels.Explorer {
|
||||
!this.isRunningOnNationalCloud() &&
|
||||
!this.isPreferredApiGraph()
|
||||
);
|
||||
this.isRightPanelV2Enabled = ko.computed<boolean>(() =>
|
||||
this.isFeatureEnabled(Constants.Features.enableRightPanelV2)
|
||||
);
|
||||
this.defaultExperience.subscribe((defaultExperience: string) => {
|
||||
if (
|
||||
defaultExperience &&
|
||||
@@ -707,6 +707,8 @@ export default class Explorer implements ViewModels.Explorer {
|
||||
container: this
|
||||
});
|
||||
|
||||
this.uploadItemsPaneAdapter = new UploadItemsPaneAdapter(this);
|
||||
|
||||
this.loadQueryPane = new LoadQueryPane({
|
||||
documentClientUtility: this.documentClientUtility,
|
||||
id: "loadquerypane",
|
||||
@@ -936,127 +938,33 @@ export default class Explorer implements ViewModels.Explorer {
|
||||
startKey
|
||||
);
|
||||
|
||||
const junoClient = new JunoClient(this.databaseAccount);
|
||||
|
||||
this.isNotebookEnabled = ko.observable(false);
|
||||
this.isNotebookEnabled.subscribe(async (isEnabled: boolean) => {
|
||||
this.refreshCommandBarButtons();
|
||||
|
||||
this.gitHubOAuthService = new GitHubOAuthService(junoClient);
|
||||
|
||||
const GitHubClientModule = await import(/* webpackChunkName: "GitHubClient" */ "../GitHub/GitHubClient");
|
||||
const gitHubClient = new GitHubClientModule.GitHubClient(config.AZURESAMPLESCOSMOSDBPAT, error => {
|
||||
Logger.logError(error, "Explorer/GitHubClient errorCallback");
|
||||
|
||||
if (error.status === Constants.HttpStatusCodes.Unauthorized) {
|
||||
this.gitHubOAuthService?.resetToken();
|
||||
|
||||
this.showOkCancelModalDialog(
|
||||
undefined,
|
||||
"Cosmos DB cannot access your Github account anymore. Please connect to GitHub again.",
|
||||
"Connect to GitHub",
|
||||
() => this.gitHubReposPane?.open(),
|
||||
"Cancel",
|
||||
undefined
|
||||
this.isNotebookEnabled.subscribe(async () => {
|
||||
if (!this.notebookManager) {
|
||||
const notebookManagerModule = await import(
|
||||
/* webpackChunkName: "NotebookManager" */ "./Notebook/NotebookManager"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
this.gitHubReposPane = new GitHubReposPane({
|
||||
documentClientUtility: this.documentClientUtility,
|
||||
id: "gitHubReposPane",
|
||||
visible: ko.observable<boolean>(false),
|
||||
this.notebookManager = new notebookManagerModule.default();
|
||||
this.notebookManager.initialize({
|
||||
container: this,
|
||||
junoClient,
|
||||
gitHubClient
|
||||
dialogProps: this._dialogProps,
|
||||
notebookBasePath: this.notebookBasePath,
|
||||
resourceTree: this.resourceTree,
|
||||
refreshCommandBarButtons: () => this.refreshCommandBarButtons(),
|
||||
refreshNotebookList: () => this.refreshNotebookList()
|
||||
});
|
||||
|
||||
this.gitHubReposPane = this.notebookManager.gitHubReposPane;
|
||||
this.isGitHubPaneEnabled(true);
|
||||
|
||||
this.gitHubOAuthService.getTokenObservable().subscribe(token => {
|
||||
gitHubClient.setToken(token?.access_token ? token.access_token : config.AZURESAMPLESCOSMOSDBPAT);
|
||||
|
||||
if (this.gitHubReposPane?.visible()) {
|
||||
this.gitHubReposPane.open();
|
||||
}
|
||||
|
||||
this.refreshCommandBarButtons();
|
||||
this.refreshNotebookList();
|
||||
});
|
||||
|
||||
if (this.isGalleryEnabled()) {
|
||||
this.galleryTab = await import(/* webpackChunkName: "GalleryTab" */ "./Tabs/GalleryTab");
|
||||
this.notebookViewerTab = await import(/* webpackChunkName: "NotebookViewerTab" */ "./Tabs/NotebookViewerTab");
|
||||
}
|
||||
|
||||
const promptForCommitMsg = (title: string, primaryButtonLabel: string) => {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
let commitMsg: string = "Committed from Azure Cosmos DB Notebooks";
|
||||
this.showOkCancelTextFieldModalDialog(
|
||||
title || "Commit",
|
||||
undefined,
|
||||
primaryButtonLabel || "Commit",
|
||||
() => {
|
||||
TelemetryProcessor.trace(Action.NotebooksGitHubCommit, ActionModifiers.Mark, {
|
||||
databaseAccountName: this.databaseAccount() && this.databaseAccount().name,
|
||||
defaultExperience: this.defaultExperience && this.defaultExperience(),
|
||||
dataExplorerArea: Constants.Areas.Notebook
|
||||
});
|
||||
resolve(commitMsg);
|
||||
},
|
||||
"Cancel",
|
||||
() => reject(new Error("Commit dialog canceled")),
|
||||
{
|
||||
label: "Commit message",
|
||||
autoAdjustHeight: true,
|
||||
multiline: true,
|
||||
defaultValue: commitMsg,
|
||||
rows: 3,
|
||||
onChange: (_, newValue: string) => {
|
||||
commitMsg = newValue;
|
||||
this._dialogProps().primaryButtonDisabled = !commitMsg;
|
||||
this._dialogProps.valueHasMutated();
|
||||
}
|
||||
},
|
||||
!commitMsg
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const GitHubContentProviderModule = await import(
|
||||
/* webpackChunkName: "rx-jupyter" */ "../GitHub/GitHubContentProvider"
|
||||
);
|
||||
const RXJupyterModule = await import(/* webpackChunkName: "rx-jupyter" */ "rx-jupyter");
|
||||
this.notebookContentProvider = new NotebookContentProvider(
|
||||
new GitHubContentProviderModule.GitHubContentProvider({ gitHubClient, promptForCommitMsg }),
|
||||
RXJupyterModule.contents.JupyterContentProvider
|
||||
);
|
||||
|
||||
const NotebookContainerClientModule = await import(
|
||||
/* webpackChunkName: "NotebookContainerClient" */ "./Notebook/NotebookContainerClient"
|
||||
);
|
||||
|
||||
this.notebookClient = new NotebookContainerClientModule.NotebookContainerClient(
|
||||
this.notebookServerInfo,
|
||||
() => this.initNotebooks(this.databaseAccount()),
|
||||
(update: DataModels.MemoryUsageInfo) => this.memoryUsageInfo(update)
|
||||
);
|
||||
|
||||
const NotebookContentClientModule = await import(
|
||||
/* webpackChunkName: "NotebookContentClient" */ "./Notebook/NotebookContentClient"
|
||||
);
|
||||
this.notebookContentClient = new NotebookContentClientModule.NotebookContentClient(
|
||||
this.notebookServerInfo,
|
||||
this.notebookBasePath,
|
||||
this.notebookContentProvider
|
||||
);
|
||||
|
||||
this.refreshNotebookList();
|
||||
});
|
||||
|
||||
this.isSparkEnabled = ko.observable(false);
|
||||
this.isSparkEnabled.subscribe((isEnabled: boolean) => this.refreshCommandBarButtons());
|
||||
this.resourceTree = new ResourceTreeAdapter(this, junoClient);
|
||||
this.resourceTree = new ResourceTreeAdapter(this);
|
||||
this.resourceTreeForResourceToken = new ResourceTreeAdapterForResourceToken(this);
|
||||
this.notebookServerInfo = ko.observable<DataModels.NotebookWorkspaceConnectionInfo>({
|
||||
notebookServerEndpoint: undefined,
|
||||
@@ -1100,8 +1008,6 @@ export default class Explorer implements ViewModels.Explorer {
|
||||
this.sparkClusterConnectionInfo.valueHasMutated();
|
||||
}
|
||||
|
||||
this.enableLegacyResourceTree(this.isFeatureEnabled(Constants.Features.enableLegacyResourceTree));
|
||||
|
||||
featureSubcription.dispose();
|
||||
});
|
||||
|
||||
@@ -1888,7 +1794,7 @@ export default class Explorer implements ViewModels.Explorer {
|
||||
}
|
||||
|
||||
public resetNotebookWorkspace() {
|
||||
if (!this.isNotebookEnabled() || !this.notebookClient) {
|
||||
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookClient) {
|
||||
const error = "Attempt to reset notebook workspace, but notebook is not enabled";
|
||||
Logger.logError(error, "Explorer/resetNotebookWorkspace");
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error);
|
||||
@@ -1995,7 +1901,7 @@ export default class Explorer implements ViewModels.Explorer {
|
||||
this._closeModalDialog();
|
||||
const id = NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.InProgress, "Resetting notebook workspace");
|
||||
try {
|
||||
await this.notebookClient.resetWorkspace();
|
||||
await this.notebookManager?.notebookClient.resetWorkspace();
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, "Successfully reset notebook workspace");
|
||||
TelemetryProcessor.traceSuccess(Action.ResetNotebookWorkspace);
|
||||
} catch (error) {
|
||||
@@ -2564,17 +2470,17 @@ export default class Explorer implements ViewModels.Explorer {
|
||||
}
|
||||
|
||||
private uploadFile(name: string, content: string, parent: NotebookContentItem): Promise<NotebookContentItem> {
|
||||
if (!this.isNotebookEnabled() || !this.notebookContentClient) {
|
||||
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
|
||||
const error = "Attempt to upload notebook, but notebook is not enabled";
|
||||
Logger.logError(error, "Explorer/uploadFile");
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error);
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
const promise = this.notebookContentClient.uploadFileAsync(name, content, parent);
|
||||
const promise = this.notebookManager?.notebookContentClient.uploadFileAsync(name, content, parent);
|
||||
promise
|
||||
.then(() => this.resourceTree.triggerRender())
|
||||
.catch(reason => this.showOkModalDialog("Unable to upload file", reason));
|
||||
.catch((reason: any) => this.showOkModalDialog("Unable to upload file", reason));
|
||||
return promise;
|
||||
}
|
||||
|
||||
@@ -2583,7 +2489,7 @@ export default class Explorer implements ViewModels.Explorer {
|
||||
const item = NotebookUtil.createNotebookContentItem(name, path, "file");
|
||||
const parent = this.resourceTree.myNotebooksContentRoot;
|
||||
|
||||
if (parent && parent.children && this.isNotebookEnabled() && this.notebookClient) {
|
||||
if (parent && parent.children && this.isNotebookEnabled() && this.notebookManager?.notebookClient) {
|
||||
if (this._filePathToImportAndOpen === path) {
|
||||
this._filePathToImportAndOpen = null; // we don't want to try opening this path again
|
||||
}
|
||||
@@ -2602,15 +2508,10 @@ export default class Explorer implements ViewModels.Explorer {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
public async importAndOpenFromGallery(path: string, newName: string, content: any): Promise<boolean> {
|
||||
const name = newName;
|
||||
public async importAndOpenFromGallery(name: string, content: string): Promise<boolean> {
|
||||
const parent = this.resourceTree.myNotebooksContentRoot;
|
||||
|
||||
if (parent && this.isNotebookEnabled() && this.notebookClient) {
|
||||
if (this._filePathToImportAndOpen === path) {
|
||||
this._filePathToImportAndOpen = undefined; // we don't want to try opening this path again
|
||||
}
|
||||
|
||||
if (parent && parent.children && this.isNotebookEnabled() && this.notebookManager?.notebookClient) {
|
||||
const existingItem = _.find(parent.children, node => node.name === name);
|
||||
if (existingItem) {
|
||||
this.showOkModalDialog("Download failed", "Notebook with the same name already exists.");
|
||||
@@ -2621,10 +2522,17 @@ export default class Explorer implements ViewModels.Explorer {
|
||||
return this.openNotebook(uploadedItem);
|
||||
}
|
||||
|
||||
this._filePathToImportAndOpen = path; // we'll try opening this path later on
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
public publishNotebook(name: string, content: string): void {
|
||||
if (this.notebookManager) {
|
||||
this.notebookManager.openPublishNotebookPane(name, content);
|
||||
this.publishNotebookPaneAdapter = this.notebookManager.publishNotebookPaneAdapter;
|
||||
this.isPublishNotebookPaneEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
public showOkModalDialog(title: string, msg: string): void {
|
||||
this._dialogProps({
|
||||
isModal: true,
|
||||
@@ -2757,7 +2665,7 @@ export default class Explorer implements ViewModels.Explorer {
|
||||
}
|
||||
|
||||
public renameNotebook(notebookFile: NotebookContentItem): Q.Promise<NotebookContentItem> {
|
||||
if (!this.isNotebookEnabled() || !this.notebookContentClient) {
|
||||
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
|
||||
const error = "Attempt to rename notebook, but notebook is not enabled";
|
||||
Logger.logError(error, "Explorer/renameNotebook");
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error);
|
||||
@@ -2768,7 +2676,7 @@ export default class Explorer implements ViewModels.Explorer {
|
||||
const openedNotebookTabs = this.openedTabs().filter(
|
||||
(tab: ViewModels.Tab) =>
|
||||
tab.tabKind === ViewModels.CollectionTabKind.NotebookV2 &&
|
||||
(tab as NotebookTab).notebookPath() === notebookFile.path
|
||||
(tab as NotebookV2Tab).notebookPath() === notebookFile.path
|
||||
);
|
||||
if (openedNotebookTabs.length > 0) {
|
||||
this.showOkModalDialog("Unable to rename file", "This file is being edited. Please close the tab and try again.");
|
||||
@@ -2785,19 +2693,19 @@ export default class Explorer implements ViewModels.Explorer {
|
||||
paneTitle: "Rename Notebook",
|
||||
submitButtonLabel: "Rename",
|
||||
defaultInput: FileSystemUtil.stripExtension(notebookFile.name, "ipynb"),
|
||||
onSubmit: (input: string) => this.notebookContentClient.renameNotebook(notebookFile, input)
|
||||
onSubmit: (input: string) => this.notebookManager?.notebookContentClient.renameNotebook(notebookFile, input)
|
||||
})
|
||||
.then(newNotebookFile => {
|
||||
this.openedTabs()
|
||||
.filter(
|
||||
(tab: ViewModels.Tab) =>
|
||||
tab.tabKind === ViewModels.CollectionTabKind.NotebookV2 &&
|
||||
FileSystemUtil.isPathEqual((tab as NotebookTab).notebookPath(), originalPath)
|
||||
FileSystemUtil.isPathEqual((tab as NotebookV2Tab).notebookPath(), originalPath)
|
||||
)
|
||||
.forEach(tab => {
|
||||
tab.tabTitle(newNotebookFile.name);
|
||||
tab.tabPath(newNotebookFile.path);
|
||||
(tab as NotebookTab).notebookPath(newNotebookFile.path);
|
||||
(tab as NotebookV2Tab).notebookPath(newNotebookFile.path);
|
||||
});
|
||||
|
||||
return newNotebookFile;
|
||||
@@ -2807,7 +2715,7 @@ export default class Explorer implements ViewModels.Explorer {
|
||||
}
|
||||
|
||||
public onCreateDirectory(parent: NotebookContentItem): Q.Promise<NotebookContentItem> {
|
||||
if (!this.isNotebookEnabled() || !this.notebookContentClient) {
|
||||
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
|
||||
const error = "Attempt to create notebook directory, but notebook is not enabled";
|
||||
Logger.logError(error, "Explorer/onCreateDirectory");
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error);
|
||||
@@ -2822,32 +2730,32 @@ export default class Explorer implements ViewModels.Explorer {
|
||||
paneTitle: "Create new directory",
|
||||
submitButtonLabel: "Create",
|
||||
defaultInput: "",
|
||||
onSubmit: (input: string) => this.notebookContentClient.createDirectory(parent, input)
|
||||
onSubmit: (input: string) => this.notebookManager?.notebookContentClient.createDirectory(parent, input)
|
||||
});
|
||||
result.then(() => this.resourceTree.triggerRender());
|
||||
return result;
|
||||
}
|
||||
|
||||
public readFile(notebookFile: NotebookContentItem): Promise<string> {
|
||||
if (!this.isNotebookEnabled() || !this.notebookContentClient) {
|
||||
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
|
||||
const error = "Attempt to read file, but notebook is not enabled";
|
||||
Logger.logError(error, "Explorer/downloadFile");
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error);
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
return this.notebookContentClient.readFileContent(notebookFile.path);
|
||||
return this.notebookManager?.notebookContentClient.readFileContent(notebookFile.path);
|
||||
}
|
||||
|
||||
public downloadFile(notebookFile: NotebookContentItem): Promise<void> {
|
||||
if (!this.isNotebookEnabled() || !this.notebookContentClient) {
|
||||
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
|
||||
const error = "Attempt to download file, but notebook is not enabled";
|
||||
Logger.logError(error, "Explorer/downloadFile");
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error);
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
return this.notebookContentClient.readFileContent(notebookFile.path).then(
|
||||
return this.notebookManager?.notebookContentClient.readFileContent(notebookFile.path).then(
|
||||
(content: string) => {
|
||||
const blob = new Blob([content], { type: "octet/stream" });
|
||||
if (navigator.msSaveBlob) {
|
||||
@@ -2867,7 +2775,7 @@ export default class Explorer implements ViewModels.Explorer {
|
||||
downloadLink.remove();
|
||||
}
|
||||
},
|
||||
error => {
|
||||
(error: any) => {
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Could not download notebook ${JSON.stringify(error)}`
|
||||
@@ -3014,7 +2922,7 @@ export default class Explorer implements ViewModels.Explorer {
|
||||
}
|
||||
|
||||
private refreshNotebookList = async (): Promise<void> => {
|
||||
if (!this.isNotebookEnabled() || !this.notebookContentClient) {
|
||||
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3025,7 +2933,7 @@ export default class Explorer implements ViewModels.Explorer {
|
||||
};
|
||||
|
||||
public deleteNotebookFile(item: NotebookContentItem): Promise<void> {
|
||||
if (!this.isNotebookEnabled() || !this.notebookContentClient) {
|
||||
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
|
||||
const error = "Attempt to delete notebook file, but notebook is not enabled";
|
||||
Logger.logError(error, "Explorer/deleteNotebookFile");
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error);
|
||||
@@ -3035,7 +2943,7 @@ export default class Explorer implements ViewModels.Explorer {
|
||||
// Don't delete if tab is open to avoid accidental deletion
|
||||
const openedNotebookTabs = this.openedTabs().filter(
|
||||
(tab: ViewModels.Tab) =>
|
||||
tab.tabKind === ViewModels.CollectionTabKind.NotebookV2 && (tab as NotebookTab).notebookPath() === item.path
|
||||
tab.tabKind === ViewModels.CollectionTabKind.NotebookV2 && (tab as NotebookV2Tab).notebookPath() === item.path
|
||||
);
|
||||
if (openedNotebookTabs.length > 0) {
|
||||
this.showOkModalDialog("Unable to delete file", "This file is being edited. Please close the tab and try again.");
|
||||
@@ -3056,11 +2964,11 @@ export default class Explorer implements ViewModels.Explorer {
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
return this.notebookContentClient.deleteContentItem(item).then(
|
||||
return this.notebookManager?.notebookContentClient.deleteContentItem(item).then(
|
||||
() => {
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, `Successfully deleted: ${item.path}`);
|
||||
},
|
||||
reason => {
|
||||
(reason: any) => {
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Failed to delete "${item.path}": ${JSON.stringify(reason)}`
|
||||
@@ -3073,7 +2981,7 @@ export default class Explorer implements ViewModels.Explorer {
|
||||
* This creates a new notebook file, then opens the notebook
|
||||
*/
|
||||
public onNewNotebookClicked(parent?: NotebookContentItem): void {
|
||||
if (!this.isNotebookEnabled() || !this.notebookContentClient) {
|
||||
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
|
||||
const error = "Attempt to create new notebook, but notebook is not enabled";
|
||||
Logger.logError(error, "Explorer/onNewNotebookClicked");
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error);
|
||||
@@ -3093,7 +3001,7 @@ export default class Explorer implements ViewModels.Explorer {
|
||||
dataExplorerArea: Constants.Areas.Notebook
|
||||
});
|
||||
|
||||
this.notebookContentClient
|
||||
this.notebookManager?.notebookContentClient
|
||||
.createNewNotebookFile(parent)
|
||||
.then((newFile: NotebookContentItem) => {
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, `Successfully created: ${newFile.name}`);
|
||||
@@ -3109,7 +3017,7 @@ export default class Explorer implements ViewModels.Explorer {
|
||||
return this.openNotebook(newFile);
|
||||
})
|
||||
.then(() => this.resourceTree.triggerRender())
|
||||
.catch(reason => {
|
||||
.catch((reason: any) => {
|
||||
const error = `Failed to create a new notebook: ${reason}`;
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error);
|
||||
TelemetryProcessor.traceFailure(
|
||||
@@ -3159,14 +3067,14 @@ export default class Explorer implements ViewModels.Explorer {
|
||||
}
|
||||
|
||||
public refreshContentItem(item: NotebookContentItem): Promise<void> {
|
||||
if (!this.isNotebookEnabled() || !this.notebookContentClient) {
|
||||
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
|
||||
const error = "Attempt to refresh notebook list, but notebook is not enabled";
|
||||
Logger.logError(error, "Explorer/refreshContentItem");
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error);
|
||||
return Promise.reject(new Error(error));
|
||||
}
|
||||
|
||||
return this.notebookContentClient.updateItemChildren(item);
|
||||
return this.notebookManager?.notebookContentClient.updateItemChildren(item);
|
||||
}
|
||||
|
||||
public getNotebookBasePath(): string {
|
||||
@@ -3235,7 +3143,7 @@ export default class Explorer implements ViewModels.Explorer {
|
||||
newTab.onTabClick();
|
||||
}
|
||||
|
||||
public openGallery() {
|
||||
public async openGallery(notebookUrl?: string, galleryItem?: IGalleryItem, isFavorite?: boolean) {
|
||||
let title: string;
|
||||
let hashLocation: string;
|
||||
|
||||
@@ -3250,25 +3158,34 @@ export default class Explorer implements ViewModels.Explorer {
|
||||
if (openedTabs[i].hashLocation() == hashLocation) {
|
||||
openedTabs[i].onTabClick();
|
||||
openedTabs[i].onActivate();
|
||||
(openedTabs[i] as any).updateGalleryParams(notebookUrl, galleryItem, isFavorite);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.galleryTab) {
|
||||
this.galleryTab = await import(/* webpackChunkName: "GalleryTab" */ "./Tabs/GalleryTab");
|
||||
}
|
||||
|
||||
const newTab = new this.galleryTab.default({
|
||||
// GalleryTabOptions
|
||||
account: CosmosClient.databaseAccount(),
|
||||
container: this,
|
||||
junoClient: this.notebookManager?.junoClient,
|
||||
notebookUrl,
|
||||
galleryItem,
|
||||
isFavorite,
|
||||
// TabOptions
|
||||
tabKind: ViewModels.CollectionTabKind.Gallery,
|
||||
node: null,
|
||||
title: title,
|
||||
tabPath: title,
|
||||
documentClientUtility: null,
|
||||
collection: null,
|
||||
selfLink: null,
|
||||
hashLocation: hashLocation,
|
||||
isActive: ko.observable(false),
|
||||
hashLocation: hashLocation,
|
||||
onUpdateTabsButtons: this.onUpdateTabsButtons,
|
||||
isTabsContentExpanded: ko.observable(true),
|
||||
onLoadStartKey: null,
|
||||
onUpdateTabsButtons: this.onUpdateTabsButtons,
|
||||
container: this,
|
||||
openedTabs: this.openedTabs()
|
||||
});
|
||||
|
||||
@@ -3278,16 +3195,14 @@ export default class Explorer implements ViewModels.Explorer {
|
||||
newTab.onTabClick();
|
||||
}
|
||||
|
||||
public openNotebookViewer(
|
||||
notebookUrl: string,
|
||||
notebookMetadata: DataModels.NotebookMetadata,
|
||||
onNotebookMetadataChange: (newNotebookMetadata: DataModels.NotebookMetadata) => Promise<void>,
|
||||
isLikedNotebook: boolean
|
||||
) {
|
||||
const notebookName = path.basename(notebookUrl);
|
||||
const title = notebookName;
|
||||
public async openNotebookViewer(notebookUrl: string) {
|
||||
const title = path.basename(notebookUrl);
|
||||
const hashLocation = notebookUrl;
|
||||
|
||||
if (!this.notebookViewerTab) {
|
||||
this.notebookViewerTab = await import(/* webpackChunkName: "NotebookViewerTab" */ "./Tabs/NotebookViewerTab");
|
||||
}
|
||||
|
||||
const notebookViewerTabModule = this.notebookViewerTab;
|
||||
|
||||
let isNotebookViewerOpen = (tab: ViewModels.Tab) => {
|
||||
@@ -3323,11 +3238,7 @@ export default class Explorer implements ViewModels.Explorer {
|
||||
onUpdateTabsButtons: this.onUpdateTabsButtons,
|
||||
container: this,
|
||||
openedTabs: this.openedTabs(),
|
||||
notebookUrl: notebookUrl,
|
||||
notebookName: notebookName,
|
||||
notebookMetadata: notebookMetadata,
|
||||
onNotebookMetadataChange: onNotebookMetadataChange,
|
||||
isLikedNotebook: isLikedNotebook
|
||||
notebookUrl
|
||||
});
|
||||
|
||||
this.openedTabs.push(newTab);
|
||||
|
||||
@@ -11,7 +11,6 @@ import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import * as DataModels from "../../../Contracts/DataModels";
|
||||
import * as StorageUtility from "../../../Shared/StorageUtility";
|
||||
import GraphTab from "../../Tabs/GraphTab";
|
||||
import DocumentClientUtilityBase from "../../../Common/DocumentClientUtilityBase";
|
||||
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
|
||||
describe("Check whether query result is vertex array", () => {
|
||||
@@ -59,10 +58,74 @@ describe("Check whether query result is edge-vertex array", () => {
|
||||
|
||||
describe("Create proper pkid pair", () => {
|
||||
it("should enclose string pk with quotes", () => {
|
||||
expect(GraphExplorer.generatePkIdPair("test", "id")).toEqual('["test", "id"]');
|
||||
expect(GraphExplorer.generatePkIdPair("test", "id")).toEqual("['test', 'id']");
|
||||
});
|
||||
|
||||
it("should not enclose non-string pk with quotes", () => {
|
||||
expect(GraphExplorer.generatePkIdPair(2, "id")).toEqual('[2, "id"]');
|
||||
expect(GraphExplorer.generatePkIdPair(2, "id")).toEqual("[2, 'id']");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getPkIdFromDocumentId", () => {
|
||||
const createFakeDoc = (override: any) => ({
|
||||
_rid: "_rid",
|
||||
_self: "_self",
|
||||
_etag: "_etag",
|
||||
_ts: 1234,
|
||||
...override
|
||||
});
|
||||
|
||||
it("should create pkid pair from non-partitioned graph", () => {
|
||||
const doc = createFakeDoc({ id: "id" });
|
||||
expect(GraphExplorer.getPkIdFromDocumentId(doc, undefined)).toEqual("'id'");
|
||||
expect(GraphExplorer.getPkIdFromDocumentId(doc, "_partitiongKey")).toEqual("'id'");
|
||||
});
|
||||
|
||||
it("should create pkid pair from partitioned graph (pk as string)", () => {
|
||||
const doc = createFakeDoc({ id: "id", mypk: "pkvalue" });
|
||||
expect(GraphExplorer.getPkIdFromDocumentId(doc, "mypk")).toEqual("['pkvalue', 'id']");
|
||||
});
|
||||
|
||||
it("should create pkid pair from partitioned graph (pk as valid array value)", () => {
|
||||
const doc = createFakeDoc({ id: "id", mypk: [{ id: "someid", _value: "pkvalue" }] });
|
||||
expect(GraphExplorer.getPkIdFromDocumentId(doc, "mypk")).toEqual("['pkvalue', 'id']");
|
||||
});
|
||||
|
||||
it("should error if id is not a string", () => {
|
||||
const doc = createFakeDoc({ id: { foo: 1 } });
|
||||
try {
|
||||
GraphExplorer.getPkIdFromDocumentId(doc, undefined);
|
||||
expect(true).toBe(false);
|
||||
} catch (e) {
|
||||
expect(true).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("should error if pk not string nor non-empty array", () => {
|
||||
let doc = createFakeDoc({ mypk: { foo: 1 } });
|
||||
|
||||
try {
|
||||
GraphExplorer.getPkIdFromDocumentId(doc, "mypk");
|
||||
} catch (e) {
|
||||
expect(true).toBe(true);
|
||||
}
|
||||
|
||||
doc = createFakeDoc({ mypk: [] });
|
||||
try {
|
||||
GraphExplorer.getPkIdFromDocumentId(doc, "mypk");
|
||||
expect(true).toBe(false);
|
||||
} catch (e) {
|
||||
expect(true).toBe(true);
|
||||
}
|
||||
|
||||
// Array must be [{ id: string, _value: string }]
|
||||
doc = createFakeDoc({ mypk: [{ foo: 1 }] });
|
||||
try {
|
||||
GraphExplorer.getPkIdFromDocumentId(doc, "mypk");
|
||||
expect(true).toBe(false);
|
||||
} catch (e) {
|
||||
expect(true).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -253,11 +316,11 @@ describe("GraphExplorer", () => {
|
||||
};
|
||||
|
||||
const createFetchOutEQuery = (vertexId: string, limit: number): string => {
|
||||
return `g.V("${vertexId}").outE().limit(${limit}).as('e').inV().as('v').select('e', 'v')`;
|
||||
return `g.V('${vertexId}').outE().limit(${limit}).as('e').inV().as('v').select('e', 'v')`;
|
||||
};
|
||||
|
||||
const createFetchInEQuery = (vertexId: string, limit: number): string => {
|
||||
return `g.V("${vertexId}").inE().limit(${limit}).as('e').outV().as('v').select('e', 'v')`;
|
||||
return `g.V('${vertexId}').inE().limit(${limit}).as('e').outV().as('v').select('e', 'v')`;
|
||||
};
|
||||
|
||||
const isVisible = (selector: string): boolean => {
|
||||
@@ -293,7 +356,7 @@ describe("GraphExplorer", () => {
|
||||
describe("Load Graph button", () => {
|
||||
beforeEach(async done => {
|
||||
const backendResponses: BackendResponses = {};
|
||||
backendResponses["g.V()"] = backendResponses['g.V("1")'] = {
|
||||
backendResponses["g.V()"] = backendResponses["g.V('1')"] = {
|
||||
response: [{ id: "1", type: "vertex" }],
|
||||
isLast: false
|
||||
};
|
||||
@@ -341,7 +404,7 @@ describe("GraphExplorer", () => {
|
||||
describe("Execute Gremlin Query button", () => {
|
||||
beforeEach(done => {
|
||||
const backendResponses: BackendResponses = {};
|
||||
backendResponses["g.V()"] = backendResponses['g.V("2")'] = {
|
||||
backendResponses["g.V()"] = backendResponses["g.V('2')"] = {
|
||||
response: [{ id: "2", type: "vertex" }],
|
||||
isLast: false
|
||||
};
|
||||
@@ -411,7 +474,7 @@ describe("GraphExplorer", () => {
|
||||
beforeEach(done => {
|
||||
const backendResponses: BackendResponses = {};
|
||||
// TODO Make this less dependent on spaces, order and quotes
|
||||
backendResponses["g.V()"] = backendResponses[`g.V("${node1Id}","${node2Id}")`] = {
|
||||
backendResponses["g.V()"] = backendResponses[`g.V('${node1Id}','${node2Id}')`] = {
|
||||
response: [
|
||||
{
|
||||
id: node1Id,
|
||||
@@ -667,7 +730,7 @@ describe("GraphExplorer", () => {
|
||||
describe("when isGraphAutoVizDisabled setting is true (autoviz disabled)", () => {
|
||||
beforeEach(done => {
|
||||
const backendResponses: BackendResponses = {};
|
||||
backendResponses["g.V()"] = backendResponses['g.V("3")'] = {
|
||||
backendResponses["g.V()"] = backendResponses["g.V('3')"] = {
|
||||
response: [{ id: "3", type: "vertex" }],
|
||||
isLast: true
|
||||
};
|
||||
|
||||
@@ -327,8 +327,8 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
* @param id
|
||||
*/
|
||||
public static generatePkIdPair(pk: PartitionKeyValueType, id: string) {
|
||||
const pkStr = typeof pk === "string" ? `"${pk}"` : `${pk}`;
|
||||
return `[${pkStr}, "${GraphUtil.escapeDoubleQuotes(id)}"]`;
|
||||
const pkStr = typeof pk === "string" ? `'${pk}'` : `${pk}`;
|
||||
return `[${pkStr}, '${GraphUtil.escapeSingleQuotes(id)}']`;
|
||||
}
|
||||
|
||||
public updateVertexProperties(editedProperties: EditedProperties): Q.Promise<GremlinClient.GremlinRequestResult> {
|
||||
@@ -1335,7 +1335,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
const pk = v.properties[this.props.collectionPartitionKeyProperty][0].value;
|
||||
return GraphExplorer.generatePkIdPair(pk, v.id);
|
||||
} else {
|
||||
return `"${GraphUtil.escapeDoubleQuotes(v.id)}"`;
|
||||
return `'${GraphUtil.escapeSingleQuotes(v.id)}'`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1361,15 +1361,34 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
/**
|
||||
* If collection is not partitioned, return 'id'.
|
||||
* If collection is partitioned, return pk-id pair.
|
||||
* public for testing purposes
|
||||
* @param vertex
|
||||
* @return id
|
||||
*/
|
||||
private getPkIdFromDocumentId(d: DataModels.DocumentId): string {
|
||||
if (this.props.collectionPartitionKeyProperty && d.hasOwnProperty(this.props.collectionPartitionKeyProperty)) {
|
||||
const pk = (d as any)[this.props.collectionPartitionKeyProperty];
|
||||
return GraphExplorer.generatePkIdPair(pk, d.id);
|
||||
public static getPkIdFromDocumentId(d: DataModels.DocumentId, collectionPartitionKeyProperty: string): string {
|
||||
let { id } = d;
|
||||
if (typeof id !== "string") {
|
||||
const error = `Vertex id is not a string: ${JSON.stringify(id)}.`;
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error);
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
if (collectionPartitionKeyProperty && d.hasOwnProperty(collectionPartitionKeyProperty)) {
|
||||
let pk = (d as any)[collectionPartitionKeyProperty];
|
||||
if (typeof pk !== "string") {
|
||||
if (Array.isArray(pk) && pk.length > 0) {
|
||||
// pk is [{ id: 'id', _value: 'value' }]
|
||||
pk = pk[0]["_value"];
|
||||
} else {
|
||||
return `"${GraphUtil.escapeDoubleQuotes(d.id)}"`;
|
||||
const error = `Vertex pk is not a string nor a non-empty array: ${JSON.stringify(pk)}.`;
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error);
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
|
||||
return GraphExplorer.generatePkIdPair(pk, id);
|
||||
} else {
|
||||
return `'${GraphUtil.escapeSingleQuotes(id)}'`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1769,7 +1788,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
const documents = results.documents || [];
|
||||
return documents.map(
|
||||
(item: DataModels.DocumentId) => {
|
||||
return this.getPkIdFromDocumentId(item);
|
||||
return GraphExplorer.getPkIdFromDocumentId(item, this.props.collectionPartitionKeyProperty);
|
||||
},
|
||||
(reason: any) => {
|
||||
// Failure
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as sinon from "sinon";
|
||||
import { GremlinClient, GremlinClientParameters } from "./GremlinClient";
|
||||
import { NotificationConsoleUtils } from "../../../Utils/NotificationConsoleUtils";
|
||||
import { Logger } from "../../../Common/Logger";
|
||||
import * as Logger from "../../../Common/Logger";
|
||||
|
||||
describe("Gremlin Client", () => {
|
||||
const emptyParams: GremlinClientParameters = {
|
||||
|
||||
@@ -7,7 +7,7 @@ import { GremlinSimpleClient, Result } from "./GremlinSimpleClient";
|
||||
import { NotificationConsoleUtils } from "../../../Utils/NotificationConsoleUtils";
|
||||
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import { HashMap } from "../../../Common/HashMap";
|
||||
import { Logger } from "../../../Common/Logger";
|
||||
import * as Logger from "../../../Common/Logger";
|
||||
|
||||
export interface GremlinClientParameters {
|
||||
endpoint: string;
|
||||
|
||||
@@ -40,6 +40,7 @@ export class QueryContainerComponent extends React.Component<
|
||||
submitFct={(inputValue: string, selection: InputTypeaheadComponent.Item) =>
|
||||
this.onSubmit(inputValue, selection)
|
||||
}
|
||||
useTextarea={true}
|
||||
/>
|
||||
{this.renderQueryInputButton()}
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@ import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { CommandBarComponentButtonFactory } from "./CommandBarComponentButtonFactory";
|
||||
import { ExplorerStub } from "../../OpenActionsStubs";
|
||||
import { GitHubOAuthService } from "../../../GitHub/GitHubOAuthService";
|
||||
import NotebookManager from "../../Notebook/NotebookManager";
|
||||
|
||||
describe("CommandBarComponentButtonFactory tests", () => {
|
||||
let mockExplorer: ViewModels.Explorer;
|
||||
@@ -19,6 +20,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
mockExplorer.isPreferredApiCassandra = ko.computed<boolean>(() => false);
|
||||
mockExplorer.isSparkEnabled = ko.observable(true);
|
||||
mockExplorer.isGalleryEnabled = ko.computed<boolean>(() => false);
|
||||
mockExplorer.isGalleryPublishEnabled = ko.computed<boolean>(() => false);
|
||||
mockExplorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
|
||||
mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
|
||||
});
|
||||
@@ -81,6 +83,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
mockExplorer.isPreferredApiCassandra = ko.computed<boolean>(() => false);
|
||||
mockExplorer.isSparkEnabled = ko.observable(true);
|
||||
mockExplorer.isGalleryEnabled = ko.computed<boolean>(() => false);
|
||||
mockExplorer.isGalleryPublishEnabled = ko.computed<boolean>(() => false);
|
||||
mockExplorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
|
||||
mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
|
||||
});
|
||||
@@ -161,6 +164,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false);
|
||||
mockExplorer.isSparkEnabled = ko.observable(true);
|
||||
mockExplorer.isGalleryEnabled = ko.computed<boolean>(() => false);
|
||||
mockExplorer.isGalleryPublishEnabled = ko.computed<boolean>(() => false);
|
||||
mockExplorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
|
||||
mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
|
||||
});
|
||||
@@ -247,7 +251,9 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
mockExplorer.isNotebooksEnabledForAccount = ko.observable(false);
|
||||
mockExplorer.isRunningOnNationalCloud = ko.observable(false);
|
||||
mockExplorer.isGalleryEnabled = ko.computed<boolean>(() => false);
|
||||
mockExplorer.gitHubOAuthService = new GitHubOAuthService(undefined);
|
||||
mockExplorer.isGalleryPublishEnabled = ko.computed<boolean>(() => false);
|
||||
mockExplorer.notebookManager = new NotebookManager();
|
||||
mockExplorer.notebookManager.gitHubOAuthService = new GitHubOAuthService(undefined);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -268,7 +274,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
|
||||
it("Notebooks is enabled and GitHubOAuthService is logged in - manage github settings button should be visible", () => {
|
||||
mockExplorer.isNotebookEnabled = ko.observable(true);
|
||||
mockExplorer.gitHubOAuthService.isLoggedIn = jest.fn().mockReturnValue(true);
|
||||
mockExplorer.notebookManager.gitHubOAuthService.isLoggedIn = jest.fn().mockReturnValue(true);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
|
||||
const manageGitHubSettingsBtn = buttons.find(
|
||||
|
||||
@@ -26,7 +26,6 @@ import EnableNotebooksIcon from "../../../../images/notebook/Notebook-enable.svg
|
||||
import NewNotebookIcon from "../../../../images/notebook/Notebook-new.svg";
|
||||
import ResetWorkspaceIcon from "../../../../images/notebook/Notebook-reset-workspace.svg";
|
||||
import LibraryManageIcon from "../../../../images/notebook/Spark-library-manage.svg";
|
||||
import GalleryIcon from "../../../../images/GalleryIcon.svg";
|
||||
import GitHubIcon from "../../../../images/github.svg";
|
||||
import SynapseIcon from "../../../../images/synapse-link.svg";
|
||||
import { config, Platform } from "../../../Config";
|
||||
@@ -64,7 +63,7 @@ export class CommandBarComponentButtonFactory {
|
||||
];
|
||||
buttons.push(newNotebookButton);
|
||||
|
||||
if (container.gitHubOAuthService) {
|
||||
if (container.notebookManager?.gitHubOAuthService) {
|
||||
buttons.push(CommandBarComponentButtonFactory.createManageGitHubAccountButton(container));
|
||||
}
|
||||
}
|
||||
@@ -87,10 +86,6 @@ export class CommandBarComponentButtonFactory {
|
||||
buttons.push(CommandBarComponentButtonFactory.createOpenTerminalButton(container));
|
||||
|
||||
buttons.push(CommandBarComponentButtonFactory.createNotebookWorkspaceResetButton(container));
|
||||
|
||||
if (container.isGalleryEnabled()) {
|
||||
buttons.push(CommandBarComponentButtonFactory.createGalleryButton(container));
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Should be replaced with the create arcadia spark pool button
|
||||
@@ -575,19 +570,6 @@ export class CommandBarComponentButtonFactory {
|
||||
};
|
||||
}
|
||||
|
||||
private static createGalleryButton(container: ViewModels.Explorer): ViewModels.NavbarButtonConfig {
|
||||
const label = "View Gallery";
|
||||
return {
|
||||
iconSrc: GalleryIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => container.openGallery(),
|
||||
commandButtonLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: false,
|
||||
ariaLabel: label
|
||||
};
|
||||
}
|
||||
|
||||
private static createOpenMongoTerminalButton(container: ViewModels.Explorer): ViewModels.NavbarButtonConfig {
|
||||
const label = "Open Mongo Shell";
|
||||
const tooltip =
|
||||
@@ -654,7 +636,7 @@ export class CommandBarComponentButtonFactory {
|
||||
}
|
||||
|
||||
private static createManageGitHubAccountButton(container: ViewModels.Explorer): ViewModels.NavbarButtonConfig {
|
||||
let connectedToGitHub: boolean = container.gitHubOAuthService.isLoggedIn();
|
||||
let connectedToGitHub: boolean = container.notebookManager?.gitHubOAuthService.isLoggedIn();
|
||||
const label = connectedToGitHub ? "Manage GitHub settings" : "Connect to GitHub";
|
||||
return {
|
||||
iconSrc: GitHubIcon,
|
||||
|
||||
@@ -36,11 +36,12 @@ export class CommandBarUtil {
|
||||
|
||||
const result: ICommandBarItemProps = {
|
||||
iconProps: {
|
||||
iconType: IconType.image,
|
||||
style: {
|
||||
width: StyleConstants.CommandBarIconWidth // 16
|
||||
width: StyleConstants.CommandBarIconWidth, // 16
|
||||
alignSelf: btn.iconName ? "baseline" : undefined
|
||||
},
|
||||
imageProps: { src: btn.iconSrc, alt: btn.iconAlt }
|
||||
imageProps: btn.iconSrc ? { src: btn.iconSrc, alt: btn.iconAlt } : undefined,
|
||||
iconName: btn.iconName
|
||||
},
|
||||
onClick: btn.onCommandClick,
|
||||
key: `${btn.commandButtonLabel}${index}`,
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import * as ko from "knockout";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { CommandButtonOptions } from "./../Controls/CommandButton/CommandButton";
|
||||
|
||||
export default class ContextMenu implements ViewModels.ContextMenu {
|
||||
public container: ViewModels.Explorer;
|
||||
public visible: ko.Observable<boolean>;
|
||||
public elementId: string;
|
||||
public options: ko.ObservableArray<CommandButtonOptions>;
|
||||
public tabIndex: ko.Observable<number>;
|
||||
|
||||
constructor(container: ViewModels.Explorer, rid: string) {
|
||||
this.container = container;
|
||||
this.visible = ko.observable<boolean>(false);
|
||||
this.elementId = `contextMenu${rid}`;
|
||||
this.options = ko.observableArray<CommandButtonOptions>([]);
|
||||
this.tabIndex = ko.observable<number>(0);
|
||||
}
|
||||
|
||||
public show(source: ViewModels.TreeNode, event: MouseEvent) {
|
||||
if (source && source.contextMenu && source.contextMenu.visible && source.contextMenu.visible()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.container.selectedNode(source);
|
||||
const elementId = source.contextMenu.elementId;
|
||||
const htmlElement = document.getElementById(elementId);
|
||||
htmlElement.style.left = `${event.clientX}px`;
|
||||
htmlElement.style.top = `${event.clientY}px`;
|
||||
|
||||
!!source.contextMenu && source.contextMenu.visible(true);
|
||||
source.contextMenu.tabIndex(0);
|
||||
htmlElement.focus();
|
||||
}
|
||||
|
||||
public hide(source: ViewModels.TreeNode, event: MouseEvent) {
|
||||
if (!source || !source.contextMenu || !source.contextMenu.visible || !source.contextMenu.visible()) {
|
||||
return;
|
||||
}
|
||||
source.contextMenu.tabIndex(-1);
|
||||
source.contextMenu.visible(false);
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
} from "@nteract/core";
|
||||
import * as Immutable from "immutable";
|
||||
import { Provider } from "react-redux";
|
||||
import { CellType, CellId } from "@nteract/commutable";
|
||||
import { CellType, CellId, toJS } from "@nteract/commutable";
|
||||
import { Store, AnyAction } from "redux";
|
||||
|
||||
import "./NotebookComponent.less";
|
||||
@@ -71,6 +71,28 @@ export class NotebookComponentBootstrapper {
|
||||
);
|
||||
}
|
||||
|
||||
public getContent(): { name: string; content: string } {
|
||||
const record = this.getStore()
|
||||
.getState()
|
||||
.core.entities.contents.byRef.get(this.contentRef);
|
||||
let content: string;
|
||||
switch (record.model.type) {
|
||||
case "notebook":
|
||||
content = JSON.stringify(toJS(record.model.notebook));
|
||||
break;
|
||||
case "file":
|
||||
content = record.model.text;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported model type ${record.model.type}`);
|
||||
}
|
||||
|
||||
return {
|
||||
name: NotebookUtil.getName(record.filepath),
|
||||
content
|
||||
};
|
||||
}
|
||||
|
||||
public setContent(name: string, content: any): void {
|
||||
this.getStore().dispatch(
|
||||
actions.fetchContentFulfilled({
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ServerConfig, IContentProvider, FileType, IContent, IGetParams } from "
|
||||
import { Observable } from "rxjs";
|
||||
import { AjaxResponse } from "rxjs/ajax";
|
||||
import { GitHubContentProvider } from "../../../GitHub/GitHubContentProvider";
|
||||
import { GitHubUtils } from "../../../Utils/GitHubUtils";
|
||||
import * as GitHubUtils from "../../../Utils/GitHubUtils";
|
||||
|
||||
export class NotebookContentProvider implements IContentProvider {
|
||||
constructor(private gitHubContentProvider: GitHubContentProvider, private jupyterContentProvider: IContentProvider) {}
|
||||
|
||||
@@ -6,7 +6,7 @@ import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import * as Constants from "../../Common/Constants";
|
||||
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils";
|
||||
import { Logger } from "../../Common/Logger";
|
||||
import * as Logger from "../../Common/Logger";
|
||||
|
||||
export class NotebookContainerClient implements ViewModels.INotebookContainerClient {
|
||||
private reconnectingNotificationId: string;
|
||||
|
||||
168
src/Explorer/Notebook/NotebookManager.ts
Normal file
168
src/Explorer/Notebook/NotebookManager.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
/*
|
||||
* Contains all notebook related stuff meant to be dynamically loaded by explorer
|
||||
*/
|
||||
|
||||
import { JunoClient } from "../../Juno/JunoClient";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { GitHubOAuthService } from "../../GitHub/GitHubOAuthService";
|
||||
import { GitHubClient } from "../../GitHub/GitHubClient";
|
||||
import { config } from "../../Config";
|
||||
import * as Logger from "../../Common/Logger";
|
||||
import { HttpStatusCodes, Areas } from "../../Common/Constants";
|
||||
import { GitHubReposPane } from "../Panes/GitHubReposPane";
|
||||
import ko from "knockout";
|
||||
import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
import { IContentProvider } from "@nteract/core";
|
||||
import { NotebookContentProvider } from "./NotebookComponent/NotebookContentProvider";
|
||||
import { GitHubContentProvider } from "../../GitHub/GitHubContentProvider";
|
||||
import { contents } from "rx-jupyter";
|
||||
import { NotebookContainerClient } from "./NotebookContainerClient";
|
||||
import { MemoryUsageInfo } from "../../Contracts/DataModels";
|
||||
import { NotebookContentClient } from "./NotebookContentClient";
|
||||
import { DialogProps } from "../Controls/DialogReactComponent/DialogComponent";
|
||||
import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter";
|
||||
import { PublishNotebookPaneAdapter } from "../Panes/PublishNotebookPaneAdapter";
|
||||
import { getFullName } from "../../Utils/UserUtils";
|
||||
|
||||
export interface NotebookManagerOptions {
|
||||
container: ViewModels.Explorer;
|
||||
notebookBasePath: ko.Observable<string>;
|
||||
dialogProps: ko.Observable<DialogProps>;
|
||||
resourceTree: ResourceTreeAdapter;
|
||||
refreshCommandBarButtons: () => void;
|
||||
refreshNotebookList: () => void;
|
||||
}
|
||||
|
||||
export default class NotebookManager {
|
||||
private params: NotebookManagerOptions;
|
||||
public junoClient: JunoClient;
|
||||
|
||||
public notebookContentProvider: IContentProvider;
|
||||
public notebookClient: ViewModels.INotebookContainerClient;
|
||||
public notebookContentClient: ViewModels.INotebookContentClient;
|
||||
|
||||
private gitHubContentProvider: GitHubContentProvider;
|
||||
public gitHubOAuthService: GitHubOAuthService;
|
||||
private gitHubClient: GitHubClient;
|
||||
|
||||
public gitHubReposPane: ViewModels.ContextualPane;
|
||||
public publishNotebookPaneAdapter: PublishNotebookPaneAdapter;
|
||||
|
||||
public initialize(params: NotebookManagerOptions): void {
|
||||
this.params = params;
|
||||
this.junoClient = new JunoClient(this.params.container.databaseAccount);
|
||||
|
||||
this.gitHubOAuthService = new GitHubOAuthService(this.junoClient);
|
||||
this.gitHubClient = new GitHubClient(config.AZURESAMPLESCOSMOSDBPAT, this.onGitHubClientError);
|
||||
this.gitHubReposPane = new GitHubReposPane({
|
||||
documentClientUtility: this.params.container.documentClientUtility,
|
||||
id: "gitHubReposPane",
|
||||
visible: ko.observable<boolean>(false),
|
||||
container: this.params.container,
|
||||
junoClient: this.junoClient,
|
||||
gitHubClient: this.gitHubClient
|
||||
});
|
||||
|
||||
this.gitHubContentProvider = new GitHubContentProvider({
|
||||
gitHubClient: this.gitHubClient,
|
||||
promptForCommitMsg: this.promptForCommitMsg
|
||||
});
|
||||
|
||||
this.notebookContentProvider = new NotebookContentProvider(
|
||||
this.gitHubContentProvider,
|
||||
contents.JupyterContentProvider
|
||||
);
|
||||
|
||||
this.notebookClient = new NotebookContainerClient(
|
||||
this.params.container.notebookServerInfo,
|
||||
() => this.params.container.initNotebooks(this.params.container.databaseAccount()),
|
||||
(update: MemoryUsageInfo) => this.params.container.memoryUsageInfo(update)
|
||||
);
|
||||
|
||||
this.notebookContentClient = new NotebookContentClient(
|
||||
this.params.container.notebookServerInfo,
|
||||
this.params.notebookBasePath,
|
||||
this.notebookContentProvider
|
||||
);
|
||||
|
||||
if (this.params.container.isGalleryPublishEnabled()) {
|
||||
this.publishNotebookPaneAdapter = new PublishNotebookPaneAdapter(this.params.container, this.junoClient);
|
||||
}
|
||||
|
||||
this.gitHubOAuthService.getTokenObservable().subscribe(token => {
|
||||
this.gitHubClient.setToken(token?.access_token ? token.access_token : config.AZURESAMPLESCOSMOSDBPAT);
|
||||
|
||||
if (this.gitHubReposPane.visible()) {
|
||||
this.gitHubReposPane.open();
|
||||
}
|
||||
|
||||
this.params.refreshCommandBarButtons();
|
||||
this.params.refreshNotebookList();
|
||||
});
|
||||
|
||||
this.junoClient.subscribeToPinnedRepos(pinnedRepos => {
|
||||
this.params.resourceTree.initializeGitHubRepos(pinnedRepos);
|
||||
this.params.resourceTree.triggerRender();
|
||||
});
|
||||
this.junoClient.getPinnedRepos(this.gitHubOAuthService.getTokenObservable()()?.scope);
|
||||
}
|
||||
|
||||
public openPublishNotebookPane(name: string, content: string): void {
|
||||
this.publishNotebookPaneAdapter.open(name, getFullName(), content);
|
||||
}
|
||||
|
||||
// Octokit's error handler uses any
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private onGitHubClientError = (error: any): void => {
|
||||
Logger.logError(error, "NotebookManager/onGitHubClientError");
|
||||
|
||||
if (error.status === HttpStatusCodes.Unauthorized) {
|
||||
this.gitHubOAuthService.resetToken();
|
||||
|
||||
this.params.container.showOkCancelModalDialog(
|
||||
undefined,
|
||||
"Cosmos DB cannot access your Github account anymore. Please connect to GitHub again.",
|
||||
"Connect to GitHub",
|
||||
() => this.gitHubReposPane.open(),
|
||||
"Cancel",
|
||||
undefined
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
private promptForCommitMsg = (title: string, primaryButtonLabel: string) => {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
let commitMsg = "Committed from Azure Cosmos DB Notebooks";
|
||||
this.params.container.showOkCancelTextFieldModalDialog(
|
||||
title || "Commit",
|
||||
undefined,
|
||||
primaryButtonLabel || "Commit",
|
||||
() => {
|
||||
TelemetryProcessor.trace(Action.NotebooksGitHubCommit, ActionModifiers.Mark, {
|
||||
databaseAccountName:
|
||||
this.params.container.databaseAccount() && this.params.container.databaseAccount().name,
|
||||
defaultExperience: this.params.container.defaultExperience && this.params.container.defaultExperience(),
|
||||
dataExplorerArea: Areas.Notebook
|
||||
});
|
||||
resolve(commitMsg);
|
||||
},
|
||||
"Cancel",
|
||||
() => reject(new Error("Commit dialog canceled")),
|
||||
{
|
||||
label: "Commit message",
|
||||
autoAdjustHeight: true,
|
||||
multiline: true,
|
||||
defaultValue: commitMsg,
|
||||
rows: 3,
|
||||
onChange: (_, newValue: string) => {
|
||||
commitMsg = newValue;
|
||||
this.params.dialogProps().primaryButtonDisabled = !commitMsg;
|
||||
this.params.dialogProps.valueHasMutated();
|
||||
}
|
||||
},
|
||||
!commitMsg
|
||||
);
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { NotebookUtil } from "./NotebookUtil";
|
||||
import { GitHubUtils } from "../../Utils/GitHubUtils";
|
||||
import * as GitHubUtils from "../../Utils/GitHubUtils";
|
||||
|
||||
const fileName = "file";
|
||||
const notebookName = "file.ipynb";
|
||||
|
||||
@@ -2,7 +2,7 @@ import path from "path";
|
||||
import { ImmutableNotebook } from "@nteract/commutable";
|
||||
import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem";
|
||||
import { StringUtils } from "../../Utils/StringUtils";
|
||||
import { GitHubUtils } from "../../Utils/GitHubUtils";
|
||||
import * as GitHubUtils from "../../Utils/GitHubUtils";
|
||||
|
||||
// Must match rx-jupyter' FileType
|
||||
export type FileType = "directory" | "file" | "notebook";
|
||||
|
||||
@@ -6,8 +6,6 @@ import Q from "q";
|
||||
import { ArcadiaWorkspaceItem } from "./Controls/Arcadia/ArcadiaMenuPicker";
|
||||
import { CassandraTableKey, CassandraTableKeys, TableDataClient } from "../../src/Explorer/Tables/TableDataClient";
|
||||
import { ConsoleData } from "../../src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import { GitHubOAuthService } from "../GitHub/GitHubOAuthService";
|
||||
import { IContentProvider } from "@nteract/core";
|
||||
import { MostRecentActivity } from "./MostRecentActivity/MostRecentActivity";
|
||||
import { NotebookContentItem } from "./Notebook/NotebookContentItem";
|
||||
import { PlatformType } from "../../src/PlatformType";
|
||||
@@ -19,8 +17,11 @@ import { TableColumnOptionsPane } from "../../src/Explorer/Panes/Tables/TableCol
|
||||
import { TextFieldProps } from "./Controls/DialogReactComponent/DialogComponent";
|
||||
import { UploadDetails } from "../workers/upload/definitions";
|
||||
import { UploadFilePane } from "./Panes/UploadFilePane";
|
||||
import { UploadItemsPaneAdapter } from "./Panes/UploadItemsPaneAdapter";
|
||||
import { Versions } from "../../src/Contracts/ExplorerContracts";
|
||||
import { CollectionCreationDefaults } from "../Shared/Constants";
|
||||
import { IGalleryItem } from "../Juno/JunoClient";
|
||||
import { ReactAdapter } from "../Bindings/ReactBindingHandler";
|
||||
|
||||
export class ExplorerStub implements ViewModels.Explorer {
|
||||
public flight: ko.Observable<string>;
|
||||
@@ -86,6 +87,7 @@ export class ExplorerStub implements ViewModels.Explorer {
|
||||
public settingsPane: ViewModels.SettingsPane;
|
||||
public executeSprocParamsPane: ViewModels.ExecuteSprocParamsPane;
|
||||
public uploadItemsPane: ViewModels.UploadItemsPane;
|
||||
public uploadItemsPaneAdapter: UploadItemsPaneAdapter;
|
||||
public loadQueryPane: ViewModels.LoadQueryPane;
|
||||
public saveQueryPane: ViewModels.ContextualPane;
|
||||
public browseQueriesPane: ViewModels.BrowseQueriesPane;
|
||||
@@ -95,8 +97,10 @@ export class ExplorerStub implements ViewModels.Explorer {
|
||||
public setupSparkClusterPane: ViewModels.ContextualPane;
|
||||
public manageSparkClusterPane: ViewModels.ContextualPane;
|
||||
public isGalleryEnabled: ko.Computed<boolean>;
|
||||
public isGalleryPublishEnabled: ko.Computed<boolean>;
|
||||
public isGitHubPaneEnabled: ko.Observable<boolean>;
|
||||
public isGraphsEnabled: ko.Computed<boolean>;
|
||||
public isPublishNotebookPaneEnabled: ko.Observable<boolean>;
|
||||
public isRightPanelV2Enabled: ko.Computed<boolean>;
|
||||
public canExceedMaximumValue: ko.Computed<boolean>;
|
||||
public isHostedDataExplorerEnabled: ko.Computed<boolean>;
|
||||
public parentFrameDataExplorerVersion: ko.Observable<string> = ko.observable<string>(Versions.DataExplorer);
|
||||
@@ -109,19 +113,19 @@ export class ExplorerStub implements ViewModels.Explorer {
|
||||
public arcadiaToken: ko.Observable<string>;
|
||||
public notebookWorkspaceManager: ViewModels.NotebookWorkspaceManager;
|
||||
public sparkClusterManager: ViewModels.SparkClusterManager;
|
||||
public notebookContentProvider: IContentProvider;
|
||||
public gitHubOAuthService: GitHubOAuthService;
|
||||
public notebookServerInfo: ko.Observable<DataModels.NotebookWorkspaceConnectionInfo>;
|
||||
public sparkClusterConnectionInfo: ko.Observable<DataModels.SparkClusterConnectionInfo>;
|
||||
public libraryManagePane: ViewModels.ContextualPane;
|
||||
public clusterLibraryPane: ViewModels.ContextualPane;
|
||||
public gitHubReposPane: ViewModels.ContextualPane;
|
||||
public publishNotebookPaneAdapter: ReactAdapter;
|
||||
public arcadiaWorkspaces: ko.ObservableArray<ArcadiaWorkspaceItem>;
|
||||
public hasStorageAnalyticsAfecFeature: ko.Observable<boolean>;
|
||||
public isSynapseLinkUpdating: ko.Observable<boolean>;
|
||||
public isNotebookTabActive: ko.Computed<boolean>;
|
||||
public memoryUsageInfo: ko.Observable<DataModels.MemoryUsageInfo>;
|
||||
public openGallery: () => void;
|
||||
public notebookManager?: any;
|
||||
public openGallery: (notebookUrl?: string, galleryItem?: IGalleryItem, isFavorite?: boolean) => void;
|
||||
public openNotebookViewer: (notebookUrl: string) => void;
|
||||
public resourceTokenDatabaseId: ko.Observable<string>;
|
||||
public resourceTokenCollectionId: ko.Observable<string>;
|
||||
@@ -329,7 +333,11 @@ export class ExplorerStub implements ViewModels.Explorer {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
public importAndOpenFromGallery(path: string, newName: string, content: any): Promise<boolean> {
|
||||
public importAndOpenFromGallery(name: string, content: string): Promise<boolean> {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
public publishNotebook(name: string, content: string): void {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
@@ -447,7 +455,6 @@ export class DatabaseStub implements ViewModels.Database {
|
||||
public collections: ko.ObservableArray<ViewModels.Collection>;
|
||||
public isDatabaseExpanded: ko.Observable<boolean>;
|
||||
public isDatabaseShared: ko.Computed<boolean>;
|
||||
public contextMenu: ViewModels.ContextMenu;
|
||||
public selectedSubnodeKind: ko.Observable<ViewModels.CollectionTabKind>;
|
||||
public offer: ko.Observable<DataModels.Offer>;
|
||||
|
||||
@@ -459,7 +466,6 @@ export class DatabaseStub implements ViewModels.Database {
|
||||
this.id = options.id;
|
||||
this.collections = options.collections;
|
||||
this.isDatabaseExpanded = options.isDatabaseExpanded;
|
||||
this.contextMenu = options.contextMenu;
|
||||
this.offer = options.offer;
|
||||
this.selectedSubnodeKind = options.selectedSubnodeKind;
|
||||
}
|
||||
@@ -562,8 +568,6 @@ export class CollectionStub implements ViewModels.Collection {
|
||||
public storedProceduresFocused: ko.Observable<boolean>;
|
||||
public userDefinedFunctionsFocused: ko.Observable<boolean>;
|
||||
public triggersFocused: ko.Observable<boolean>;
|
||||
public contextMenu: ViewModels.ContextMenu;
|
||||
public documentsContextMenu: ViewModels.ContextMenu;
|
||||
public conflictResolutionPolicy: ko.Observable<DataModels.ConflictResolutionPolicy>;
|
||||
public changeFeedPolicy: ko.Observable<DataModels.ChangeFeedPolicy>;
|
||||
public geospatialConfig: ko.Observable<DataModels.GeospatialConfig>;
|
||||
@@ -608,69 +612,8 @@ export class CollectionStub implements ViewModels.Collection {
|
||||
this.storedProceduresFocused = options.storedProceduresFocused;
|
||||
this.userDefinedFunctionsFocused = options.userDefinedFunctionsFocused;
|
||||
this.triggersFocused = options.triggersFocused;
|
||||
this.contextMenu = options.contextMenu;
|
||||
this.documentsContextMenu = options.documentsContextMenu;
|
||||
}
|
||||
|
||||
public onKeyPress = (source: any, event: KeyboardEvent): boolean => {
|
||||
throw new Error("Not implemented");
|
||||
};
|
||||
|
||||
public onKeyDown = (source: any, event: KeyboardEvent): boolean => {
|
||||
throw new Error("Not implemented");
|
||||
};
|
||||
|
||||
public onMenuKeyDown = (source: any, event: KeyboardEvent): boolean => {
|
||||
throw new Error("Not implemented");
|
||||
};
|
||||
|
||||
public onDocumentDBDocumentsKeyDown = (source: any, event: KeyboardEvent): boolean => {
|
||||
throw new Error("Not implemented");
|
||||
};
|
||||
|
||||
public onDocumentDBDocumentsKeyPress = (source: any, event: KeyboardEvent): boolean => {
|
||||
throw new Error("Not implemented");
|
||||
};
|
||||
|
||||
public onMongoDBDocumentsKeyDown = (source: any, event: KeyboardEvent): boolean => {
|
||||
throw new Error("Not implemented");
|
||||
};
|
||||
|
||||
public onMongoDBDocumentsKeyPress = (source: any, event: KeyboardEvent): boolean => {
|
||||
throw new Error("Not implemented");
|
||||
};
|
||||
|
||||
public onSettingsKeyDown = (source: any, event: KeyboardEvent): boolean => {
|
||||
throw new Error("Not implemented");
|
||||
};
|
||||
|
||||
public onSettingsKeyPress = (source: any, event: KeyboardEvent): boolean => {
|
||||
throw new Error("Not implemented");
|
||||
};
|
||||
|
||||
public onStoredProceduresKeyDown = (source: any, event: KeyboardEvent): boolean => {
|
||||
throw new Error("Not implemented");
|
||||
};
|
||||
|
||||
public onStoredProceduresKeyPress = (source: any, event: KeyboardEvent): boolean => {
|
||||
throw new Error("Not implemented");
|
||||
};
|
||||
|
||||
public onUserDefinedFunctionsKeyDown = (source: any, event: KeyboardEvent): boolean => {
|
||||
throw new Error("Not implemented");
|
||||
};
|
||||
|
||||
public onUserDefinedFunctionsKeyPress = (source: any, event: KeyboardEvent): boolean => {
|
||||
throw new Error("Not implemented");
|
||||
};
|
||||
|
||||
public onTriggersKeyDown = (source: any, event: KeyboardEvent): boolean => {
|
||||
throw new Error("Not implemented");
|
||||
};
|
||||
|
||||
public onTriggersKeyPress = (source: any, event: KeyboardEvent): boolean => {
|
||||
throw new Error("Not implemented");
|
||||
};
|
||||
public expandCollapseCollection() {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
@@ -5,8 +5,6 @@ import Explorer from "../Explorer";
|
||||
import ko from "knockout";
|
||||
import { AutopilotTier } from "../../Contracts/DataModels";
|
||||
|
||||
jest.mock("../Tabs/NotebookTab");
|
||||
|
||||
describe("Add Collection Pane", () => {
|
||||
describe("isValid()", () => {
|
||||
let explorer: ViewModels.Explorer;
|
||||
|
||||
@@ -2,8 +2,6 @@ import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import Explorer from "../Explorer";
|
||||
import AddDatabasePane from "./AddDatabasePane";
|
||||
|
||||
jest.mock("../Tabs/NotebookTab");
|
||||
|
||||
describe("Add Database Pane", () => {
|
||||
describe("getSharedThroughputDefault()", () => {
|
||||
let explorer: ViewModels.Explorer;
|
||||
|
||||
@@ -3,7 +3,7 @@ import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
import { Areas } from "../../Common/Constants";
|
||||
import { ContextualPaneBase } from "./ContextualPaneBase";
|
||||
import { Logger } from "../../Common/Logger";
|
||||
import * as Logger from "../../Common/Logger";
|
||||
import { QueriesGridComponentAdapter } from "../Controls/QueriesGridReactComponent/QueriesGridComponentAdapter";
|
||||
import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import { ContextualPaneBase } from "./ContextualPaneBase";
|
||||
import { ClusterLibraryGridAdapter } from "../Controls/LibraryManagement/ClusterLibraryGridAdapter";
|
||||
import { ClusterLibraryGridProps, ClusterLibraryItem } from "../Controls/LibraryManagement/ClusterLibraryGrid";
|
||||
import { Library, SparkCluster, SparkClusterLibrary } from "../../Contracts/DataModels";
|
||||
import { Logger } from "../../Common/Logger";
|
||||
import * as Logger from "../../Common/Logger";
|
||||
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils";
|
||||
|
||||
export class ClusterLibraryPane extends ContextualPaneBase {
|
||||
|
||||
146
src/Explorer/Panes/GenericRightPaneComponent.tsx
Normal file
146
src/Explorer/Panes/GenericRightPaneComponent.tsx
Normal file
@@ -0,0 +1,146 @@
|
||||
import * as React from "react";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { IconButton, PrimaryButton } from "office-ui-fabric-react/lib/Button";
|
||||
import { KeyCodes } from "../../Common/Constants";
|
||||
import { Subscription } from "knockout";
|
||||
import ErrorRedIcon from "../../../images/error_red.svg";
|
||||
import LoadingIndicatorIcon from "../../../images/LoadingIndicator_3Squares.gif";
|
||||
|
||||
export interface GenericRightPaneProps {
|
||||
container: ViewModels.Explorer;
|
||||
content: JSX.Element;
|
||||
formError: string;
|
||||
formErrorDetail: string;
|
||||
id: string;
|
||||
isExecuting: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: () => void;
|
||||
submitButtonText: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export interface GenericRightPaneState {
|
||||
panelHeight: number;
|
||||
}
|
||||
|
||||
export class GenericRightPaneComponent extends React.Component<GenericRightPaneProps, GenericRightPaneState> {
|
||||
private notificationConsoleSubscription: Subscription;
|
||||
|
||||
constructor(props: GenericRightPaneProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
panelHeight: this.getPanelHeight()
|
||||
};
|
||||
}
|
||||
|
||||
public componentDidMount(): void {
|
||||
this.notificationConsoleSubscription = this.props.container.isNotificationConsoleExpanded.subscribe(() => {
|
||||
this.setState({ panelHeight: this.getPanelHeight() });
|
||||
});
|
||||
this.props.container.isNotificationConsoleExpanded.extend({ rateLimit: 10 });
|
||||
}
|
||||
|
||||
public componentWillUnmount(): void {
|
||||
this.notificationConsoleSubscription && this.notificationConsoleSubscription.dispose();
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
return (
|
||||
<div tabIndex={-1} onKeyDown={this.onKeyDown}>
|
||||
<div className="contextual-pane-out" onClick={this.props.onClose}></div>
|
||||
<div
|
||||
className="contextual-pane"
|
||||
id={this.props.id}
|
||||
style={{ height: this.state.panelHeight }}
|
||||
onKeyDown={this.onKeyDown}
|
||||
>
|
||||
<div className="panelContentWrapper">
|
||||
{this.createPanelHeader()}
|
||||
{this.createErrorSection()}
|
||||
{this.props.content}
|
||||
{this.createPanelFooter()}
|
||||
</div>
|
||||
{this.createLoadingScreen()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private createPanelHeader = (): JSX.Element => {
|
||||
return (
|
||||
<div className="firstdivbg headerline">
|
||||
<span id="databaseTitle">{this.props.title}</span>
|
||||
<IconButton
|
||||
ariaLabel="Close pane"
|
||||
title="Close pane"
|
||||
onClick={this.props.onClose}
|
||||
tabIndex={0}
|
||||
className="closePaneBtn"
|
||||
iconProps={{ iconName: "Cancel" }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
private createErrorSection = (): JSX.Element => {
|
||||
return (
|
||||
<div className="warningErrorContainer" aria-live="assertive" hidden={!this.props.formError}>
|
||||
<div className="warningErrorContent">
|
||||
<span>
|
||||
<img className="paneErrorIcon" src={ErrorRedIcon} alt="Error" />
|
||||
</span>
|
||||
<span className="warningErrorDetailsLinkContainer">
|
||||
<span className="formErrors" title={this.props.formError}>
|
||||
{this.props.formError}
|
||||
</span>
|
||||
<a className="errorLink" role="link" hidden={!this.props.formErrorDetail} onClick={this.showErrorDetail}>
|
||||
More details
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
private createPanelFooter = (): JSX.Element => {
|
||||
return (
|
||||
<div className="paneFooter">
|
||||
<div className="leftpanel-okbut">
|
||||
<PrimaryButton
|
||||
ariaLabel="Submit"
|
||||
title="Submit"
|
||||
onClick={this.props.onSubmit}
|
||||
tabIndex={0}
|
||||
className="genericPaneSubmitBtn"
|
||||
text={this.props.submitButtonText}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
private createLoadingScreen = (): JSX.Element => {
|
||||
return (
|
||||
<div className="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer" hidden={!this.props.isExecuting}>
|
||||
<img className="dataExplorerLoader" src={LoadingIndicatorIcon} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
private onKeyDown = (event: React.KeyboardEvent<HTMLDivElement>): void => {
|
||||
if (event.keyCode === KeyCodes.Escape) {
|
||||
this.props.onClose();
|
||||
event.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
private showErrorDetail = (): void => {
|
||||
this.props.container.expandConsole();
|
||||
};
|
||||
|
||||
private getPanelHeight = (): number => {
|
||||
const notificationConsoleElement: HTMLElement = document.getElementById("explorerNotificationConsole");
|
||||
return window.innerHeight - $(notificationConsoleElement).height();
|
||||
};
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
import _ from "underscore";
|
||||
import { Areas, HttpStatusCodes } from "../../Common/Constants";
|
||||
import { Logger } from "../../Common/Logger";
|
||||
import * as Logger from "../../Common/Logger";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { GitHubClient, IGitHubPageInfo, IGitHubRepo } from "../../GitHub/GitHubClient";
|
||||
import { IPinnedRepo, JunoClient } from "../../Juno/JunoClient";
|
||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { GitHubUtils } from "../../Utils/GitHubUtils";
|
||||
import * as GitHubUtils from "../../Utils/GitHubUtils";
|
||||
import { JunoUtils } from "../../Utils/JunoUtils";
|
||||
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils";
|
||||
import { AuthorizeAccessComponent } from "../Controls/GitHub/AuthorizeAccessComponent";
|
||||
@@ -132,12 +132,13 @@ export class GitHubReposPane extends ContextualPaneBase {
|
||||
|
||||
private getOAuthScope(): string {
|
||||
return (
|
||||
this.container.gitHubOAuthService?.getTokenObservable()()?.scope || AuthorizeAccessComponent.Scopes.Public.key
|
||||
this.container.notebookManager?.gitHubOAuthService.getTokenObservable()()?.scope ||
|
||||
AuthorizeAccessComponent.Scopes.Public.key
|
||||
);
|
||||
}
|
||||
|
||||
private setup(forceShowConnectToGitHub = false): void {
|
||||
forceShowConnectToGitHub || !this.container.gitHubOAuthService.isLoggedIn()
|
||||
forceShowConnectToGitHub || !this.container.notebookManager?.gitHubOAuthService.isLoggedIn()
|
||||
? this.setupForConnectToGitHub()
|
||||
: this.setupForManageRepos();
|
||||
}
|
||||
@@ -294,7 +295,7 @@ export class GitHubReposPane extends ContextualPaneBase {
|
||||
|
||||
try {
|
||||
const response = await this.junoClient.getPinnedRepos(
|
||||
this.container.gitHubOAuthService?.getTokenObservable()()?.scope
|
||||
this.container.notebookManager?.gitHubOAuthService.getTokenObservable()()?.scope
|
||||
);
|
||||
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
|
||||
throw new Error(`Received HTTP ${response.status} when fetching pinned repos`);
|
||||
@@ -350,7 +351,7 @@ export class GitHubReposPane extends ContextualPaneBase {
|
||||
dataExplorerArea: Areas.Notebook,
|
||||
scopesSelected: scope
|
||||
});
|
||||
this.container.gitHubOAuthService.startOAuth(scope);
|
||||
this.container.notebookManager?.gitHubOAuthService.startOAuth(scope);
|
||||
}
|
||||
|
||||
private triggerRender(): void {
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
LibraryManageGridProps
|
||||
} from "../Controls/LibraryManagement/LibraryManage";
|
||||
import { Library } from "../../Contracts/DataModels";
|
||||
import { Logger } from "../../Common/Logger";
|
||||
import * as Logger from "../../Common/Logger";
|
||||
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils";
|
||||
|
||||
export class LibraryManagePane extends ContextualPaneBase {
|
||||
|
||||
@@ -4,7 +4,7 @@ import * as Constants from "../../Common/Constants";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { ContextualPaneBase } from "./ContextualPaneBase";
|
||||
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import { Logger } from "../../Common/Logger";
|
||||
import * as Logger from "../../Common/Logger";
|
||||
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils";
|
||||
|
||||
export class LoadQueryPane extends ContextualPaneBase implements ViewModels.LoadQueryPane {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user