mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-01-07 19:46:53 +00:00
Compare commits
17 Commits
MPAC-2020-
...
MPAC-2020-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84ea3796ec | ||
|
|
27024ef75c | ||
|
|
3f34936acd | ||
|
|
c51a55013c | ||
|
|
9a95c7d069 | ||
|
|
ec07ff05a4 | ||
|
|
7512b3c1d5 | ||
|
|
dd199e6565 | ||
|
|
8200cc521f | ||
|
|
1d3b672a14 | ||
|
|
e5fc6f2022 | ||
|
|
3bf42b23dd | ||
|
|
d22cb598a9 | ||
|
|
269ea6a349 | ||
|
|
123902e7ee | ||
|
|
bccebaade5 | ||
|
|
9fedf63a77 |
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/
|
**/node_modules/
|
||||||
|
dist/
|
||||||
src/Api/Apis.ts
|
src/Api/Apis.ts
|
||||||
src/AuthType.ts
|
src/AuthType.ts
|
||||||
src/Bindings/BindingHandlersRegisterer.ts
|
src/Bindings/BindingHandlersRegisterer.ts
|
||||||
@@ -25,7 +26,6 @@ src/Common/Logger.test.ts
|
|||||||
src/Common/MessageHandler.test.ts
|
src/Common/MessageHandler.test.ts
|
||||||
src/Common/MessageHandler.ts
|
src/Common/MessageHandler.ts
|
||||||
src/Common/MongoProxyClient.test.ts
|
src/Common/MongoProxyClient.test.ts
|
||||||
src/Common/MongoProxyClient.ts
|
|
||||||
src/Common/MongoUtility.ts
|
src/Common/MongoUtility.ts
|
||||||
src/Common/NotificationsClientBase.ts
|
src/Common/NotificationsClientBase.ts
|
||||||
src/Common/ObjectCache.test.ts
|
src/Common/ObjectCache.test.ts
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ module.exports = {
|
|||||||
browser: true,
|
browser: true,
|
||||||
es6: true
|
es6: true
|
||||||
},
|
},
|
||||||
plugins: ["@typescript-eslint"],
|
plugins: ["@typescript-eslint", "no-null"],
|
||||||
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
|
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
|
||||||
globals: {
|
globals: {
|
||||||
Atomics: "readonly",
|
Atomics: "readonly",
|
||||||
@@ -37,6 +37,8 @@ module.exports = {
|
|||||||
],
|
],
|
||||||
rules: {
|
rules: {
|
||||||
curly: "error",
|
curly: "error",
|
||||||
"@typescript-eslint/no-unused-vars": "error"
|
"@typescript-eslint/no-unused-vars": "error",
|
||||||
|
"@typescript-eslint/no-extraneous-class": "error",
|
||||||
|
"no-null/no-null": "error"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
# 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
|
name: CI
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -71,6 +68,7 @@ jobs:
|
|||||||
key: ${{ runner.os }}-build-cache
|
key: ${{ runner.os }}-build-cache
|
||||||
- run: npm run pack:prod
|
- run: npm run pack:prod
|
||||||
- run: cp -r ./Contracts ./dist/contracts
|
- run: cp -r ./Contracts ./dist/contracts
|
||||||
|
- run: cp -r ./configs ./dist/configs
|
||||||
- uses: actions/upload-artifact@v2
|
- uses: actions/upload-artifact@v2
|
||||||
with:
|
with:
|
||||||
name: dist
|
name: dist
|
||||||
@@ -172,10 +170,12 @@ jobs:
|
|||||||
uses: actions/download-artifact@v2
|
uses: actions/download-artifact@v2
|
||||||
with:
|
with:
|
||||||
name: dist
|
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 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 pack -Version "2.0.0-github-${GITHUB_SHA}"
|
||||||
- run: nuget push -Source "$NUGET_SOURCE" -ApiKey Az *.nupkg
|
- run: nuget push -Source "$NUGET_SOURCE" -ApiKey Az *.nupkg
|
||||||
- uses: actions/upload-artifact@v2
|
- uses: actions/upload-artifact@v2
|
||||||
|
name: packages
|
||||||
with:
|
with:
|
||||||
path: "*.nupkg"
|
path: "*.nupkg"
|
||||||
nugetmpac:
|
nugetmpac:
|
||||||
@@ -193,10 +193,12 @@ jobs:
|
|||||||
uses: actions/download-artifact@v2
|
uses: actions/download-artifact@v2
|
||||||
with:
|
with:
|
||||||
name: dist
|
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: 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 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 pack -Version "2.0.0-github-${GITHUB_SHA}"
|
||||||
- run: nuget push -Source "$NUGET_SOURCE" -ApiKey Az *.nupkg
|
- run: nuget push -Source "$NUGET_SOURCE" -ApiKey Az *.nupkg
|
||||||
- uses: actions/upload-artifact@v2
|
- uses: actions/upload-artifact@v2
|
||||||
|
name: packages
|
||||||
with:
|
with:
|
||||||
path: "*.nupkg"
|
path: "*.nupkg"
|
||||||
25
.github/workflows/runners.yml
vendored
Normal file
25
.github/workflows/runners.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
name: Runners
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "*/5 * * * *"
|
||||||
|
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
|
||||||
|
- uses: actions/upload-artifact@v2
|
||||||
|
if: failure()
|
||||||
|
with:
|
||||||
|
name: screenshots
|
||||||
|
path: failure.png
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -15,4 +15,6 @@ cypress/fixtures
|
|||||||
notebookapp/*
|
notebookapp/*
|
||||||
Contracts/*
|
Contracts/*
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.cache/
|
.cache/
|
||||||
|
.env
|
||||||
|
failure.png
|
||||||
@@ -30,12 +30,13 @@ For IE support, polyfill is preferred over new usage of lodash or underscore. We
|
|||||||
### Typescript
|
### Typescript
|
||||||
* Follow this [typescript style guide](https://github.com/excelmicro/typescript) which is based on [airbnb's style guide](https://github.com/airbnb/javascript).
|
* 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:
|
* Conventions speficic to this project:
|
||||||
* Use double-quotes for string
|
- Use double-quotes for string
|
||||||
* Don't use null, use undefined
|
- Don't use `null`, use `undefined`
|
||||||
* Pascal case for private static readonly fields
|
- Pascal case for private static readonly fields
|
||||||
* Camel case for classnames in markup
|
- Camel case for classnames in markup
|
||||||
* Don't use class unless necessary
|
* 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.
|
* 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.
|
||||||
|
* Prefer using [Fluent UI controls](https://developer.microsoft.com/en-us/fluentui#/controls/web) over creating your own, in order to maintain consistency and support a11y.
|
||||||
|
|
||||||
### React
|
### React
|
||||||
* Prefer using React class components over function components and hooks unless you have a simple component and require no nested functions:
|
* Prefer using React class components over function components and hooks unless you have a simple component and require no nested functions:
|
||||||
|
|||||||
11
README.md
11
README.md
@@ -70,7 +70,7 @@ Unit tests are located adjacent to the code under test and run with [Jest](https
|
|||||||
|
|
||||||
`npm run test`
|
`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:
|
[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,6 +80,13 @@ Unit tests are located adjacent to the code under test and run with [Jest](https
|
|||||||
4. Install dependencies: `npm install`
|
4. Install dependencies: `npm install`
|
||||||
5. Run cypress headless(`npm run test`) or in interactive mode(`npm run test:debug`)
|
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
|
# Contributing
|
||||||
|
|
||||||
Please read the [contribution guidelines](./CONTRIBUTING.md).
|
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"
|
||||||
|
}
|
||||||
22
images/CosmosDB-logo.svg
Normal file
22
images/CosmosDB-logo.svg
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<svg id="b089cfca-0de1-451c-a1ca-6680ea50cb4f" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18">
|
||||||
|
<defs>
|
||||||
|
<radialGradient id="b25d0836-964a-4c84-8c20-855f66e8345e" cx="-105.006" cy="-10.409" r="5.954" gradientTransform="translate(117.739 19.644) scale(1.036 1.027)" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0.183" stop-color="#5ea0ef"/>
|
||||||
|
<stop offset="1" stop-color="#0078d4"/>
|
||||||
|
</radialGradient>
|
||||||
|
<clipPath id="b36c7f5d-2ef1-4760-8a25-eeb9661f4e47">
|
||||||
|
<path d="M14.969,7.53A6.137,6.137,0,1,1,7.574,2.987,6.137,6.137,0,0,1,14.969,7.53Z" fill="none"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
<title>Icon-databases-121</title>
|
||||||
|
<path d="M2.954,5.266a.175.175,0,0,1-.176-.176h0A2.012,2.012,0,0,0,.769,3.081a.176.176,0,0,1-.176-.175h0a.176.176,0,0,1,.176-.176A2.012,2.012,0,0,0,2.778.72.175.175,0,0,1,2.954.544h0A.175.175,0,0,1,3.13.72h0A2.012,2.012,0,0,0,5.139,2.729a.175.175,0,0,1,.176.176h0a.175.175,0,0,1-.176.176h0A2.011,2.011,0,0,0,3.13,5.09.177.177,0,0,1,2.954,5.266Z" fill="#50e6ff"/>
|
||||||
|
<path d="M15.611,17.456a.141.141,0,0,1-.141-.141h0a1.609,1.609,0,0,0-1.607-1.607.141.141,0,0,1-.141-.14h0a.141.141,0,0,1,.141-.141h0a1.608,1.608,0,0,0,1.607-1.607.141.141,0,0,1,.141-.141h0a.141.141,0,0,1,.141.141h0a1.608,1.608,0,0,0,1.607,1.607.141.141,0,1,1,0,.282h0a1.609,1.609,0,0,0-1.607,1.607A.141.141,0,0,1,15.611,17.456Z" fill="#50e6ff"/>
|
||||||
|
<g>
|
||||||
|
<path d="M14.969,7.53A6.137,6.137,0,1,1,7.574,2.987,6.137,6.137,0,0,1,14.969,7.53Z" fill="url(#b25d0836-964a-4c84-8c20-855f66e8345e)"/>
|
||||||
|
<g clip-path="url(#b36c7f5d-2ef1-4760-8a25-eeb9661f4e47)">
|
||||||
|
<path d="M5.709,13.115A1.638,1.638,0,1,0,5.714,9.84,1.307,1.307,0,0,0,5.721,9.7,1.651,1.651,0,0,0,4.06,8.064H2.832a6.251,6.251,0,0,0,1.595,5.051Z" fill="#f2f2f2"/>
|
||||||
|
<path d="M15.045,7.815c0-.015,0-.03-.007-.044a5.978,5.978,0,0,0-1.406-2.88,1.825,1.825,0,0,0-.289-.09,1.806,1.806,0,0,0-2.3,1.663,2,2,0,0,0-.2-.013,1.737,1.737,0,0,0-.581,3.374,1.451,1.451,0,0,0,.541.1h2.03A13.453,13.453,0,0,0,15.045,7.815Z" fill="#f2f2f2"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<path d="M17.191,3.832c-.629-1.047-2.1-1.455-4.155-1.149a14.606,14.606,0,0,0-2.082.452,6.456,6.456,0,0,1,1.528.767c.241-.053.483-.116.715-.151A7.49,7.49,0,0,1,14.3,3.662a2.188,2.188,0,0,1,1.959.725h0c.383.638.06,1.729-.886,3a16.723,16.723,0,0,1-4.749,4.051A16.758,16.758,0,0,1,4.8,13.7c-1.564.234-2.682,0-3.065-.636s-.06-1.73.886-2.995c.117-.157.146-.234.279-.392a6.252,6.252,0,0,1,.026-1.63A11.552,11.552,0,0,0,1.756,9.419C.517,11.076.181,12.566.809,13.613a3.165,3.165,0,0,0,2.9,1.249,8.434,8.434,0,0,0,1.251-.1,17.855,17.855,0,0,0,6.219-2.4,17.808,17.808,0,0,0,5.061-4.332C17.483,6.369,17.819,4.88,17.191,3.832Z" fill="#50e6ff"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.6 KiB |
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?)$",
|
// testRegex: "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?|ts?)$",
|
||||||
|
|
||||||
// This option allows the use of a custom results processor
|
// 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
|
// This option allows use of a custom test runner
|
||||||
// testRunner: "jasmine2",
|
// testRunner: "jasmine2",
|
||||||
|
|||||||
@@ -54,6 +54,8 @@
|
|||||||
|
|
||||||
@SelectionColor: #3074B0;
|
@SelectionColor: #3074B0;
|
||||||
|
|
||||||
|
@FocusColor: #00bcf2;
|
||||||
|
|
||||||
/******************************************************************************
|
/******************************************************************************
|
||||||
METRICS
|
METRICS
|
||||||
/******************************************************************************/
|
/******************************************************************************/
|
||||||
@@ -198,7 +200,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.focus() {
|
.focus() {
|
||||||
outline: 1px dashed @AccentMedium;
|
outline: 1px dashed @FocusColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
/************************************************************************************************
|
/************************************************************************************************
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ body {
|
|||||||
font-family: @DataExplorerFont;
|
font-family: @DataExplorerFont;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
|
:focus {
|
||||||
|
.focus()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.float-right {
|
.float-right {
|
||||||
@@ -174,7 +178,7 @@ body {
|
|||||||
&:active {
|
&:active {
|
||||||
.active();
|
.active();
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus .urlTokenCopyTooltiptext, &:focus .urlTokenCopyTooltiptext {
|
&:focus .urlTokenCopyTooltiptext, &:focus .urlTokenCopyTooltiptext {
|
||||||
.tooltipVisible();
|
.tooltipVisible();
|
||||||
}
|
}
|
||||||
@@ -362,7 +366,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.splashLoaderContainer {
|
.splashLoaderContainer {
|
||||||
z-index: 5;
|
z-index: 5;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
@@ -1449,7 +1453,7 @@ p {
|
|||||||
.throughputModeRadio {
|
.throughputModeRadio {
|
||||||
vertical-align: text-bottom;
|
vertical-align: text-bottom;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nonFirstRadio {
|
.nonFirstRadio {
|
||||||
margin-left: @LargeSpace;
|
margin-left: @LargeSpace;
|
||||||
}
|
}
|
||||||
@@ -1484,7 +1488,7 @@ p {
|
|||||||
|
|
||||||
.largePartitionKeyDescription {
|
.largePartitionKeyDescription {
|
||||||
margin: @DefaultSpace 0px 0px;
|
margin: @DefaultSpace 0px 0px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.enableAnalyticalStorage {
|
.enableAnalyticalStorage {
|
||||||
@@ -2216,13 +2220,13 @@ a:link {
|
|||||||
|
|
||||||
.documentsGridHeaderContainer table thead tr {
|
.documentsGridHeaderContainer table thead tr {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
th {
|
th {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
background-color: #fff !important;
|
background-color: #fff !important;
|
||||||
border-bottom: 1px solid #CCCCCC !important;
|
border-bottom: 1px solid #CCCCCC !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.documentsGridHeader {
|
.documentsGridHeader {
|
||||||
|
|||||||
9284
package-lock.json
generated
9284
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
20
package.json
20
package.json
@@ -4,7 +4,7 @@
|
|||||||
"description": "Cosmos Explorer",
|
"description": "Cosmos Explorer",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@azure/cosmos": "3.7.1",
|
"@azure/cosmos": "3.7.4",
|
||||||
"@azure/cosmos-language-service": "0.0.4",
|
"@azure/cosmos-language-service": "0.0.4",
|
||||||
"@jupyterlab/services": "4.2.0",
|
"@jupyterlab/services": "4.2.0",
|
||||||
"@jupyterlab/terminal": "1.2.1",
|
"@jupyterlab/terminal": "1.2.1",
|
||||||
@@ -37,22 +37,25 @@
|
|||||||
"@uifabric/react-cards": "0.109.53",
|
"@uifabric/react-cards": "0.109.53",
|
||||||
"@uifabric/styling": "7.11.2",
|
"@uifabric/styling": "7.11.2",
|
||||||
"abort-controller": "3.0.0",
|
"abort-controller": "3.0.0",
|
||||||
|
"applicationinsights": "1.8.0",
|
||||||
"babel-polyfill": "6.26.0",
|
"babel-polyfill": "6.26.0",
|
||||||
"bootstrap": "3.3.7",
|
"bootstrap": "3.3.7",
|
||||||
"canvas": "2.6.0",
|
"canvas": "2.6.0",
|
||||||
"clean-webpack-plugin": "0.1.19",
|
"clean-webpack-plugin": "0.1.19",
|
||||||
"copy-webpack-plugin": "4.5.4",
|
"copy-webpack-plugin": "6.0.2",
|
||||||
"crossroads": "0.12.2",
|
"crossroads": "0.12.2",
|
||||||
"css-element-queries": "1.1.1",
|
"css-element-queries": "1.1.1",
|
||||||
"datatables.net-colreorder-dt": "1.5.1",
|
"datatables.net-colreorder-dt": "1.5.1",
|
||||||
"datatables.net-dt": "1.10.19",
|
"datatables.net-dt": "1.10.19",
|
||||||
"date-fns": "1.29.0",
|
"date-fns": "1.29.0",
|
||||||
"dayjs": "1.8.19",
|
"dayjs": "1.8.19",
|
||||||
|
"dotenv": "8.2.0",
|
||||||
"es6-object-assign": "1.1.0",
|
"es6-object-assign": "1.1.0",
|
||||||
"es6-symbol": "3.1.3",
|
"es6-symbol": "3.1.3",
|
||||||
"eslint-plugin-jest": "23.13.2",
|
"eslint-plugin-jest": "23.13.2",
|
||||||
"hasher": "1.2.0",
|
"hasher": "1.2.0",
|
||||||
"immutable": "4.0.0-rc.12",
|
"immutable": "4.0.0-rc.12",
|
||||||
|
"is-ci": "2.0.0",
|
||||||
"jquery": "3.4.0",
|
"jquery": "3.4.0",
|
||||||
"jquery-typeahead": "2.10.6",
|
"jquery-typeahead": "2.10.6",
|
||||||
"jquery-ui-dist": "1.12.1",
|
"jquery-ui-dist": "1.12.1",
|
||||||
@@ -123,8 +126,9 @@
|
|||||||
"enzyme": "3.10.0",
|
"enzyme": "3.10.0",
|
||||||
"enzyme-adapter-react-16": "1.15.1",
|
"enzyme-adapter-react-16": "1.15.1",
|
||||||
"enzyme-to-json": "3.4.3",
|
"enzyme-to-json": "3.4.3",
|
||||||
"eslint": "7.2.0",
|
"eslint": "7.3.1",
|
||||||
"eslint-cli": "1.1.1",
|
"eslint-cli": "1.1.1",
|
||||||
|
"eslint-plugin-no-null": "1.0.2",
|
||||||
"eslint-plugin-react": "7.20.0",
|
"eslint-plugin-react": "7.20.0",
|
||||||
"expose-loader": "0.7.5",
|
"expose-loader": "0.7.5",
|
||||||
"file-loader": "2.0.0",
|
"file-loader": "2.0.0",
|
||||||
@@ -133,8 +137,9 @@
|
|||||||
"html-loader-jest": "0.2.1",
|
"html-loader-jest": "0.2.1",
|
||||||
"html-webpack-plugin": "3.2.0",
|
"html-webpack-plugin": "3.2.0",
|
||||||
"inline-css": "2.2.5",
|
"inline-css": "2.2.5",
|
||||||
"jest": "24.9.0",
|
"jest": "25.5.4",
|
||||||
"jest-canvas-mock": "2.1.0",
|
"jest-canvas-mock": "2.1.0",
|
||||||
|
"jest-puppeteer": "4.4.0",
|
||||||
"jest-trx-results-processor": "0.0.7",
|
"jest-trx-results-processor": "0.0.7",
|
||||||
"less": "3.8.1",
|
"less": "3.8.1",
|
||||||
"less-loader": "4.1.0",
|
"less-loader": "4.1.0",
|
||||||
@@ -142,17 +147,18 @@
|
|||||||
"mini-css-extract-plugin": "0.4.3",
|
"mini-css-extract-plugin": "0.4.3",
|
||||||
"monaco-editor-webpack-plugin": "1.7.0",
|
"monaco-editor-webpack-plugin": "1.7.0",
|
||||||
"prettier": "1.19.1",
|
"prettier": "1.19.1",
|
||||||
|
"puppeteer": "4.0.0",
|
||||||
"raw-loader": "0.5.1",
|
"raw-loader": "0.5.1",
|
||||||
"rimraf": "3.0.0",
|
"rimraf": "3.0.0",
|
||||||
"sinon": "3.2.1",
|
"sinon": "3.2.1",
|
||||||
"style-loader": "0.23.0",
|
"style-loader": "0.23.0",
|
||||||
"terser-webpack-plugin": "2.3.5",
|
"terser-webpack-plugin": "3.0.5",
|
||||||
"ts-loader": "6.2.2",
|
"ts-loader": "6.2.2",
|
||||||
"tslint": "5.11.0",
|
"tslint": "5.11.0",
|
||||||
"tslint-microsoft-contrib": "6.0.0",
|
"tslint-microsoft-contrib": "6.0.0",
|
||||||
"typescript": "3.8.3",
|
"typescript": "3.8.3",
|
||||||
"url-loader": "1.1.1",
|
"url-loader": "1.1.1",
|
||||||
"webpack": "4.41.2",
|
"webpack": "4.43.0",
|
||||||
"webpack-bundle-analyzer": "3.6.1",
|
"webpack-bundle-analyzer": "3.6.1",
|
||||||
"webpack-cli": "3.3.10",
|
"webpack-cli": "3.3.10",
|
||||||
"webpack-dev-server": "3.11.0",
|
"webpack-dev-server": "3.11.0",
|
||||||
@@ -168,8 +174,8 @@
|
|||||||
"pack:fast": "node --max_old_space_size=10196 ./node_modules/webpack/bin/webpack.js --mode development --progress",
|
"pack:fast": "node --max_old_space_size=10196 ./node_modules/webpack/bin/webpack.js --mode development --progress",
|
||||||
"copyToConsumers": "node copyToConsumers",
|
"copyToConsumers": "node copyToConsumers",
|
||||||
"test": "rimraf coverage && jest",
|
"test": "rimraf coverage && jest",
|
||||||
|
"test:e2e": "jest -c ./jest.config.e2e.js --detectOpenHandles",
|
||||||
"watch": "npm run start",
|
"watch": "npm run start",
|
||||||
"integrationTest": "runIntegrationTests.cmd",
|
|
||||||
"build:ase": "gulp build:ase",
|
"build:ase": "gulp build:ase",
|
||||||
"compile": "tsc",
|
"compile": "tsc",
|
||||||
"compile:contracts": "tsc -p ./tsconfig.contracts.json",
|
"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
|
|
||||||
@@ -1,11 +1,6 @@
|
|||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
import * as ReactBindingHandler from "./ReactBindingHandler";
|
import * as ReactBindingHandler from "./ReactBindingHandler";
|
||||||
|
|
||||||
interface RestorePoint {
|
|
||||||
readonly element: JQuery;
|
|
||||||
readonly width: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class BindingHandlersRegisterer {
|
export class BindingHandlersRegisterer {
|
||||||
public static registerBindingHandlers() {
|
public static registerBindingHandlers() {
|
||||||
ko.bindingHandlers.setTemplateReady = {
|
ko.bindingHandlers.setTemplateReady = {
|
||||||
@@ -17,7 +12,7 @@ export class BindingHandlersRegisterer {
|
|||||||
bindingContext?: ko.BindingContext
|
bindingContext?: ko.BindingContext
|
||||||
) {
|
) {
|
||||||
const value = ko.unwrap(wrappedValueAccessor());
|
const value = ko.unwrap(wrappedValueAccessor());
|
||||||
bindingContext.$data.isTemplateReady(value);
|
bindingContext?.$data.isTemplateReady(value);
|
||||||
}
|
}
|
||||||
} as ko.BindingHandler;
|
} as ko.BindingHandler;
|
||||||
|
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ export class Features {
|
|||||||
public static readonly hostedDataExplorer = "hosteddataexplorerenabled";
|
public static readonly hostedDataExplorer = "hosteddataexplorerenabled";
|
||||||
public static readonly enableTtl = "enablettl";
|
public static readonly enableTtl = "enablettl";
|
||||||
public static readonly enableNotebooks = "enablenotebooks";
|
public static readonly enableNotebooks = "enablenotebooks";
|
||||||
public static readonly enableGallery = "enablegallery";
|
public static readonly enableGalleryPublish = "enablegallerypublish";
|
||||||
public static readonly enableSpark = "enablespark";
|
public static readonly enableSpark = "enablespark";
|
||||||
public static readonly livyEndpoint = "livyendpoint";
|
public static readonly livyEndpoint = "livyendpoint";
|
||||||
public static readonly notebookServerUrl = "notebookserverurl";
|
public static readonly notebookServerUrl = "notebookserverurl";
|
||||||
@@ -443,6 +443,17 @@ export class KeyCodes {
|
|||||||
public static Tab: number = 9;
|
public static Tab: number = 9;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Normalized per: https://www.w3.org/TR/uievents-key/#named-key-attribute-values
|
||||||
|
export class NormalizedEventKey {
|
||||||
|
public static readonly Space = " ";
|
||||||
|
public static readonly Enter = "Enter";
|
||||||
|
public static readonly Escape = "Escape";
|
||||||
|
public static readonly UpArrow = "ArrowUp";
|
||||||
|
public static readonly DownArrow = "ArrowDown";
|
||||||
|
public static readonly LeftArrow = "ArrowLeft";
|
||||||
|
public static readonly RightArrow = "ArrowRight";
|
||||||
|
}
|
||||||
|
|
||||||
export class TryCosmosExperience {
|
export class TryCosmosExperience {
|
||||||
public static extendUrl: string = "https://trycosmosdb.azure.com/api/resource/extendportal?userId={0}";
|
public static extendUrl: string = "https://trycosmosdb.azure.com/api/resource/extendportal?userId={0}";
|
||||||
public static deleteUrl: string = "https://trycosmosdb.azure.com/api/resource/deleteportal?userId={0}";
|
public static deleteUrl: string = "https://trycosmosdb.azure.com/api/resource/deleteportal?userId={0}";
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import Q from "q";
|
|||||||
import { ConflictDefinition, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
import { ConflictDefinition, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
||||||
import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
|
import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
|
||||||
import { DataAccessUtilityBase } from "./DataAccessUtilityBase";
|
import { DataAccessUtilityBase } from "./DataAccessUtilityBase";
|
||||||
import { Logger } from "./Logger";
|
import * as Logger from "./Logger";
|
||||||
import { MessageHandler } from "./MessageHandler";
|
import { MessageHandler } from "./MessageHandler";
|
||||||
import { MessageTypes } from "../Contracts/ExplorerContracts";
|
import { MessageTypes } from "../Contracts/ExplorerContracts";
|
||||||
import { MinimalQueryIterator, nextPage } from "./IteratorUtilities";
|
import { MinimalQueryIterator, nextPage } from "./IteratorUtilities";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { LogEntryLevel } from "../Contracts/Diagnostics";
|
import { LogEntryLevel } from "../Contracts/Diagnostics";
|
||||||
import { Logger } from "./Logger";
|
import * as Logger from "./Logger";
|
||||||
import { MessageHandler } from "./MessageHandler";
|
import { MessageHandler } from "./MessageHandler";
|
||||||
import { MessageTypes } from "../Contracts/ExplorerContracts";
|
import { MessageTypes } from "../Contracts/ExplorerContracts";
|
||||||
|
|
||||||
|
|||||||
@@ -4,85 +4,68 @@ import { appInsights } from "../Shared/appInsights";
|
|||||||
import { SeverityLevel } from "@microsoft/applicationinsights-web";
|
import { SeverityLevel } from "@microsoft/applicationinsights-web";
|
||||||
|
|
||||||
// TODO: Move to a separate Diagnostics folder
|
// TODO: Move to a separate Diagnostics folder
|
||||||
export class Logger {
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
export function logInfo(message: string | Record<string, any>, area: string, code?: number): void {
|
||||||
public static logInfo(message: string | Record<string, any>, area: string, code?: number): void {
|
let logMessage: string;
|
||||||
let logMessage: string;
|
if (typeof message === "string") {
|
||||||
if (typeof message === "string") {
|
logMessage = message;
|
||||||
logMessage = message;
|
} else {
|
||||||
} else {
|
logMessage = JSON.stringify(message, Object.getOwnPropertyNames(message));
|
||||||
logMessage = JSON.stringify(message, Object.getOwnPropertyNames(message));
|
|
||||||
}
|
|
||||||
const entry: Diagnostics.LogEntry = Logger._generateLogEntry(
|
|
||||||
Diagnostics.LogEntryLevel.Verbose,
|
|
||||||
logMessage,
|
|
||||||
area,
|
|
||||||
code
|
|
||||||
);
|
|
||||||
return Logger._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);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static _logEntry(entry: Diagnostics.LogEntry): void {
|
|
||||||
MessageHandler.sendMessage({
|
|
||||||
type: MessageTypes.LogInfo,
|
|
||||||
data: JSON.stringify(entry)
|
|
||||||
});
|
|
||||||
|
|
||||||
const severityLevel = ((level: Diagnostics.LogEntryLevel): SeverityLevel => {
|
|
||||||
switch (level) {
|
|
||||||
case Diagnostics.LogEntryLevel.Custom:
|
|
||||||
case Diagnostics.LogEntryLevel.Debug:
|
|
||||||
case Diagnostics.LogEntryLevel.Verbose:
|
|
||||||
return SeverityLevel.Verbose;
|
|
||||||
case Diagnostics.LogEntryLevel.Warning:
|
|
||||||
return SeverityLevel.Warning;
|
|
||||||
case Diagnostics.LogEntryLevel.Error:
|
|
||||||
return SeverityLevel.Error;
|
|
||||||
default:
|
|
||||||
return SeverityLevel.Information;
|
|
||||||
}
|
|
||||||
})(entry.level);
|
|
||||||
appInsights.trackTrace({ message: entry.message, severityLevel }, { area: entry.area });
|
|
||||||
}
|
|
||||||
|
|
||||||
private static _generateLogEntry(
|
|
||||||
level: Diagnostics.LogEntryLevel,
|
|
||||||
message: string,
|
|
||||||
area: string,
|
|
||||||
code: number
|
|
||||||
): Diagnostics.LogEntry {
|
|
||||||
return {
|
|
||||||
timestamp: new Date().getUTCSeconds(),
|
|
||||||
level: level,
|
|
||||||
message: message,
|
|
||||||
area: area,
|
|
||||||
code: code
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
const entry: Diagnostics.LogEntry = _generateLogEntry(Diagnostics.LogEntryLevel.Verbose, logMessage, area, code);
|
||||||
|
return _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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = _generateLogEntry(Diagnostics.LogEntryLevel.Error, logMessage, area, code);
|
||||||
|
return _logEntry(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _logEntry(entry: Diagnostics.LogEntry): void {
|
||||||
|
MessageHandler.sendMessage({
|
||||||
|
type: MessageTypes.LogInfo,
|
||||||
|
data: JSON.stringify(entry)
|
||||||
|
});
|
||||||
|
|
||||||
|
const severityLevel = ((level: Diagnostics.LogEntryLevel): SeverityLevel => {
|
||||||
|
switch (level) {
|
||||||
|
case Diagnostics.LogEntryLevel.Custom:
|
||||||
|
case Diagnostics.LogEntryLevel.Debug:
|
||||||
|
case Diagnostics.LogEntryLevel.Verbose:
|
||||||
|
return SeverityLevel.Verbose;
|
||||||
|
case Diagnostics.LogEntryLevel.Warning:
|
||||||
|
return SeverityLevel.Warning;
|
||||||
|
case Diagnostics.LogEntryLevel.Error:
|
||||||
|
return SeverityLevel.Error;
|
||||||
|
default:
|
||||||
|
return SeverityLevel.Information;
|
||||||
|
}
|
||||||
|
})(entry.level);
|
||||||
|
appInsights.trackTrace({ message: entry.message, severityLevel }, { area: entry.area });
|
||||||
|
}
|
||||||
|
|
||||||
|
function _generateLogEntry(
|
||||||
|
level: Diagnostics.LogEntryLevel,
|
||||||
|
message: string,
|
||||||
|
area: string,
|
||||||
|
code?: number
|
||||||
|
): Diagnostics.LogEntry {
|
||||||
|
return {
|
||||||
|
timestamp: new Date().getUTCSeconds(),
|
||||||
|
level,
|
||||||
|
message,
|
||||||
|
area,
|
||||||
|
code
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,13 +31,13 @@ function authHeaders(): any {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function queryIterator(databaseId: string, collection: Collection, query: string) {
|
export function queryIterator(databaseId: string, collection: Collection, query: string): any {
|
||||||
let continuationToken: string = null;
|
let continuationToken: string;
|
||||||
return {
|
return {
|
||||||
fetchNext: () => {
|
fetchNext: () => {
|
||||||
return queryDocuments(databaseId, collection, false, query).then(response => {
|
return queryDocuments(databaseId, collection, false, query).then(response => {
|
||||||
continuationToken = response.continuationToken;
|
continuationToken = response.continuationToken;
|
||||||
let headers = {} as any;
|
const headers = {} as any;
|
||||||
response.headers.forEach((value: any, key: any) => {
|
response.headers.forEach((value: any, key: any) => {
|
||||||
headers[key] = value;
|
headers[key] = value;
|
||||||
});
|
});
|
||||||
@@ -114,14 +114,7 @@ export function queryDocuments(
|
|||||||
headers: response.headers
|
headers: response.headers
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const errorMessage = await response.text();
|
return errorHandling(response, "querying documents", params);
|
||||||
if (response.status === HttpStatusCodes.Forbidden) {
|
|
||||||
MessageHandler.sendMessage({
|
|
||||||
type: MessageTypes.ForbiddenError,
|
|
||||||
reason: errorMessage
|
|
||||||
});
|
|
||||||
}
|
|
||||||
throw new Error(errorMessage);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,11 +153,11 @@ export function readDocument(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then(async response => {
|
.then(response => {
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
return response.json();
|
return response.json();
|
||||||
}
|
}
|
||||||
errorHandling(response);
|
return errorHandling(response, "reading document", params);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,11 +192,11 @@ export function createDocument(
|
|||||||
...authHeaders()
|
...authHeaders()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then(async response => {
|
.then(response => {
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
return response.json();
|
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())
|
[CosmosSDKConstants.HttpHeaders.PartitionKey]: JSON.stringify(documentId.partitionKeyHeader())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then(async response => {
|
.then(response => {
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
return response.json();
|
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())
|
[CosmosSDKConstants.HttpHeaders.PartitionKey]: JSON.stringify(documentId.partitionKeyHeader())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then(async response => {
|
.then(response => {
|
||||||
if (response.ok) {
|
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) {
|
if (response.ok) {
|
||||||
return;
|
return undefined;
|
||||||
}
|
}
|
||||||
NotificationConsoleUtils.logConsoleMessage(
|
return errorHandling(response, "creating collection", params);
|
||||||
ConsoleDataType.Error,
|
|
||||||
`Error creating collection: ${await response.json()}, Payload: ${params}`
|
|
||||||
);
|
|
||||||
errorHandling(response);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -407,13 +396,16 @@ export function getEndpoint(databaseAccount: ViewModels.DatabaseAccount): string
|
|||||||
return url;
|
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();
|
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) {
|
if (response.status === HttpStatusCodes.Forbidden) {
|
||||||
MessageHandler.sendMessage({
|
MessageHandler.sendMessage({ type: MessageTypes.ForbiddenError, reason: errorMessage });
|
||||||
type: MessageTypes.ForbiddenError,
|
return;
|
||||||
reason: errorMessage
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
throw new Error(errorMessage);
|
throw new Error(errorMessage);
|
||||||
}
|
}
|
||||||
@@ -462,14 +454,6 @@ export async function _createMongoCollectionWithARM(
|
|||||||
rpPayloadToCreateCollection
|
rpPayloadToCreateCollection
|
||||||
);
|
);
|
||||||
} catch (response) {
|
} catch (response) {
|
||||||
NotificationConsoleUtils.logConsoleMessage(
|
return errorHandling(response, "creating collection", undefined);
|
||||||
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`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { BackendDefaults, HttpStatusCodes, SavedQueries } from "./Constants";
|
|||||||
import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
|
import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
|
||||||
import { CosmosClient } from "./CosmosClient";
|
import { CosmosClient } from "./CosmosClient";
|
||||||
import { ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
import { ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
||||||
import { Logger } from "./Logger";
|
import * as Logger from "./Logger";
|
||||||
import { NotificationConsoleUtils } from "../Utils/NotificationConsoleUtils";
|
import { NotificationConsoleUtils } from "../Utils/NotificationConsoleUtils";
|
||||||
import { QueryUtils } from "../Utils/QueryUtils";
|
import { QueryUtils } from "../Utils/QueryUtils";
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ interface Config {
|
|||||||
ARM_API_VERSION: string;
|
ARM_API_VERSION: string;
|
||||||
GRAPH_ENDPOINT: string;
|
GRAPH_ENDPOINT: string;
|
||||||
GRAPH_API_VERSION: string;
|
GRAPH_API_VERSION: string;
|
||||||
AZURESAMPLESCOSMOSDBPAT: string;
|
|
||||||
ARCADIA_ENDPOINT: string;
|
ARCADIA_ENDPOINT: string;
|
||||||
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: string;
|
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: string;
|
||||||
BACKEND_ENDPOINT?: string;
|
BACKEND_ENDPOINT?: string;
|
||||||
@@ -44,8 +43,7 @@ let config: Config = {
|
|||||||
ARCADIA_ENDPOINT: "https://workspaceartifacts.projectarcadia.net",
|
ARCADIA_ENDPOINT: "https://workspaceartifacts.projectarcadia.net",
|
||||||
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: "dev.azuresynapse.net",
|
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: "dev.azuresynapse.net",
|
||||||
GITHUB_CLIENT_ID: "6cb2f63cf6f7b5cbdeca", // Registered OAuth app: https://github.com/settings/applications/1189306
|
GITHUB_CLIENT_ID: "6cb2f63cf6f7b5cbdeca", // Registered OAuth app: https://github.com/settings/applications/1189306
|
||||||
JUNO_ENDPOINT: "https://tools.cosmos.azure.com",
|
JUNO_ENDPOINT: "https://tools.cosmos.azure.com"
|
||||||
AZURESAMPLESCOSMOSDBPAT: "99e38770e29b4a61d7c49f188780504efd35cc86" //[SuppressMessage("Microsoft.Security", "CS001:SecretInline", Justification=" // this PAT is a "no scopes" PAT with zero access to any projects, this is just used to get around the dev.github.com rate limit when accessing public samples repo.")]
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Injected for local develpment. These will be removed in the production bundle by webpack
|
// Injected for local develpment. These will be removed in the production bundle by webpack
|
||||||
|
|||||||
@@ -704,49 +704,6 @@ export interface MemoryUsageInfo {
|
|||||||
totalKB: number;
|
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 {
|
export interface resourceTokenConnectionStringProperties {
|
||||||
accountEndpoint: string;
|
accountEndpoint: string;
|
||||||
collectionId: string;
|
collectionId: string;
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export interface LogEntry {
|
|||||||
/**
|
/**
|
||||||
* The message code.
|
* The message code.
|
||||||
*/
|
*/
|
||||||
code: number;
|
code?: number;
|
||||||
/**
|
/**
|
||||||
* Any additional data to be logged.
|
* Any additional data to be logged.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -12,10 +12,8 @@ import { CommandButtonComponentProps } from "../Explorer/Controls/CommandButton/
|
|||||||
import { ConsoleData } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
|
import { ConsoleData } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
|
||||||
import { ExecuteSprocParam } from "../Explorer/Panes/ExecuteSprocParamsPane";
|
import { ExecuteSprocParam } from "../Explorer/Panes/ExecuteSprocParamsPane";
|
||||||
import { GitHubClient } from "../GitHub/GitHubClient";
|
import { GitHubClient } from "../GitHub/GitHubClient";
|
||||||
import { GitHubOAuthService } from "../GitHub/GitHubOAuthService";
|
|
||||||
import { IColumnSetting } from "../Explorer/Panes/Tables/TableColumnOptionsPane";
|
import { IColumnSetting } from "../Explorer/Panes/Tables/TableColumnOptionsPane";
|
||||||
import { IContentProvider } from "@nteract/core";
|
import { JunoClient, IGalleryItem } from "../Juno/JunoClient";
|
||||||
import { JunoClient } from "../Juno/JunoClient";
|
|
||||||
import { Library } from "./DataModels";
|
import { Library } from "./DataModels";
|
||||||
import { MostRecentActivity } from "../Explorer/MostRecentActivity/MostRecentActivity";
|
import { MostRecentActivity } from "../Explorer/MostRecentActivity/MostRecentActivity";
|
||||||
import { NotebookContentItem } from "../Explorer/Notebook/NotebookContentItem";
|
import { NotebookContentItem } from "../Explorer/Notebook/NotebookContentItem";
|
||||||
@@ -27,6 +25,7 @@ import { StringInputPane } from "../Explorer/Panes/StringInputPane";
|
|||||||
import { TextFieldProps } from "../Explorer/Controls/DialogReactComponent/DialogComponent";
|
import { TextFieldProps } from "../Explorer/Controls/DialogReactComponent/DialogComponent";
|
||||||
import { UploadDetails } from "../workers/upload/definitions";
|
import { UploadDetails } from "../workers/upload/definitions";
|
||||||
import { UploadItemsPaneAdapter } from "../Explorer/Panes/UploadItemsPaneAdapter";
|
import { UploadItemsPaneAdapter } from "../Explorer/Panes/UploadItemsPaneAdapter";
|
||||||
|
import { ReactAdapter } from "../Bindings/ReactBindingHandler";
|
||||||
|
|
||||||
export interface ExplorerOptions {
|
export interface ExplorerOptions {
|
||||||
documentClientUtility: DocumentClientUtilityBase;
|
documentClientUtility: DocumentClientUtilityBase;
|
||||||
@@ -84,8 +83,9 @@ export interface Explorer {
|
|||||||
extensionEndpoint: ko.Observable<string>;
|
extensionEndpoint: ko.Observable<string>;
|
||||||
armEndpoint: ko.Observable<string>;
|
armEndpoint: ko.Observable<string>;
|
||||||
isFeatureEnabled: (feature: string) => boolean;
|
isFeatureEnabled: (feature: string) => boolean;
|
||||||
isGalleryEnabled: ko.Computed<boolean>;
|
isGalleryPublishEnabled: ko.Computed<boolean>;
|
||||||
isGitHubPaneEnabled: ko.Observable<boolean>;
|
isGitHubPaneEnabled: ko.Observable<boolean>;
|
||||||
|
isPublishNotebookPaneEnabled: ko.Observable<boolean>;
|
||||||
isRightPanelV2Enabled: ko.Computed<boolean>;
|
isRightPanelV2Enabled: ko.Computed<boolean>;
|
||||||
canExceedMaximumValue: ko.Computed<boolean>;
|
canExceedMaximumValue: ko.Computed<boolean>;
|
||||||
hasAutoPilotV2FeatureFlag: ko.Computed<boolean>;
|
hasAutoPilotV2FeatureFlag: ko.Computed<boolean>;
|
||||||
@@ -153,6 +153,7 @@ export interface Explorer {
|
|||||||
libraryManagePane: ContextualPane;
|
libraryManagePane: ContextualPane;
|
||||||
clusterLibraryPane: ContextualPane;
|
clusterLibraryPane: ContextualPane;
|
||||||
gitHubReposPane: ContextualPane;
|
gitHubReposPane: ContextualPane;
|
||||||
|
publishNotebookPaneAdapter: ReactAdapter;
|
||||||
|
|
||||||
// Facade
|
// Facade
|
||||||
logConsoleData(data: ConsoleData): void;
|
logConsoleData(data: ConsoleData): void;
|
||||||
@@ -224,22 +225,17 @@ export interface Explorer {
|
|||||||
arcadiaWorkspaces: ko.ObservableArray<ArcadiaWorkspaceItem>;
|
arcadiaWorkspaces: ko.ObservableArray<ArcadiaWorkspaceItem>;
|
||||||
isNotebookTabActive: ko.Computed<boolean>;
|
isNotebookTabActive: ko.Computed<boolean>;
|
||||||
memoryUsageInfo: ko.Observable<DataModels.MemoryUsageInfo>;
|
memoryUsageInfo: ko.Observable<DataModels.MemoryUsageInfo>;
|
||||||
|
notebookManager?: any; // This is dynamically loaded
|
||||||
openNotebook(notebookContentItem: NotebookContentItem): Promise<boolean>; // True if it was opened, false otherwise
|
openNotebook(notebookContentItem: NotebookContentItem): Promise<boolean>; // True if it was opened, false otherwise
|
||||||
resetNotebookWorkspace(): void;
|
resetNotebookWorkspace(): void;
|
||||||
importAndOpen: (path: string) => Promise<boolean>;
|
importAndOpen: (path: string) => Promise<boolean>;
|
||||||
importAndOpenFromGallery: (path: string, newName: string, content: any) => Promise<boolean>;
|
importAndOpenContent: (name: string, content: string) => Promise<boolean>;
|
||||||
|
publishNotebook: (name: string, content: string) => void;
|
||||||
openNotebookTerminal: (kind: TerminalKind) => void;
|
openNotebookTerminal: (kind: TerminalKind) => void;
|
||||||
openGallery: () => void;
|
openGallery: (notebookUrl?: string, galleryItem?: IGalleryItem, isFavorite?: boolean) => void;
|
||||||
openNotebookViewer: (
|
openNotebookViewer: (notebookUrl: string) => void;
|
||||||
notebookUrl: string,
|
|
||||||
notebookMetadata: DataModels.NotebookMetadata,
|
|
||||||
onNotebookMetadataChange: (newNotebookMetadata: DataModels.NotebookMetadata) => Promise<void>,
|
|
||||||
isLikedNotebook: boolean
|
|
||||||
) => void;
|
|
||||||
notebookWorkspaceManager: NotebookWorkspaceManager;
|
notebookWorkspaceManager: NotebookWorkspaceManager;
|
||||||
sparkClusterManager: SparkClusterManager;
|
sparkClusterManager: SparkClusterManager;
|
||||||
notebookContentProvider: IContentProvider;
|
|
||||||
gitHubOAuthService: GitHubOAuthService;
|
|
||||||
mostRecentActivity: MostRecentActivity;
|
mostRecentActivity: MostRecentActivity;
|
||||||
initNotebooks: (databaseAccount: DataModels.DatabaseAccount) => Promise<void>;
|
initNotebooks: (databaseAccount: DataModels.DatabaseAccount) => Promise<void>;
|
||||||
deleteCluster(): void;
|
deleteCluster(): void;
|
||||||
@@ -594,6 +590,16 @@ export interface GitHubReposPaneOptions extends PaneOptions {
|
|||||||
junoClient: JunoClient;
|
junoClient: JunoClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PublishNotebookPaneOptions extends PaneOptions {
|
||||||
|
junoClient: JunoClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PublishNotebookPaneOpenOptions {
|
||||||
|
name: string;
|
||||||
|
author: string;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface AddCollectionPaneOptions extends PaneOptions {
|
export interface AddCollectionPaneOptions extends PaneOptions {
|
||||||
isPreferredApiTable: ko.Computed<boolean>;
|
isPreferredApiTable: ko.Computed<boolean>;
|
||||||
databaseId?: string;
|
databaseId?: string;
|
||||||
@@ -873,16 +879,16 @@ export interface TerminalTabOptions extends TabOptions {
|
|||||||
export interface GalleryTabOptions extends TabOptions {
|
export interface GalleryTabOptions extends TabOptions {
|
||||||
account: DatabaseAccount;
|
account: DatabaseAccount;
|
||||||
container: Explorer;
|
container: Explorer;
|
||||||
|
junoClient: JunoClient;
|
||||||
|
notebookUrl?: string;
|
||||||
|
galleryItem?: IGalleryItem;
|
||||||
|
isFavorite?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NotebookViewerTabOptions extends TabOptions {
|
export interface NotebookViewerTabOptions extends TabOptions {
|
||||||
account: DatabaseAccount;
|
account: DatabaseAccount;
|
||||||
container: Explorer;
|
container: Explorer;
|
||||||
notebookUrl: string;
|
notebookUrl: string;
|
||||||
notebookName: string;
|
|
||||||
notebookMetadata: DataModels.NotebookMetadata;
|
|
||||||
onNotebookMetadataChange: (newNotebookMetadata: DataModels.NotebookMetadata) => Promise<void>;
|
|
||||||
isLikedNotebook: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DocumentsTabOptions extends TabOptions {
|
export interface DocumentsTabOptions extends TabOptions {
|
||||||
|
|||||||
@@ -198,7 +198,7 @@ export class Heatmap {
|
|||||||
let timeSelected: string = data.points[0].x;
|
let timeSelected: string = data.points[0].x;
|
||||||
timeSelected = timeSelected.replace(" ", "T");
|
timeSelected = timeSelected.replace(" ", "T");
|
||||||
timeSelected = `${timeSelected}Z`;
|
timeSelected = `${timeSelected}Z`;
|
||||||
let xAxisIndex;
|
let xAxisIndex = 0;
|
||||||
for (let i = 0; i < this._chartData.xAxisPoints.length; i++) {
|
for (let i = 0; i < this._chartData.xAxisPoints.length; i++) {
|
||||||
if (this._chartData.xAxisPoints[i] === timeSelected) {
|
if (this._chartData.xAxisPoints[i] === timeSelected) {
|
||||||
xAxisIndex = i;
|
xAxisIndex = i;
|
||||||
@@ -234,7 +234,8 @@ export function handleMessage(event: MessageEvent) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Plotly.purge(Heatmap.elementId);
|
Plotly.purge(Heatmap.elementId);
|
||||||
document.getElementById(Heatmap.elementId).innerHTML = "";
|
|
||||||
|
document.getElementById(Heatmap.elementId)!.innerHTML = "";
|
||||||
const data = event.data.data;
|
const data = event.data.data;
|
||||||
const chartData: DataPayload = data.chartData;
|
const chartData: DataPayload = data.chartData;
|
||||||
const chartSettings: HeatmapCaptions = data.chartSettings;
|
const chartSettings: HeatmapCaptions = data.chartSettings;
|
||||||
@@ -259,8 +260,8 @@ export function handleMessage(event: MessageEvent) {
|
|||||||
noDataMessageContent.classList.add("dark-theme");
|
noDataMessageContent.classList.add("dark-theme");
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById(Heatmap.elementId).appendChild(chartTitleElement);
|
document.getElementById(Heatmap.elementId)!.appendChild(chartTitleElement);
|
||||||
document.getElementById(Heatmap.elementId).appendChild(noDataMessageElement);
|
document.getElementById(Heatmap.elementId)!.appendChild(noDataMessageElement);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
IContextualMenuProps,
|
IContextualMenuProps,
|
||||||
ContextualMenuItemType
|
ContextualMenuItemType
|
||||||
} from "office-ui-fabric-react/lib/ContextualMenu";
|
} from "office-ui-fabric-react/lib/ContextualMenu";
|
||||||
import { Logger } from "../../../Common/Logger";
|
import * as Logger from "../../../Common/Logger";
|
||||||
|
|
||||||
export interface ArcadiaMenuPickerProps {
|
export interface ArcadiaMenuPickerProps {
|
||||||
selectText?: string;
|
selectText?: string;
|
||||||
|
|||||||
@@ -15,15 +15,20 @@ import { ArcadiaMenuPickerProps } from "../Arcadia/ArcadiaMenuPicker";
|
|||||||
* Options for this component
|
* Options for this component
|
||||||
*/
|
*/
|
||||||
export interface CommandButtonComponentProps {
|
export interface CommandButtonComponentProps {
|
||||||
|
/**
|
||||||
|
* font icon name for the button
|
||||||
|
*/
|
||||||
|
iconName?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* image source for the button icon
|
* image source for the button icon
|
||||||
*/
|
*/
|
||||||
iconSrc: string;
|
iconSrc?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* image alt for accessibility
|
* image alt for accessibility
|
||||||
*/
|
*/
|
||||||
iconAlt: string;
|
iconAlt?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Click handler for command button click
|
* Click handler for command button click
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export const FeaturePanelComponent: React.FunctionComponent = () => {
|
|||||||
{ key: "feature.dataexplorerexecutesproc", label: "Execute stored procedure", value: "true" },
|
{ key: "feature.dataexplorerexecutesproc", label: "Execute stored procedure", value: "true" },
|
||||||
{ key: "feature.hosteddataexplorerenabled", label: "Hosted Data Explorer (deprecated?)", value: "true" },
|
{ key: "feature.hosteddataexplorerenabled", label: "Hosted Data Explorer (deprecated?)", value: "true" },
|
||||||
{ key: "feature.enablettl", label: "Enable TTL", 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.canexceedmaximumvalue", label: "Can exceed max value", value: "true" },
|
||||||
{
|
{
|
||||||
key: "feature.enablefixedcollectionwithsharedthroughput",
|
key: "feature.enablefixedcollectionwithsharedthroughput",
|
||||||
@@ -133,7 +133,7 @@ export const FeaturePanelComponent: React.FunctionComponent = () => {
|
|||||||
.find(f => f.key === "feature.notebookserverurl")
|
.find(f => f.key === "feature.notebookserverurl")
|
||||||
.reactState[1]("https://localhost:10001/12345/notebook/");
|
.reactState[1]("https://localhost:10001/12345/notebook/");
|
||||||
stringFeatures.find(f => f.key === "feature.notebookservertoken").reactState[1]("token");
|
stringFeatures.find(f => f.key === "feature.notebookservertoken").reactState[1]("token");
|
||||||
stringFeatures.find(f => f.key === "feature.notebookbasepath").reactState[1](".");
|
stringFeatures.find(f => f.key === "feature.notebookbasepath").reactState[1]("./notebooks");
|
||||||
setPlatform(platformOptions.find(o => o.key === "Hosted"));
|
setPlatform(platformOptions.find(o => o.key === "Hosted"));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -157,8 +157,8 @@ exports[`Feature panel renders all flags 1`] = `
|
|||||||
/>
|
/>
|
||||||
<StyledCheckboxBase
|
<StyledCheckboxBase
|
||||||
checked={false}
|
checked={false}
|
||||||
key="feature.enablegallery"
|
key="feature.enablegallerypublish"
|
||||||
label="Enable Notebook Gallery"
|
label="Enable Notebook Gallery Publishing"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
/>
|
/>
|
||||||
<StyledCheckboxBase
|
<StyledCheckboxBase
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import * as Constants from "../../../Common/Constants";
|
|||||||
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
|
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||||
import { RepoListItem } from "./GitHubReposComponent";
|
import { RepoListItem } from "./GitHubReposComponent";
|
||||||
import { ChildrenMargin } from "./GitHubStyleConstants";
|
import { ChildrenMargin } from "./GitHubStyleConstants";
|
||||||
import { GitHubUtils } from "../../../Utils/GitHubUtils";
|
import * as GitHubUtils from "../../../Utils/GitHubUtils";
|
||||||
import { IGitHubRepo } from "../../../GitHub/GitHubClient";
|
import { IGitHubRepo } from "../../../GitHub/GitHubClient";
|
||||||
import TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
import TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||||
import UrlUtility from "../../../Common/UrlUtility";
|
import UrlUtility from "../../../Common/UrlUtility";
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import {
|
|||||||
} from "office-ui-fabric-react";
|
} from "office-ui-fabric-react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { IGitHubBranch, IGitHubPageInfo } from "../../../GitHub/GitHubClient";
|
import { IGitHubBranch, IGitHubPageInfo } from "../../../GitHub/GitHubClient";
|
||||||
import { GitHubUtils } from "../../../Utils/GitHubUtils";
|
import * as GitHubUtils from "../../../Utils/GitHubUtils";
|
||||||
import { RepoListItem } from "./GitHubReposComponent";
|
import { RepoListItem } from "./GitHubReposComponent";
|
||||||
import {
|
import {
|
||||||
BranchesDropdownCheckboxStyles,
|
BranchesDropdownCheckboxStyles,
|
||||||
|
|||||||
@@ -4,4 +4,21 @@
|
|||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 100%;
|
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 * as React from "react";
|
||||||
|
import "./InputTypeahead.less";
|
||||||
import { KeyCodes } from "../../../Common/Constants";
|
import { KeyCodes } from "../../../Common/Constants";
|
||||||
|
|
||||||
export interface Item {
|
export interface Item {
|
||||||
@@ -17,7 +18,7 @@ export interface Item {
|
|||||||
/**
|
/**
|
||||||
* Parameters for this component
|
* Parameters for this component
|
||||||
*/
|
*/
|
||||||
interface InputTypeaheadComponentProps {
|
export interface InputTypeaheadComponentProps {
|
||||||
/**
|
/**
|
||||||
* List of choices available in the dropdown.
|
* 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
|
* true: show (X) button that clears the text inside the textbox when typing
|
||||||
*/
|
*/
|
||||||
showCancelButton?: boolean;
|
showCancelButton?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* true: use <textarea /> instead of <input />
|
||||||
|
*/
|
||||||
|
useTextarea?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface OnClickItem {
|
interface OnClickItem {
|
||||||
@@ -135,14 +141,25 @@ export class InputTypeaheadComponent extends React.Component<
|
|||||||
<div className="typeahead__container" ref={input => (this.containerElt = input)}>
|
<div className="typeahead__container" ref={input => (this.containerElt = input)}>
|
||||||
<div className="typeahead__field">
|
<div className="typeahead__field">
|
||||||
<span className="typeahead__query">
|
<span className="typeahead__query">
|
||||||
<input
|
{this.props.useTextarea ? (
|
||||||
name="q"
|
<textarea
|
||||||
type="search"
|
rows={1}
|
||||||
autoComplete="off"
|
name="q"
|
||||||
aria-label="Input query"
|
autoComplete="off"
|
||||||
ref={input => (this.inputElt = input)}
|
aria-label="Input query"
|
||||||
defaultValue={this.props.defaultValue}
|
ref={input => (this.inputElt = input)}
|
||||||
/>
|
defaultValue={this.props.defaultValue}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<input
|
||||||
|
name="q"
|
||||||
|
type="search"
|
||||||
|
autoComplete="off"
|
||||||
|
aria-label="Input query"
|
||||||
|
ref={input => (this.inputElt = input)}
|
||||||
|
defaultValue={this.props.defaultValue}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
{this.props.showSearchButton && (
|
{this.props.showSearchButton && (
|
||||||
<span className="typeahead__button">
|
<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>
|
||||||
|
`;
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as DataModels from "../../../Contracts/DataModels";
|
import * as DataModels from "../../../Contracts/DataModels";
|
||||||
import { Logger } from "../../../Common/Logger";
|
import * as Logger from "../../../Common/Logger";
|
||||||
import { NotificationConsoleUtils } from "../../../Utils/NotificationConsoleUtils";
|
import { NotificationConsoleUtils } from "../../../Utils/NotificationConsoleUtils";
|
||||||
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
|
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
|
||||||
import { StringUtils } from "../../../Utils/StringUtils";
|
import { StringUtils } from "../../../Utils/StringUtils";
|
||||||
|
|||||||
@@ -1,15 +1,32 @@
|
|||||||
import React from "react";
|
|
||||||
import { shallow } from "enzyme";
|
import { shallow } from "enzyme";
|
||||||
|
import React from "react";
|
||||||
import { GalleryCardComponent, GalleryCardComponentProps } from "./GalleryCardComponent";
|
import { GalleryCardComponent, GalleryCardComponentProps } from "./GalleryCardComponent";
|
||||||
|
|
||||||
describe("GalleryCardComponent", () => {
|
describe("GalleryCardComponent", () => {
|
||||||
it("renders", () => {
|
it("renders", () => {
|
||||||
const props: GalleryCardComponentProps = {
|
const props: GalleryCardComponentProps = {
|
||||||
name: "mycard",
|
data: {
|
||||||
url: "url",
|
id: "id",
|
||||||
notebookMetadata: undefined,
|
name: "name",
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
description: "description",
|
||||||
onClick: () => {}
|
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} />);
|
const wrapper = shallow(<GalleryCardComponent {...props} />);
|
||||||
|
|||||||
@@ -1,65 +1,207 @@
|
|||||||
import * as React from "react";
|
import { Card, ICardTokens } from "@uifabric/react-cards";
|
||||||
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 {
|
import {
|
||||||
siteTextStyles,
|
FontWeights,
|
||||||
descriptionTextStyles,
|
Icon,
|
||||||
helpfulTextStyles,
|
IconButton,
|
||||||
subtleHelpfulTextStyles,
|
Image,
|
||||||
subtleIconStyles
|
ImageFit,
|
||||||
} from "./CardStyleConstants";
|
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";
|
||||||
|
import CosmosDBLogo from "../../../../../images/CosmosDB-logo.svg";
|
||||||
|
|
||||||
export interface GalleryCardComponentProps {
|
export interface GalleryCardComponentProps {
|
||||||
name: string;
|
data: IGalleryItem;
|
||||||
url: string;
|
isFavorite: boolean;
|
||||||
notebookMetadata: DataModels.NotebookMetadata;
|
showDelete: boolean;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
|
onTagClick: (tag: string) => void;
|
||||||
|
onFavoriteClick: () => void;
|
||||||
|
onUnfavoriteClick: () => void;
|
||||||
|
onDownloadClick: () => void;
|
||||||
|
onDeleteClick: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class GalleryCardComponent extends React.Component<GalleryCardComponentProps> {
|
export class GalleryCardComponent extends React.Component<GalleryCardComponentProps> {
|
||||||
private cardTokens: ICardTokens = { childrenMargin: 12 };
|
public static readonly CARD_HEIGHT = 384;
|
||||||
private attendantsCardSectionTokens: ICardSectionTokens = { childrenGap: 6 };
|
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 {
|
public render(): JSX.Element {
|
||||||
return this.props.notebookMetadata != null ? (
|
const options: Intl.DateTimeFormatOptions = {
|
||||||
<Card aria-label="Notebook Card" onClick={this.props.onClick} tokens={this.cardTokens}>
|
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>
|
<Card.Item>
|
||||||
<Persona text={this.props.notebookMetadata.author} secondaryText={this.props.notebookMetadata.date} />
|
<Persona
|
||||||
|
imageUrl={this.props.data.isSample && CosmosDBLogo}
|
||||||
|
text={this.props.data.author}
|
||||||
|
secondaryText={dateString}
|
||||||
|
/>
|
||||||
</Card.Item>
|
</Card.Item>
|
||||||
|
|
||||||
<Card.Item fill>
|
<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.Item>
|
||||||
|
|
||||||
<Card.Section>
|
<Card.Section>
|
||||||
<Text variant="small" styles={siteTextStyles}>
|
<Text variant="small" nowrap>
|
||||||
{this.props.notebookMetadata.tags.join(", ")}
|
{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>
|
||||||
<Text styles={descriptionTextStyles}>{this.props.name}</Text>
|
<Text styles={{ root: { fontWeight: FontWeights.semibold } }} nowrap>
|
||||||
<Text variant="small" styles={helpfulTextStyles}>
|
{FileSystemUtil.stripExtension(this.props.data.name, "ipynb")}
|
||||||
{this.props.notebookMetadata.description}
|
</Text>
|
||||||
|
<Text variant="small" styles={{ root: { height: 36 } }}>
|
||||||
|
{this.props.data.description.substr(0, GalleryCardComponent.cardDescriptionMaxChars)}
|
||||||
</Text>
|
</Text>
|
||||||
</Card.Section>
|
</Card.Section>
|
||||||
<Card.Section horizontal tokens={this.attendantsCardSectionTokens}>
|
|
||||||
<Icon iconName="RedEye" styles={subtleIconStyles} />
|
<Card.Section horizontal styles={{ root: { alignItems: "flex-end" } }}>
|
||||||
<Text variant="small" styles={subtleHelpfulTextStyles}>
|
{this.generateIconText("RedEye", this.props.data.views.toString())}
|
||||||
{this.props.notebookMetadata.views}
|
{this.generateIconText("Download", this.props.data.downloads.toString())}
|
||||||
</Text>
|
{this.props.isFavorite !== undefined && this.generateIconText("Heart", this.props.data.favorites.toString())}
|
||||||
<Icon iconName="Download" styles={subtleIconStyles} />
|
|
||||||
<Text variant="small" styles={subtleHelpfulTextStyles}>
|
|
||||||
{this.props.notebookMetadata.downloads}
|
|
||||||
</Text>
|
|
||||||
<Icon iconName="Heart" styles={subtleIconStyles} />
|
|
||||||
<Text variant="small" styles={subtleHelpfulTextStyles}>
|
|
||||||
{this.props.notebookMetadata.likes}
|
|
||||||
</Text>
|
|
||||||
</Card.Section>
|
</Card.Section>
|
||||||
</Card>
|
|
||||||
) : (
|
<Card.Item>
|
||||||
<Card aria-label="Notebook Card" onClick={this.props.onClick} tokens={this.cardTokens}>
|
<Separator styles={{ root: { padding: 0, height: 1 } }} />
|
||||||
<Card.Section>
|
</Card.Item>
|
||||||
<Text styles={descriptionTextStyles}>{this.props.name}</Text>
|
|
||||||
|
<Card.Section horizontal styles={{ root: { marginTop: 0 } }}>
|
||||||
|
{this.props.isFavorite !== undefined &&
|
||||||
|
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.onDeleteClick)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</Card.Section>
|
</Card.Section>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private generateIconText = (iconName: string, text: string): JSX.Element => {
|
||||||
|
return (
|
||||||
|
<Text variant="tiny" styles={{ root: { color: "#ccc" } }}>
|
||||||
|
<Icon iconName={iconName} styles={{ root: { verticalAlign: "middle" } }} /> {text}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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,265 @@
|
|||||||
exports[`GalleryCardComponent renders 1`] = `
|
exports[`GalleryCardComponent renders 1`] = `
|
||||||
<Card
|
<Card
|
||||||
aria-label="Notebook Card"
|
aria-label="Notebook Card"
|
||||||
onClick={[Function]}
|
|
||||||
tokens={
|
tokens={
|
||||||
Object {
|
Object {
|
||||||
"childrenMargin": 12,
|
"childrenGap": 8,
|
||||||
|
"childrenMargin": 10,
|
||||||
|
"height": 384,
|
||||||
|
"width": 256,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
<CardItem>
|
||||||
|
<StyledPersonaBase
|
||||||
|
imageUrl={false}
|
||||||
|
secondaryText="Invalid Date"
|
||||||
|
text="author"
|
||||||
|
/>
|
||||||
|
</CardItem>
|
||||||
|
<CardItem
|
||||||
|
fill={true}
|
||||||
|
>
|
||||||
|
<StyledImageBase
|
||||||
|
alt="Notebook cover image"
|
||||||
|
height={144}
|
||||||
|
imageFit={2}
|
||||||
|
src="thumbnailUrl"
|
||||||
|
width={256}
|
||||||
|
/>
|
||||||
|
</CardItem>
|
||||||
<CardSection>
|
<CardSection>
|
||||||
<Text
|
<Text
|
||||||
|
nowrap={true}
|
||||||
|
variant="small"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
key="tag"
|
||||||
|
>
|
||||||
|
<StyledLinkBase
|
||||||
|
onClick={[Function]}
|
||||||
|
>
|
||||||
|
tag
|
||||||
|
</StyledLinkBase>
|
||||||
|
</span>
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
nowrap={true}
|
||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
"color": "#333333",
|
|
||||||
"fontWeight": 600,
|
"fontWeight": 600,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
mycard
|
name
|
||||||
</Text>
|
</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",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onClick={[Function]}
|
||||||
|
title="Remove"
|
||||||
|
/>
|
||||||
|
</StyledTooltipHostBase>
|
||||||
|
</div>
|
||||||
</CardSection>
|
</CardSection>
|
||||||
</Card>
|
</Card>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -1,62 +1,17 @@
|
|||||||
import React from "react";
|
|
||||||
import { shallow } from "enzyme";
|
import { shallow } from "enzyme";
|
||||||
import {
|
import React from "react";
|
||||||
GalleryViewerContainerComponent,
|
import { GalleryViewerComponent, GalleryViewerComponentProps, GalleryTab, SortBy } from "./GalleryViewerComponent";
|
||||||
GalleryViewerContainerComponentProps,
|
|
||||||
FullWidthTabs,
|
|
||||||
FullWidthTabsProps,
|
|
||||||
GalleryCardsComponent,
|
|
||||||
GalleryCardsComponentProps,
|
|
||||||
GalleryViewerComponent,
|
|
||||||
GalleryViewerComponentProps
|
|
||||||
} from "./GalleryViewerComponent";
|
|
||||||
|
|
||||||
describe("GalleryCardsComponent", () => {
|
describe("GalleryViewerComponent", () => {
|
||||||
it("renders", () => {
|
|
||||||
// TODO Mock this
|
|
||||||
const props: GalleryCardsComponentProps = {
|
|
||||||
data: [],
|
|
||||||
userMetadata: undefined,
|
|
||||||
onNotebookMetadataChange: () => Promise.resolve(),
|
|
||||||
onClick: () => Promise.resolve()
|
|
||||||
};
|
|
||||||
|
|
||||||
const wrapper = shallow(<GalleryCardsComponent {...props} />);
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("FullWidthTabs", () => {
|
|
||||||
it("renders", () => {
|
|
||||||
const props: FullWidthTabsProps = {
|
|
||||||
officialSamplesContent: [],
|
|
||||||
likedNotebooksContent: [],
|
|
||||||
userMetadata: undefined,
|
|
||||||
onClick: () => 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", () => {
|
|
||||||
it("renders", () => {
|
it("renders", () => {
|
||||||
const props: GalleryViewerComponentProps = {
|
const props: GalleryViewerComponentProps = {
|
||||||
container: undefined,
|
junoClient: undefined,
|
||||||
officialSamplesData: [],
|
selectedTab: GalleryTab.OfficialSamples,
|
||||||
likedNotebookData: undefined
|
sortBy: SortBy.MostViewed,
|
||||||
|
searchText: undefined,
|
||||||
|
onSelectedTabChange: undefined,
|
||||||
|
onSortByChange: undefined,
|
||||||
|
onSearchTextChange: undefined
|
||||||
};
|
};
|
||||||
|
|
||||||
const wrapper = shallow(<GalleryViewerComponent {...props} />);
|
const wrapper = shallow(<GalleryViewerComponent {...props} />);
|
||||||
|
|||||||
@@ -1,361 +1,511 @@
|
|||||||
/**
|
import {
|
||||||
* Gallery Viewer
|
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 React from "react";
|
||||||
import * as DataModels from "../../../Contracts/DataModels";
|
import * as Logger from "../../../Common/Logger";
|
||||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||||
import { GalleryCardComponent } from "./Cards/GalleryCardComponent";
|
import { IGalleryItem, JunoClient } from "../../../Juno/JunoClient";
|
||||||
import { Stack, IStackTokens } from "office-ui-fabric-react";
|
import * as GalleryUtils from "../../../Utils/GalleryUtils";
|
||||||
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 { NotificationConsoleUtils } from "../../../Utils/NotificationConsoleUtils";
|
import { NotificationConsoleUtils } from "../../../Utils/NotificationConsoleUtils";
|
||||||
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
|
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 "./GalleryViewerComponent.less";
|
||||||
|
import { HttpStatusCodes } from "../../../Common/Constants";
|
||||||
|
|
||||||
export interface GalleryCardsComponentProps {
|
export interface GalleryViewerComponentProps {
|
||||||
data: DataModels.GitHubInfoJunoResponse[];
|
container?: ViewModels.Explorer;
|
||||||
userMetadata: DataModels.UserMetadata;
|
junoClient: JunoClient;
|
||||||
onNotebookMetadataChange: (
|
selectedTab: GalleryTab;
|
||||||
officialSamplesIndex: number,
|
sortBy: SortBy;
|
||||||
notebookMetadata: DataModels.NotebookMetadata
|
searchText: string;
|
||||||
) => Promise<void>;
|
onSelectedTabChange: (newTab: GalleryTab) => void;
|
||||||
onClick: (
|
onSortByChange: (sortBy: SortBy) => void;
|
||||||
url: string,
|
onSearchTextChange: (searchText: string) => void;
|
||||||
notebookMetadata: DataModels.NotebookMetadata,
|
|
||||||
onNotebookMetadataChange: (newNotebookMetadata: DataModels.NotebookMetadata) => Promise<void>,
|
|
||||||
isLikedNotebook: boolean
|
|
||||||
) => Promise<void>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class GalleryCardsComponent extends React.Component<GalleryCardsComponentProps> {
|
export enum GalleryTab {
|
||||||
private sectionStackTokens: IStackTokens = { childrenGap: 30 };
|
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 liked";
|
||||||
|
private static readonly mostRecentText = "Most recent";
|
||||||
|
|
||||||
|
private readonly sortingOptions: IDropdownOption[];
|
||||||
|
|
||||||
|
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.sortingOptions = [
|
||||||
|
{
|
||||||
|
key: SortBy.MostViewed,
|
||||||
|
text: GalleryViewerComponent.mostViewedText
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: SortBy.MostDownloaded,
|
||||||
|
text: GalleryViewerComponent.mostDownloadedText
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: SortBy.MostRecent,
|
||||||
|
text: GalleryViewerComponent.mostRecentText
|
||||||
|
}
|
||||||
|
];
|
||||||
|
if (this.props.container?.isGalleryPublishEnabled()) {
|
||||||
|
this.sortingOptions.push({
|
||||||
|
key: SortBy.MostFavorited,
|
||||||
|
text: GalleryViewerComponent.mostFavoritedText
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loadTabContent(this.state.selectedTab, this.state.searchText, this.state.sortBy, false);
|
||||||
|
if (this.props.container?.isGalleryPublishEnabled()) {
|
||||||
|
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 {
|
public render(): JSX.Element {
|
||||||
return (
|
const tabs: GalleryTabInfo[] = [this.createTab(GalleryTab.OfficialSamples, this.state.sampleNotebooks)];
|
||||||
<Stack horizontal wrap tokens={this.sectionStackTokens}>
|
|
||||||
{this.props.data.map((githubInfo: DataModels.GitHubInfoJunoResponse) => {
|
|
||||||
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 officialSamplesIndex = githubInfo.officialSamplesIndex;
|
|
||||||
const isLikedNotebook = githubInfo.isLikedNotebook;
|
|
||||||
const updateTabsStatePerNotebook = this.props.onNotebookMetadataChange
|
|
||||||
? (notebookMetadata: DataModels.NotebookMetadata): Promise<void> =>
|
|
||||||
this.props.onNotebookMetadataChange(officialSamplesIndex, notebookMetadata)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
return (
|
if (this.props.container?.isGalleryPublishEnabled()) {
|
||||||
name !== ".gitignore" &&
|
tabs.push(this.createTab(GalleryTab.PublicGallery, this.state.publicNotebooks));
|
||||||
url && (
|
tabs.push(this.createTab(GalleryTab.Favorites, this.state.favoriteNotebooks));
|
||||||
<GalleryCardComponent
|
tabs.push(this.createTab(GalleryTab.Published, this.state.publishedNotebooks));
|
||||||
key={url}
|
}
|
||||||
name={name}
|
|
||||||
url={url}
|
const pivotProps: IPivotProps = {
|
||||||
notebookMetadata={notebookMetadata}
|
onLinkClick: this.onPivotChange,
|
||||||
onClick={(): Promise<void> =>
|
selectedKey: GalleryTab[this.state.selectedTab]
|
||||||
this.props.onClick(url, notebookMetadata, updateTabsStatePerNotebook, isLikedNotebook)
|
};
|
||||||
}
|
|
||||||
/>
|
const pivotItems = tabs.map(tab => {
|
||||||
)
|
const pivotItemProps: IPivotItemProps = {
|
||||||
);
|
itemKey: GalleryTab[tab.tab],
|
||||||
})}
|
style: { marginTop: 20 },
|
||||||
|
headerText: GalleryUtils.getTabTitle(tab.tab)
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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={this.sortingOptions} selectedKey={this.state.sortBy} onChange={this.onDropdownChange} />
|
||||||
|
</Stack.Item>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
{data && this.createCardsTabContent(data)}
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export interface FullWidthTabsProps {
|
private createCardsTabContent(data: IGalleryItem[]): JSX.Element {
|
||||||
officialSamplesContent: DataModels.GitHubInfoJunoResponse[];
|
return (
|
||||||
likedNotebooksContent: DataModels.GitHubInfoJunoResponse[];
|
<FocusZone>
|
||||||
userMetadata: DataModels.UserMetadata;
|
<List
|
||||||
onClick: (
|
items={data}
|
||||||
url: string,
|
getPageSpecification={this.getPageSpecification}
|
||||||
notebookMetadata: DataModels.NotebookMetadata,
|
renderedWindowsAhead={3}
|
||||||
onNotebookMetadataChange: (newNotebookMetadata: DataModels.NotebookMetadata) => Promise<void>,
|
onRenderCell={this.onRenderCell}
|
||||||
isLikedNotebook: boolean
|
/>
|
||||||
) => Promise<void>;
|
</FocusZone>
|
||||||
}
|
);
|
||||||
|
|
||||||
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: (): JSX.Element => (
|
|
||||||
<GalleryCardsComponent
|
|
||||||
data={this.state.officialSamplesContent}
|
|
||||||
onClick={this.props.onClick}
|
|
||||||
userMetadata={this.state.userMetadata}
|
|
||||||
onNotebookMetadataChange={this.updateTabsState}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
isVisible: (): boolean => true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Liked Notebooks",
|
|
||||||
content: {
|
|
||||||
className: "",
|
|
||||||
render: (): JSX.Element => (
|
|
||||||
<GalleryCardsComponent
|
|
||||||
data={this.state.likedNotebooksContent}
|
|
||||||
onClick={this.props.onClick}
|
|
||||||
userMetadata={this.state.userMetadata}
|
|
||||||
onNotebookMetadataChange={this.updateTabsState}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
isVisible: (): boolean => true
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public updateTabsState = async (
|
private loadTabContent(tab: GalleryTab, searchText: string, sortBy: SortBy, offline: boolean): void {
|
||||||
officialSamplesIndex: number,
|
switch (tab) {
|
||||||
notebookMetadata: DataModels.NotebookMetadata
|
case GalleryTab.OfficialSamples:
|
||||||
): Promise<void> => {
|
this.loadSampleNotebooks(searchText, sortBy, offline);
|
||||||
let currentLikedNotebooksContent = [...this.state.likedNotebooksContent];
|
break;
|
||||||
let currentUserMetadata = { ...this.state.userMetadata };
|
|
||||||
let currentLikedNotebooks = [...currentUserMetadata.likedNotebooks];
|
|
||||||
|
|
||||||
const currentOfficialSamplesContent = [...this.state.officialSamplesContent];
|
case GalleryTab.PublicGallery:
|
||||||
const currentOfficialSamplesObject = { ...currentOfficialSamplesContent[officialSamplesIndex] };
|
this.loadPublicNotebooks(searchText, sortBy, offline);
|
||||||
const metadata = { ...currentOfficialSamplesObject.metadata };
|
break;
|
||||||
const metadataLikesUpdates = metadata.likes - notebookMetadata.likes;
|
|
||||||
|
|
||||||
metadata.views = notebookMetadata.views;
|
case GalleryTab.Favorites:
|
||||||
metadata.downloads = notebookMetadata.downloads;
|
this.loadFavoriteNotebooks(searchText, sortBy, offline);
|
||||||
metadata.likes = notebookMetadata.likes;
|
break;
|
||||||
currentOfficialSamplesObject.metadata = metadata;
|
|
||||||
|
|
||||||
// Notebook has been liked. Add To likedNotebooksContent, update isLikedNotebook flag
|
case GalleryTab.Published:
|
||||||
if (metadataLikesUpdates < 0) {
|
this.loadPublishedNotebooks(searchText, sortBy, offline);
|
||||||
currentOfficialSamplesObject.isLikedNotebook = true;
|
break;
|
||||||
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;
|
default:
|
||||||
const likedNotebookIndex = currentLikedNotebooks.findIndex((path: string) => {
|
throw new Error(`Unknown tab ${tab}`);
|
||||||
return path === currentOfficialSamplesObject.path;
|
|
||||||
});
|
|
||||||
currentLikedNotebooksContent.splice(likedNotebookIndex, 1);
|
|
||||||
currentLikedNotebooks.splice(likedNotebookIndex, 1);
|
|
||||||
currentUserMetadata = { likedNotebooks: currentLikedNotebooks };
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
currentOfficialSamplesContent[officialSamplesIndex] = currentOfficialSamplesObject;
|
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({
|
this.setState({
|
||||||
activeTabIndex: 0,
|
sampleNotebooks: this.sampleNotebooks && [...this.sort(sortBy, this.search(searchText, this.sampleNotebooks))]
|
||||||
userMetadata: currentUserMetadata,
|
});
|
||||||
likedNotebooksContent: currentLikedNotebooksContent,
|
}
|
||||||
officialSamplesContent: currentOfficialSamplesContent
|
|
||||||
|
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`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.publicNotebooks = response.data;
|
||||||
|
} catch (error) {
|
||||||
|
const message = `Failed to load public notebooks: ${error}`;
|
||||||
|
Logger.logError(message, "GalleryViewerComponent/loadPublicNotebooks");
|
||||||
|
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
JunoUtils.updateNotebookMetadata(this.authorizationToken, notebookMetadata).then(
|
// Refresh favorite button state
|
||||||
async () => {
|
if (this.state.selectedTab !== GalleryTab.Favorites) {
|
||||||
if (metadataLikesUpdates !== 0) {
|
this.refreshSelectedTab();
|
||||||
JunoUtils.updateUserMetadata(this.authorizationToken, currentUserMetadata);
|
}
|
||||||
// TODO: update state here?
|
}
|
||||||
|
|
||||||
|
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`);
|
||||||
}
|
}
|
||||||
},
|
|
||||||
error => {
|
this.publishedNotebooks = response.data;
|
||||||
NotificationConsoleUtils.logConsoleMessage(
|
} catch (error) {
|
||||||
ConsoleDataType.Error,
|
const message = `Failed to load published notebooks: ${error}`;
|
||||||
`Error updating notebook metadata: ${JSON.stringify(error)}`
|
Logger.logError(message, "GalleryViewerComponent/loadPublishedNotebooks");
|
||||||
);
|
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message);
|
||||||
// TODO add telemetry
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 => {
|
||||||
|
let isFavorite: boolean;
|
||||||
|
if (this.props.container?.isGalleryPublishEnabled()) {
|
||||||
|
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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
private onTabIndexChange = (activeTabIndex: number): void => this.setState({ activeTabIndex });
|
private openNotebook = (data: IGalleryItem, isFavorite: boolean): void => {
|
||||||
|
if (this.props.container && this.props.junoClient) {
|
||||||
public render(): JSX.Element {
|
this.props.container.openGallery(this.props.junoClient.getNotebookContentUrl(data.id), data, isFavorite);
|
||||||
return (
|
|
||||||
<TabComponent.TabComponent
|
|
||||||
tabs={this.appTabs}
|
|
||||||
onTabIndexChange={this.onTabIndexChange.bind(this)}
|
|
||||||
currentTabIndex={this.state.activeTabIndex}
|
|
||||||
hideHeader={false}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GalleryViewerContainerComponentProps {
|
|
||||||
container: ViewModels.Explorer;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GalleryViewerContainerComponentState {
|
|
||||||
officialSamplesData: DataModels.GitHubInfoJunoResponse[];
|
|
||||||
likedNotebooksData: DataModels.LikedNotebooksJunoResponse;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class GalleryViewerContainerComponent extends React.Component<
|
|
||||||
GalleryViewerContainerComponentProps,
|
|
||||||
GalleryViewerContainerComponentState
|
|
||||||
> {
|
|
||||||
constructor(props: GalleryViewerContainerComponentProps) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
officialSamplesData: undefined,
|
|
||||||
likedNotebooksData: undefined
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount(): void {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
officialSamplesData: officialSamplesData,
|
|
||||||
likedNotebooksData: likedNotebooksData
|
|
||||||
});
|
|
||||||
},
|
|
||||||
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}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GalleryViewerComponentProps {
|
|
||||||
container: ViewModels.Explorer;
|
|
||||||
officialSamplesData: DataModels.GitHubInfoJunoResponse[];
|
|
||||||
likedNotebookData: DataModels.LikedNotebooksJunoResponse;
|
|
||||||
}
|
|
||||||
|
|
||||||
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}
|
|
||||||
/>
|
|
||||||
</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
|
|
||||||
): Promise<void> => {
|
|
||||||
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");
|
|
||||||
} else {
|
} 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
|
||||||
|
});
|
||||||
|
|
||||||
|
const location = new URL("./notebookViewer.html", window.location.href).href;
|
||||||
|
window.open(`${location}?${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,84 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`FullWidthTabs renders 1`] = `
|
exports[`GalleryViewerComponent 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`] = `
|
|
||||||
<div
|
<div
|
||||||
className="galleryContainer"
|
className="galleryContainer"
|
||||||
>
|
>
|
||||||
<GalleryCardsComponent
|
<StyledPivotBase
|
||||||
data={Array []}
|
onLinkClick={[Function]}
|
||||||
onClick={[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": 20,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<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": 3,
|
||||||
|
"text": "Most recent",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
selectedKey={0}
|
||||||
|
/>
|
||||||
|
</StackItem>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</PivotItem>
|
||||||
|
</StyledPivotBase>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`GalleryCardsComponent renders 1`] = `
|
|
||||||
<Stack
|
|
||||||
horizontal={true}
|
|
||||||
tokens={
|
|
||||||
Object {
|
|
||||||
"childrenGap": 30,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
wrap={true}
|
|
||||||
/>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`GalleryViewerContainerComponent renders 1`] = `<Fragment />`;
|
|
||||||
|
|||||||
@@ -1,16 +1,30 @@
|
|||||||
import React from "react";
|
|
||||||
import { shallow } from "enzyme";
|
import { shallow } from "enzyme";
|
||||||
import { NotebookMetadataComponentProps, NotebookMetadataComponent } from "./NotebookMetadataComponent";
|
import React from "react";
|
||||||
|
import { NotebookMetadataComponent, NotebookMetadataComponentProps } from "./NotebookMetadataComponent";
|
||||||
|
|
||||||
describe("NotebookMetadataComponent", () => {
|
describe("NotebookMetadataComponent", () => {
|
||||||
it("renders un-liked notebook", () => {
|
it("renders un-liked notebook", () => {
|
||||||
const props: NotebookMetadataComponentProps = {
|
const props: NotebookMetadataComponentProps = {
|
||||||
notebookName: "My notebook",
|
data: {
|
||||||
container: undefined,
|
id: "id",
|
||||||
notebookMetadata: undefined,
|
name: "name",
|
||||||
notebookContent: {},
|
description: "description",
|
||||||
onNotebookMetadataChange: () => Promise.resolve(),
|
author: "author",
|
||||||
isLikedNotebook: false
|
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} />);
|
const wrapper = shallow(<NotebookMetadataComponent {...props} />);
|
||||||
@@ -19,12 +33,26 @@ describe("NotebookMetadataComponent", () => {
|
|||||||
|
|
||||||
it("renders liked notebook", () => {
|
it("renders liked notebook", () => {
|
||||||
const props: NotebookMetadataComponentProps = {
|
const props: NotebookMetadataComponentProps = {
|
||||||
notebookName: "My notebook",
|
data: {
|
||||||
container: undefined,
|
id: "id",
|
||||||
notebookMetadata: undefined,
|
name: "name",
|
||||||
notebookContent: {},
|
description: "description",
|
||||||
onNotebookMetadataChange: () => Promise.resolve(),
|
author: "author",
|
||||||
isLikedNotebook: true
|
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} />);
|
const wrapper = shallow(<NotebookMetadataComponent {...props} />);
|
||||||
|
|||||||
@@ -1,189 +1,89 @@
|
|||||||
/**
|
/**
|
||||||
* Wrapper around Notebook metadata
|
* 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 {
|
import {
|
||||||
siteTextStyles,
|
FontWeights,
|
||||||
subtleIconStyles,
|
Icon,
|
||||||
iconStyles,
|
IconButton,
|
||||||
iconButtonStyles,
|
Link,
|
||||||
mainHelpfulTextStyles,
|
Persona,
|
||||||
subtleHelpfulTextStyles,
|
PersonaSize,
|
||||||
helpfulTextStyles
|
PrimaryButton,
|
||||||
} from "../NotebookGallery/Cards/CardStyleConstants";
|
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";
|
import "./NotebookViewerComponent.less";
|
||||||
|
|
||||||
initializeIcons();
|
|
||||||
|
|
||||||
export interface NotebookMetadataComponentProps {
|
export interface NotebookMetadataComponentProps {
|
||||||
notebookName: string;
|
data: IGalleryItem;
|
||||||
container: ViewModels.Explorer;
|
isFavorite: boolean;
|
||||||
notebookMetadata: NotebookMetadata;
|
downloadButtonText: string;
|
||||||
notebookContent: any;
|
onTagClick: (tag: string) => void;
|
||||||
onNotebookMetadataChange: (newNotebookMetadata: NotebookMetadata) => Promise<void>;
|
onFavoriteClick: () => void;
|
||||||
isLikedNotebook: boolean;
|
onUnfavoriteClick: () => void;
|
||||||
|
onDownloadClick: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface NotebookMetadatComponentState {
|
export class NotebookMetadataComponent extends React.Component<NotebookMetadataComponentProps> {
|
||||||
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);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
public render(): JSX.Element {
|
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 (
|
return (
|
||||||
<div className="notebookViewerMetadataContainer">
|
<Stack tokens={{ childrenGap: 10 }}>
|
||||||
<h3 className="title">{this.props.notebookName}</h3>
|
<Stack horizontal verticalAlign="center" tokens={{ childrenGap: 30 }}>
|
||||||
|
<Text variant="xxLarge" nowrap>
|
||||||
{this.props.notebookMetadata && (
|
{FileSystemUtil.stripExtension(this.props.data.name, "ipynb")}
|
||||||
<div className="decoration">
|
</Text>
|
||||||
{this.props.container ? (
|
<Text>
|
||||||
<IconButton
|
{this.props.isFavorite !== undefined && (
|
||||||
iconProps={{ iconName: this.state.liked ? "HeartFill" : "Heart" }}
|
<>
|
||||||
styles={iconButtonStyles}
|
<IconButton
|
||||||
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} />
|
{this.props.data.favorites} likes
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
<Text variant="large" styles={mainHelpfulTextStyles}>
|
</Text>
|
||||||
{this.state.notebookMetadata.likes} likes
|
<PrimaryButton text={this.props.downloadButtonText} onClick={this.props.onDownloadClick} />
|
||||||
</Text>
|
</Stack>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{this.props.container && (
|
<Stack horizontal verticalAlign="center" tokens={{ childrenGap: 10 }}>
|
||||||
<button aria-label="downloadButton" className="downloadButton" onClick={this.onDownload}>
|
<Persona text={this.props.data.author} size={PersonaSize.size32} />
|
||||||
Download Notebook
|
<Text>{dateString}</Text>
|
||||||
</button>
|
<Text>
|
||||||
)}
|
<Icon iconName="RedEye" /> {this.props.data.views}
|
||||||
|
</Text>
|
||||||
|
<Text>
|
||||||
|
<Icon iconName="Download" />
|
||||||
|
{this.props.data.downloads}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
{this.props.notebookMetadata && (
|
<Text nowrap>
|
||||||
<>
|
{this.props.data.tags?.map((tag, index, array) => (
|
||||||
<div>
|
<span key={tag}>
|
||||||
<Persona
|
<Link onClick={(): void => this.props.onTagClick(tag)}>{tag}</Link>
|
||||||
className="persona"
|
{index === array.length - 1 ? <></> : ", "}
|
||||||
text={this.props.notebookMetadata.author}
|
</span>
|
||||||
secondaryText={this.props.notebookMetadata.date}
|
))}
|
||||||
/>
|
</Text>
|
||||||
</div>
|
|
||||||
<div>
|
<Text variant="large" styles={{ root: { fontWeight: FontWeights.semibold } }}>
|
||||||
<div className="extras">
|
Description
|
||||||
<Icon iconName="RedEye" styles={subtleIconStyles} />
|
</Text>
|
||||||
<Text variant="small" styles={subtleHelpfulTextStyles}>
|
|
||||||
{this.state.notebookMetadata.views}
|
<Text>{this.props.data.description}</Text>
|
||||||
</Text>
|
</Stack>
|
||||||
<Icon iconName="Download" styles={subtleIconStyles} />
|
|
||||||
<Text variant="small" styles={subtleHelpfulTextStyles}>
|
|
||||||
{this.state.notebookMetadata.downloads}
|
|
||||||
</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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
@import "../../../../less/Common/Constants";
|
@import "../../../../less/Common/Constants";
|
||||||
|
|
||||||
.notebookViewerContainer {
|
.notebookViewerContainer {
|
||||||
padding: @DefaultSpace;
|
padding: 30px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|||||||
@@ -1,36 +1,44 @@
|
|||||||
/**
|
/**
|
||||||
* Wrapper around Notebook Viewer Read only content
|
* 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 * as React from "react";
|
||||||
|
import { contents } from "rx-jupyter";
|
||||||
|
import * as Logger from "../../../Common/Logger";
|
||||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
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 { NotebookClientV2 } from "../../Notebook/NotebookClientV2";
|
||||||
import { NotebookComponentBootstrapper } from "../../Notebook/NotebookComponent/NotebookComponentBootstrapper";
|
import { NotebookComponentBootstrapper } from "../../Notebook/NotebookComponent/NotebookComponentBootstrapper";
|
||||||
import { createContentRef } from "@nteract/core";
|
|
||||||
import NotebookReadOnlyRenderer from "../../Notebook/NotebookRenderer/NotebookReadOnlyRenderer";
|
import NotebookReadOnlyRenderer from "../../Notebook/NotebookRenderer/NotebookReadOnlyRenderer";
|
||||||
import { contents } from "rx-jupyter";
|
import { DialogComponent, DialogProps } from "../DialogReactComponent/DialogComponent";
|
||||||
import { NotebookMetadata } from "../../../Contracts/DataModels";
|
|
||||||
import { NotebookMetadataComponent } from "./NotebookMetadataComponent";
|
import { NotebookMetadataComponent } from "./NotebookMetadataComponent";
|
||||||
import "./NotebookViewerComponent.less";
|
import "./NotebookViewerComponent.less";
|
||||||
|
|
||||||
export interface NotebookViewerComponentProps {
|
export interface NotebookViewerComponentProps {
|
||||||
notebookName: string;
|
|
||||||
notebookUrl: string;
|
|
||||||
container?: ViewModels.Explorer;
|
container?: ViewModels.Explorer;
|
||||||
notebookMetadata: NotebookMetadata;
|
junoClient?: JunoClient;
|
||||||
onNotebookMetadataChange?: (newNotebookMetadata: NotebookMetadata) => Promise<void>;
|
notebookUrl: string;
|
||||||
isLikedNotebook?: boolean;
|
galleryItem?: IGalleryItem;
|
||||||
hideInputs?: boolean;
|
isFavorite?: boolean;
|
||||||
|
backNavigationText: string;
|
||||||
|
onBackClick: () => void;
|
||||||
|
onTagClick: (tag: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface NotebookViewerComponentState {
|
interface NotebookViewerComponentState {
|
||||||
content: any;
|
content: Notebook;
|
||||||
|
galleryItem?: IGalleryItem;
|
||||||
|
isFavorite?: boolean;
|
||||||
|
dialogProps: DialogProps;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class NotebookViewerComponent extends React.Component<
|
export class NotebookViewerComponent extends React.Component<NotebookViewerComponentProps, NotebookViewerComponentState>
|
||||||
NotebookViewerComponentProps,
|
implements GalleryUtils.DialogEnabledComponent {
|
||||||
NotebookViewerComponentState
|
|
||||||
> {
|
|
||||||
private clientManager: NotebookClientV2;
|
private clientManager: NotebookClientV2;
|
||||||
private notebookComponentBootstrapper: NotebookComponentBootstrapper;
|
private notebookComponentBootstrapper: NotebookComponentBootstrapper;
|
||||||
|
|
||||||
@@ -52,40 +60,118 @@ export class NotebookViewerComponent extends React.Component<
|
|||||||
contentRef: createContentRef()
|
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> {
|
setDialogProps = (dialogProps: DialogProps): void => {
|
||||||
const response: Response = await fetch(this.props.notebookUrl);
|
this.setState({ dialogProps });
|
||||||
if (response.ok) {
|
};
|
||||||
return await response.json();
|
|
||||||
} else {
|
private async loadNotebookContent(): Promise<void> {
|
||||||
return undefined;
|
try {
|
||||||
|
const response = await fetch(this.props.notebookUrl);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Received HTTP ${response.status} while fetching ${this.props.notebookUrl}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.getJsonNotebookContent().then((jsonContent: any) => {
|
|
||||||
this.notebookComponentBootstrapper.setContent("json", jsonContent);
|
|
||||||
this.setState({ content: jsonContent });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public render(): JSX.Element {
|
public render(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<div className="notebookViewerContainer">
|
<div className="notebookViewerContainer">
|
||||||
<NotebookMetadataComponent
|
{this.props.backNavigationText ? (
|
||||||
notebookMetadata={this.props.notebookMetadata}
|
<Link onClick={this.props.onBackClick}>
|
||||||
notebookName={this.props.notebookName}
|
<Icon iconName="Back" /> {this.props.backNavigationText}
|
||||||
container={this.props.container}
|
</Link>
|
||||||
notebookContent={this.state.content}
|
) : (
|
||||||
onNotebookMetadataChange={this.props.onNotebookMetadataChange}
|
<></>
|
||||||
isLikedNotebook={this.props.isLikedNotebook}
|
)}
|
||||||
/>
|
|
||||||
{this.notebookComponentBootstrapper.renderComponent(NotebookReadOnlyRenderer, {
|
{this.state.galleryItem ? (
|
||||||
hideInputs: this.props.hideInputs
|
<div style={{ margin: 10 }}>
|
||||||
})}
|
<NotebookMetadataComponent
|
||||||
|
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}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{this.notebookComponentBootstrapper.renderComponent(NotebookReadOnlyRenderer, { hideInputs: false })}
|
||||||
|
|
||||||
|
{this.state.dialogProps && <DialogComponent {...this.state.dialogProps} />}
|
||||||
</div>
|
</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
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`NotebookMetadataComponent renders liked notebook 1`] = `
|
exports[`NotebookMetadataComponent renders liked notebook 1`] = `
|
||||||
<div
|
<Stack
|
||||||
className="notebookViewerMetadataContainer"
|
tokens={
|
||||||
|
Object {
|
||||||
|
"childrenGap": 10,
|
||||||
|
}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<h3
|
<Stack
|
||||||
className="title"
|
horizontal={true}
|
||||||
|
tokens={
|
||||||
|
Object {
|
||||||
|
"childrenGap": 30,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
verticalAlign="center"
|
||||||
>
|
>
|
||||||
My notebook
|
<Text
|
||||||
</h3>
|
nowrap={true}
|
||||||
</div>
|
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`] = `
|
exports[`NotebookMetadataComponent renders un-liked notebook 1`] = `
|
||||||
<div
|
<Stack
|
||||||
className="notebookViewerMetadataContainer"
|
tokens={
|
||||||
|
Object {
|
||||||
|
"childrenGap": 10,
|
||||||
|
}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<h3
|
<Stack
|
||||||
className="title"
|
horizontal={true}
|
||||||
|
tokens={
|
||||||
|
Object {
|
||||||
|
"childrenGap": 30,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
verticalAlign="center"
|
||||||
>
|
>
|
||||||
My notebook
|
<Text
|
||||||
</h3>
|
nowrap={true}
|
||||||
</div>
|
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>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
@import "../../../../less/Common/Constants.less";
|
||||||
|
|
||||||
|
.radioSwitchComponent {
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
&>span:nth-child(n+2) {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.caption {
|
||||||
|
color: @BaseDark;
|
||||||
|
padding-left: @SmallSpace;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
/**
|
||||||
|
* Horizontal switch component
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import "./RadioSwitchComponent.less";
|
||||||
|
import { Icon } from "office-ui-fabric-react/lib/Icon";
|
||||||
|
import { NormalizedEventKey } from "../../../Common/Constants";
|
||||||
|
|
||||||
|
export interface Choice {
|
||||||
|
key: string;
|
||||||
|
onSelect: () => void;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RadioSwitchComponentProps {
|
||||||
|
choices: Choice[];
|
||||||
|
selectedKey: string;
|
||||||
|
onSelectionKeyChange?: (newValue: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RadioSwitchComponent extends React.Component<RadioSwitchComponentProps> {
|
||||||
|
public render(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div className="radioSwitchComponent">
|
||||||
|
{this.props.choices.map((choice: Choice) => (
|
||||||
|
<span
|
||||||
|
tabIndex={0}
|
||||||
|
key={choice.key}
|
||||||
|
onClick={() => this.onSelect(choice)}
|
||||||
|
onKeyPress={event => this.onKeyPress(event, choice)}
|
||||||
|
>
|
||||||
|
<Icon iconName={this.props.selectedKey === choice.key ? "RadioBtnOn" : "RadioBtnOff"} />
|
||||||
|
<span className="caption">{choice.label}</span>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onSelect(choice: Choice): void {
|
||||||
|
this.props.onSelectionKeyChange && this.props.onSelectionKeyChange(choice.key);
|
||||||
|
choice.onSelect();
|
||||||
|
}
|
||||||
|
|
||||||
|
private onKeyPress(event: React.KeyboardEvent<HTMLSpanElement>, choice: Choice): void {
|
||||||
|
if (event.key === NormalizedEventKey.Enter || event.key === NormalizedEventKey.Space) {
|
||||||
|
this.onSelect(choice);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
35
src/Explorer/Controls/SmartUi/InputUtils.ts
Normal file
35
src/Explorer/Controls/SmartUi/InputUtils.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
/* Utilities for validation */
|
||||||
|
|
||||||
|
export const onValidateValueChange = (newValue: string, minValue?: number, maxValue?: number): number => {
|
||||||
|
let numericValue = parseInt(newValue);
|
||||||
|
if (!isNaN(numericValue) && isFinite(numericValue)) {
|
||||||
|
if (minValue !== undefined && numericValue < minValue) {
|
||||||
|
numericValue = minValue;
|
||||||
|
}
|
||||||
|
if (maxValue !== undefined && numericValue > maxValue) {
|
||||||
|
numericValue = maxValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.floor(numericValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const onIncrementValue = (newValue: string, step: number, max?: number): number => {
|
||||||
|
const numericValue = parseInt(newValue);
|
||||||
|
if (!isNaN(numericValue) && isFinite(numericValue)) {
|
||||||
|
const newValue = numericValue + step;
|
||||||
|
return max !== undefined ? Math.min(max, newValue) : newValue;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const onDecrementValue = (newValue: string, step: number, min?: number): number => {
|
||||||
|
const numericValue = parseInt(newValue);
|
||||||
|
if (!isNaN(numericValue) && isFinite(numericValue)) {
|
||||||
|
const newValue = numericValue - step;
|
||||||
|
return min !== undefined ? Math.max(min, newValue) : newValue;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
14
src/Explorer/Controls/SmartUi/SmartUiComponent.less
Normal file
14
src/Explorer/Controls/SmartUi/SmartUiComponent.less
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
@import "../../../../less/Common/Constants.less";
|
||||||
|
|
||||||
|
.widgetRendererContainer {
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
.inputLabelContainer {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
|
||||||
|
.inputLabel {
|
||||||
|
color: #393939;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
88
src/Explorer/Controls/SmartUi/SmartUiComponent.test.tsx
Normal file
88
src/Explorer/Controls/SmartUi/SmartUiComponent.test.tsx
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { shallow } from "enzyme";
|
||||||
|
import { SmartUiComponent, Descriptor, InputType } from "./SmartUiComponent";
|
||||||
|
|
||||||
|
describe("SmartUiComponent", () => {
|
||||||
|
const exampleData: Descriptor = {
|
||||||
|
root: {
|
||||||
|
id: "root",
|
||||||
|
info: {
|
||||||
|
message: "Start at $24/mo per database",
|
||||||
|
link: {
|
||||||
|
href: "https://aka.ms/azure-cosmos-db-pricing",
|
||||||
|
text: "More Details"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: "throughput",
|
||||||
|
input: {
|
||||||
|
label: "Throughput (input)",
|
||||||
|
dataFieldName: "throughput",
|
||||||
|
type: "number",
|
||||||
|
min: 400,
|
||||||
|
max: 500,
|
||||||
|
step: 10,
|
||||||
|
defaultValue: 400,
|
||||||
|
inputType: "spin"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "throughput2",
|
||||||
|
input: {
|
||||||
|
label: "Throughput (Slider)",
|
||||||
|
dataFieldName: "throughput2",
|
||||||
|
type: "number",
|
||||||
|
min: 400,
|
||||||
|
max: 500,
|
||||||
|
step: 10,
|
||||||
|
defaultValue: 400,
|
||||||
|
inputType: "slider"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "containerId",
|
||||||
|
input: {
|
||||||
|
label: "Container id",
|
||||||
|
dataFieldName: "containerId",
|
||||||
|
type: "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "analyticalStore",
|
||||||
|
input: {
|
||||||
|
label: "Analytical Store",
|
||||||
|
trueLabel: "Enabled",
|
||||||
|
falseLabel: "Disabled",
|
||||||
|
defaultValue: true,
|
||||||
|
dataFieldName: "analyticalStore",
|
||||||
|
type: "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "database",
|
||||||
|
input: {
|
||||||
|
label: "Database",
|
||||||
|
dataFieldName: "database",
|
||||||
|
type: "enum",
|
||||||
|
choices: [
|
||||||
|
{ label: "Database 1", key: "db1", value: "database1" },
|
||||||
|
{ label: "Database 2", key: "db2", value: "database2" },
|
||||||
|
{ label: "Database 3", key: "db3", value: "database3" }
|
||||||
|
],
|
||||||
|
defaultKey: "db2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const exampleCallbacks = (newValues: Map<string, InputType>): void => {
|
||||||
|
console.log("New values:", newValues);
|
||||||
|
};
|
||||||
|
|
||||||
|
it("should render", () => {
|
||||||
|
const wrapper = shallow(<SmartUiComponent descriptor={exampleData} onChange={exampleCallbacks} />);
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
335
src/Explorer/Controls/SmartUi/SmartUiComponent.tsx
Normal file
335
src/Explorer/Controls/SmartUi/SmartUiComponent.tsx
Normal file
@@ -0,0 +1,335 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { Position } from "office-ui-fabric-react/lib/utilities/positioning";
|
||||||
|
import { Slider } from "office-ui-fabric-react/lib/Slider";
|
||||||
|
import { SpinButton } from "office-ui-fabric-react/lib/SpinButton";
|
||||||
|
import { Dropdown, IDropdownOption } from "office-ui-fabric-react/lib/Dropdown";
|
||||||
|
import { TextField } from "office-ui-fabric-react/lib/TextField";
|
||||||
|
import { Text } from "office-ui-fabric-react/lib/Text";
|
||||||
|
import { InputType } from "../../Tables/Constants";
|
||||||
|
import { RadioSwitchComponent } from "../RadioSwitchComponent/RadioSwitchComponent";
|
||||||
|
import { Stack, IStackTokens } from "office-ui-fabric-react/lib/Stack";
|
||||||
|
import { Link, MessageBar, MessageBarType } from "office-ui-fabric-react";
|
||||||
|
|
||||||
|
import * as InputUtils from "./InputUtils";
|
||||||
|
import "./SmartUiComponent.less";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic UX renderer
|
||||||
|
* It takes:
|
||||||
|
* - a JSON object as data
|
||||||
|
* - a Map of callbacks
|
||||||
|
* - a descriptor of the UX.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type InputTypeValue = "number" | "string" | "boolean" | "enum";
|
||||||
|
|
||||||
|
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||||
|
export type EnumItem = { label: string; key: string; value: any };
|
||||||
|
|
||||||
|
export type InputType = number | string | boolean | EnumItem;
|
||||||
|
|
||||||
|
interface BaseInput {
|
||||||
|
label: string;
|
||||||
|
dataFieldName: string;
|
||||||
|
type: InputTypeValue;
|
||||||
|
placeholder?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For now, this only supports integers
|
||||||
|
*/
|
||||||
|
export interface NumberInput extends BaseInput {
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
|
step: number;
|
||||||
|
defaultValue: number;
|
||||||
|
inputType: "spin" | "slider";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BooleanInput extends BaseInput {
|
||||||
|
trueLabel: string;
|
||||||
|
falseLabel: string;
|
||||||
|
defaultValue: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StringInput extends BaseInput {
|
||||||
|
defaultValue?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EnumInput extends BaseInput {
|
||||||
|
choices: EnumItem[];
|
||||||
|
defaultKey: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Info {
|
||||||
|
message: string;
|
||||||
|
link?: {
|
||||||
|
href: string;
|
||||||
|
text: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AnyInput = NumberInput | BooleanInput | StringInput | EnumInput;
|
||||||
|
|
||||||
|
export interface Node {
|
||||||
|
id: string;
|
||||||
|
info?: Info;
|
||||||
|
input?: AnyInput;
|
||||||
|
children?: Node[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Descriptor {
|
||||||
|
root: Node;
|
||||||
|
}
|
||||||
|
|
||||||
|
/************************** Component implementation starts here ************************************* */
|
||||||
|
|
||||||
|
export interface SmartUiComponentProps {
|
||||||
|
descriptor: Descriptor;
|
||||||
|
onChange: (newValues: Map<string, InputType>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SmartUiComponentState {
|
||||||
|
currentValues: Map<string, InputType>;
|
||||||
|
errors: Map<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SmartUiComponent extends React.Component<SmartUiComponentProps, SmartUiComponentState> {
|
||||||
|
private static readonly labelStyle = {
|
||||||
|
color: "#393939",
|
||||||
|
fontFamily: "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
|
||||||
|
fontSize: 12
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props: SmartUiComponentProps) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
currentValues: new Map(),
|
||||||
|
errors: new Map()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderInfo(info: Info): JSX.Element {
|
||||||
|
return (
|
||||||
|
<MessageBar>
|
||||||
|
{info.message}
|
||||||
|
<Link href={info.link.href} target="_blank">
|
||||||
|
{info.link.text}
|
||||||
|
</Link>
|
||||||
|
</MessageBar>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onInputChange = (newValue: string | number | boolean, dataFieldName: string) => {
|
||||||
|
const { currentValues } = this.state;
|
||||||
|
currentValues.set(dataFieldName, newValue);
|
||||||
|
this.setState({ currentValues }, () => this.props.onChange(this.state.currentValues));
|
||||||
|
};
|
||||||
|
|
||||||
|
private renderStringInput(input: StringInput): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div className="stringInputContainer">
|
||||||
|
<div>
|
||||||
|
<TextField
|
||||||
|
id={`${input.dataFieldName}-input`}
|
||||||
|
label={input.label}
|
||||||
|
type="text"
|
||||||
|
value={input.defaultValue}
|
||||||
|
placeholder={input.placeholder}
|
||||||
|
onChange={(_, newValue) => this.onInputChange(newValue, input.dataFieldName)}
|
||||||
|
styles={{
|
||||||
|
subComponentStyles: {
|
||||||
|
label: {
|
||||||
|
root: {
|
||||||
|
...SmartUiComponent.labelStyle,
|
||||||
|
fontWeight: 600
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private clearError(dataFieldName: string): void {
|
||||||
|
const { errors } = this.state;
|
||||||
|
errors.delete(dataFieldName);
|
||||||
|
this.setState({ errors });
|
||||||
|
}
|
||||||
|
|
||||||
|
private onValidate = (value: string, min: number, max: number, dataFieldName: string): string => {
|
||||||
|
const newValue = InputUtils.onValidateValueChange(value, min, max);
|
||||||
|
if (newValue) {
|
||||||
|
this.onInputChange(newValue, dataFieldName);
|
||||||
|
this.clearError(dataFieldName);
|
||||||
|
return newValue.toString();
|
||||||
|
} else {
|
||||||
|
const { errors } = this.state;
|
||||||
|
errors.set(dataFieldName, `Invalid value ${value}: must be between ${min} and ${max}`);
|
||||||
|
this.setState({ errors });
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
private onIncrement = (value: string, step: number, max: number, dataFieldName: string): string => {
|
||||||
|
const newValue = InputUtils.onIncrementValue(value, step, max);
|
||||||
|
if (newValue) {
|
||||||
|
this.onInputChange(newValue, dataFieldName);
|
||||||
|
this.clearError(dataFieldName);
|
||||||
|
return newValue.toString();
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
private onDecrement = (value: string, step: number, min: number, dataFieldName: string): string => {
|
||||||
|
const newValue = InputUtils.onDecrementValue(value, step, min);
|
||||||
|
if (newValue) {
|
||||||
|
this.onInputChange(newValue, dataFieldName);
|
||||||
|
this.clearError(dataFieldName);
|
||||||
|
return newValue.toString();
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
private renderNumberInput(input: NumberInput): JSX.Element {
|
||||||
|
const { label, min, max, defaultValue, dataFieldName, step } = input;
|
||||||
|
const props = { label, min, max, ariaLabel: label, step };
|
||||||
|
|
||||||
|
if (input.inputType === "spin") {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<SpinButton
|
||||||
|
{...props}
|
||||||
|
defaultValue={defaultValue.toString()}
|
||||||
|
onValidate={newValue => this.onValidate(newValue, min, max, dataFieldName)}
|
||||||
|
onIncrement={newValue => this.onIncrement(newValue, step, max, dataFieldName)}
|
||||||
|
onDecrement={newValue => this.onDecrement(newValue, step, min, dataFieldName)}
|
||||||
|
labelPosition={Position.top}
|
||||||
|
styles={{
|
||||||
|
label: {
|
||||||
|
...SmartUiComponent.labelStyle,
|
||||||
|
fontWeight: 600
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{this.state.errors.has(dataFieldName) && (
|
||||||
|
<MessageBar messageBarType={MessageBarType.error}>Error: {this.state.errors.get(dataFieldName)}</MessageBar>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (input.inputType === "slider") {
|
||||||
|
return (
|
||||||
|
<Slider
|
||||||
|
// showValue={true}
|
||||||
|
// valueFormat={}
|
||||||
|
{...props}
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
onChange={newValue => this.onInputChange(newValue, dataFieldName)}
|
||||||
|
styles={{
|
||||||
|
titleLabel: {
|
||||||
|
...SmartUiComponent.labelStyle,
|
||||||
|
fontWeight: 600
|
||||||
|
},
|
||||||
|
valueLabel: SmartUiComponent.labelStyle
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return <>Unsupported number input type {input.inputType}</>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderBooleanInput(input: BooleanInput): JSX.Element {
|
||||||
|
const { dataFieldName } = input;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="inputLabelContainer">
|
||||||
|
<Text variant="small" nowrap className="inputLabel">
|
||||||
|
{input.label}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<RadioSwitchComponent
|
||||||
|
choices={[
|
||||||
|
{
|
||||||
|
label: input.falseLabel,
|
||||||
|
key: "false",
|
||||||
|
onSelect: () => this.onInputChange(false, dataFieldName)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: input.trueLabel,
|
||||||
|
key: "true",
|
||||||
|
onSelect: () => this.onInputChange(true, dataFieldName)
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
selectedKey={
|
||||||
|
(this.state.currentValues.has(dataFieldName)
|
||||||
|
? (this.state.currentValues.get(dataFieldName) as boolean)
|
||||||
|
: input.defaultValue)
|
||||||
|
? "true"
|
||||||
|
: "false"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderEnumInput(input: EnumInput): JSX.Element {
|
||||||
|
const { label, defaultKey, dataFieldName, choices, placeholder } = input;
|
||||||
|
return (
|
||||||
|
<Dropdown
|
||||||
|
label={label}
|
||||||
|
selectedKey={
|
||||||
|
this.state.currentValues.has(dataFieldName)
|
||||||
|
? (this.state.currentValues.get(dataFieldName) as string)
|
||||||
|
: defaultKey
|
||||||
|
}
|
||||||
|
onChange={(_, item: IDropdownOption) => this.onInputChange(item.key.toString(), dataFieldName)}
|
||||||
|
placeholder={placeholder}
|
||||||
|
options={choices.map(c => ({
|
||||||
|
key: c.key,
|
||||||
|
text: c.value
|
||||||
|
}))}
|
||||||
|
styles={{
|
||||||
|
label: {
|
||||||
|
...SmartUiComponent.labelStyle,
|
||||||
|
fontWeight: 600
|
||||||
|
},
|
||||||
|
dropdown: SmartUiComponent.labelStyle
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderInput(input: AnyInput): JSX.Element {
|
||||||
|
switch (input.type) {
|
||||||
|
case "string":
|
||||||
|
return this.renderStringInput(input as StringInput);
|
||||||
|
case "number":
|
||||||
|
return this.renderNumberInput(input as NumberInput);
|
||||||
|
case "boolean":
|
||||||
|
return this.renderBooleanInput(input as BooleanInput);
|
||||||
|
case "enum":
|
||||||
|
return this.renderEnumInput(input as EnumInput);
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown input type: ${input.type}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderNode(node: Node): JSX.Element {
|
||||||
|
const containerStackTokens: IStackTokens = { childrenGap: 10 };
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack tokens={containerStackTokens} className="widgetRendererContainer">
|
||||||
|
{node.info && this.renderInfo(node.info)}
|
||||||
|
{node.input && this.renderInput(node.input)}
|
||||||
|
{node.children && node.children.map(child => <div key={child.id}>{this.renderNode(child)}</div>)}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(): JSX.Element {
|
||||||
|
return <>{this.renderNode(this.props.descriptor.root)}</>;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,240 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`SmartUiComponent should render 1`] = `
|
||||||
|
<Fragment>
|
||||||
|
<Stack
|
||||||
|
className="widgetRendererContainer"
|
||||||
|
tokens={
|
||||||
|
Object {
|
||||||
|
"childrenGap": 10,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<StyledMessageBarBase>
|
||||||
|
Start at $24/mo per database
|
||||||
|
<StyledLinkBase
|
||||||
|
href="https://aka.ms/azure-cosmos-db-pricing"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
More Details
|
||||||
|
</StyledLinkBase>
|
||||||
|
</StyledMessageBarBase>
|
||||||
|
<div
|
||||||
|
key="throughput"
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
className="widgetRendererContainer"
|
||||||
|
tokens={
|
||||||
|
Object {
|
||||||
|
"childrenGap": 10,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<CustomizedSpinButton
|
||||||
|
ariaLabel="Throughput (input)"
|
||||||
|
decrementButtonIcon={
|
||||||
|
Object {
|
||||||
|
"iconName": "ChevronDownSmall",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defaultValue="400"
|
||||||
|
disabled={false}
|
||||||
|
incrementButtonIcon={
|
||||||
|
Object {
|
||||||
|
"iconName": "ChevronUpSmall",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
label="Throughput (input)"
|
||||||
|
labelPosition={0}
|
||||||
|
max={500}
|
||||||
|
min={400}
|
||||||
|
onDecrement={[Function]}
|
||||||
|
onIncrement={[Function]}
|
||||||
|
onValidate={[Function]}
|
||||||
|
step={10}
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"label": Object {
|
||||||
|
"color": "#393939",
|
||||||
|
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
|
||||||
|
"fontSize": 12,
|
||||||
|
"fontWeight": 600,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
key="throughput2"
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
className="widgetRendererContainer"
|
||||||
|
tokens={
|
||||||
|
Object {
|
||||||
|
"childrenGap": 10,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<StyledSliderBase
|
||||||
|
ariaLabel="Throughput (Slider)"
|
||||||
|
defaultValue={400}
|
||||||
|
label="Throughput (Slider)"
|
||||||
|
max={500}
|
||||||
|
min={400}
|
||||||
|
onChange={[Function]}
|
||||||
|
step={10}
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"titleLabel": Object {
|
||||||
|
"color": "#393939",
|
||||||
|
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
|
||||||
|
"fontSize": 12,
|
||||||
|
"fontWeight": 600,
|
||||||
|
},
|
||||||
|
"valueLabel": Object {
|
||||||
|
"color": "#393939",
|
||||||
|
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
|
||||||
|
"fontSize": 12,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
key="containerId"
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
className="widgetRendererContainer"
|
||||||
|
tokens={
|
||||||
|
Object {
|
||||||
|
"childrenGap": 10,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="stringInputContainer"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<StyledTextFieldBase
|
||||||
|
id="containerId-input"
|
||||||
|
label="Container id"
|
||||||
|
onChange={[Function]}
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"subComponentStyles": Object {
|
||||||
|
"label": Object {
|
||||||
|
"root": Object {
|
||||||
|
"color": "#393939",
|
||||||
|
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
|
||||||
|
"fontSize": 12,
|
||||||
|
"fontWeight": 600,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
key="analyticalStore"
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
className="widgetRendererContainer"
|
||||||
|
tokens={
|
||||||
|
Object {
|
||||||
|
"childrenGap": 10,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
className="inputLabelContainer"
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
className="inputLabel"
|
||||||
|
nowrap={true}
|
||||||
|
variant="small"
|
||||||
|
>
|
||||||
|
Analytical Store
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<RadioSwitchComponent
|
||||||
|
choices={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"key": "false",
|
||||||
|
"label": "Disabled",
|
||||||
|
"onSelect": [Function],
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"key": "true",
|
||||||
|
"label": "Enabled",
|
||||||
|
"onSelect": [Function],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
selectedKey="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
key="database"
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
className="widgetRendererContainer"
|
||||||
|
tokens={
|
||||||
|
Object {
|
||||||
|
"childrenGap": 10,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<StyledWithResponsiveMode
|
||||||
|
label="Database"
|
||||||
|
onChange={[Function]}
|
||||||
|
options={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"key": "db1",
|
||||||
|
"text": "database1",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"key": "db2",
|
||||||
|
"text": "database2",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"key": "db3",
|
||||||
|
"text": "database3",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
selectedKey="db2"
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"dropdown": Object {
|
||||||
|
"color": "#393939",
|
||||||
|
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
|
||||||
|
"fontSize": 12,
|
||||||
|
},
|
||||||
|
"label": Object {
|
||||||
|
"color": "#393939",
|
||||||
|
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
|
||||||
|
"fontSize": 12,
|
||||||
|
"fontWeight": 600,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
</Fragment>
|
||||||
|
`;
|
||||||
@@ -6,10 +6,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as ReactDOM from "react-dom";
|
|
||||||
import * as Constants from "../../../Common/Constants";
|
import * as Constants from "../../../Common/Constants";
|
||||||
import AnimateHeight from "react-animate-height";
|
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 {
|
import {
|
||||||
DirectionalHint,
|
DirectionalHint,
|
||||||
IContextualMenuItemProps,
|
IContextualMenuItemProps,
|
||||||
@@ -227,6 +226,10 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
|
|||||||
|
|
||||||
private renderContextMenuButton(node: TreeNode): JSX.Element {
|
private renderContextMenuButton(node: TreeNode): JSX.Element {
|
||||||
const menuItemLabel = "More";
|
const menuItemLabel = "More";
|
||||||
|
const buttonStyles: Partial<IButtonStyles> = {
|
||||||
|
rootFocused: { outline: `1px dashed ${Constants.StyleConstants.FocusColor}` }
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={this.contextMenuRef} onContextMenu={this.onRightClick} onKeyPress={this.onMoreButtonKeyPress}>
|
<div ref={this.contextMenuRef} onContextMenu={this.onRightClick} onKeyPress={this.onMoreButtonKeyPress}>
|
||||||
<IconButton
|
<IconButton
|
||||||
@@ -264,6 +267,7 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
|
|||||||
onRenderIcon: (props: any) => <img src={menuItem.iconSrc} alt="" />
|
onRenderIcon: (props: any) => <img src={menuItem.iconSrc} alt="" />
|
||||||
}))
|
}))
|
||||||
}}
|
}}
|
||||||
|
styles={buttonStyles}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -191,6 +191,13 @@ exports[`TreeNodeComponent renders a simple node (sorted children, expanded) 1`]
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
name="More"
|
name="More"
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"rootFocused": Object {
|
||||||
|
"outline": "1px dashed undefined",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -314,6 +321,13 @@ exports[`TreeNodeComponent renders sorted children, expanded, leaves and parents
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
name="More"
|
name="More"
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"rootFocused": Object {
|
||||||
|
"outline": "1px dashed undefined",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -49,19 +49,15 @@ import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane";
|
|||||||
import { ExplorerMetrics } from "../Common/Constants";
|
import { ExplorerMetrics } from "../Common/Constants";
|
||||||
import { ExplorerSettings } from "../Shared/ExplorerSettings";
|
import { ExplorerSettings } from "../Shared/ExplorerSettings";
|
||||||
import { FileSystemUtil } from "./Notebook/FileSystemUtil";
|
import { FileSystemUtil } from "./Notebook/FileSystemUtil";
|
||||||
import { GitHubOAuthService } from "../GitHub/GitHubOAuthService";
|
|
||||||
import { GitHubReposPane } from "./Panes/GitHubReposPane";
|
|
||||||
import { handleOpenAction } from "./OpenActions";
|
import { handleOpenAction } from "./OpenActions";
|
||||||
import { IContentProvider } from "@nteract/core";
|
|
||||||
import { isInvalidParentFrameOrigin } from "../Utils/MessageValidation";
|
import { isInvalidParentFrameOrigin } from "../Utils/MessageValidation";
|
||||||
import { JunoClient } from "../Juno/JunoClient";
|
import { IGalleryItem } from "../Juno/JunoClient";
|
||||||
import { LibraryManagePane } from "./Panes/LibraryManagePane";
|
import { LibraryManagePane } from "./Panes/LibraryManagePane";
|
||||||
import { LoadQueryPane } from "./Panes/LoadQueryPane";
|
import { LoadQueryPane } from "./Panes/LoadQueryPane";
|
||||||
import { Logger } from "../Common/Logger";
|
import * as Logger from "../Common/Logger";
|
||||||
import { ManageSparkClusterPane } from "./Panes/ManageSparkClusterPane";
|
import { ManageSparkClusterPane } from "./Panes/ManageSparkClusterPane";
|
||||||
import { MessageHandler } from "../Common/MessageHandler";
|
import { MessageHandler } from "../Common/MessageHandler";
|
||||||
import { NotebookContentItem, NotebookContentItemType } from "./Notebook/NotebookContentItem";
|
import { NotebookContentItem, NotebookContentItemType } from "./Notebook/NotebookContentItem";
|
||||||
import { NotebookContentProvider } from "./Notebook/NotebookComponent/NotebookContentProvider";
|
|
||||||
import { NotebookUtil } from "./Notebook/NotebookUtil";
|
import { NotebookUtil } from "./Notebook/NotebookUtil";
|
||||||
import { NotebookWorkspaceManager } from "../NotebookWorkspaceManager/NotebookWorkspaceManager";
|
import { NotebookWorkspaceManager } from "../NotebookWorkspaceManager/NotebookWorkspaceManager";
|
||||||
import { NotificationConsoleComponentAdapter } from "./Menus/NotificationConsole/NotificationConsoleComponentAdapter";
|
import { NotificationConsoleComponentAdapter } from "./Menus/NotificationConsole/NotificationConsoleComponentAdapter";
|
||||||
@@ -86,6 +82,8 @@ import { TableColumnOptionsPane } from "./Panes/Tables/TableColumnOptionsPane";
|
|||||||
import { UploadFilePane } from "./Panes/UploadFilePane";
|
import { UploadFilePane } from "./Panes/UploadFilePane";
|
||||||
import { UploadItemsPane } from "./Panes/UploadItemsPane";
|
import { UploadItemsPane } from "./Panes/UploadItemsPane";
|
||||||
import { UploadItemsPaneAdapter } from "./Panes/UploadItemsPaneAdapter";
|
import { UploadItemsPaneAdapter } from "./Panes/UploadItemsPaneAdapter";
|
||||||
|
import { ReactAdapter } from "../Bindings/ReactBindingHandler";
|
||||||
|
import { toRawContentUri, fromContentUri } from "../Utils/GitHubUtils";
|
||||||
|
|
||||||
BindingHandlersRegisterer.registerBindingHandlers();
|
BindingHandlersRegisterer.registerBindingHandlers();
|
||||||
// Hold a reference to ComponentRegisterer to prevent transpiler to ignore import
|
// Hold a reference to ComponentRegisterer to prevent transpiler to ignore import
|
||||||
@@ -199,10 +197,12 @@ export default class Explorer implements ViewModels.Explorer {
|
|||||||
public libraryManagePane: ViewModels.ContextualPane;
|
public libraryManagePane: ViewModels.ContextualPane;
|
||||||
public clusterLibraryPane: ViewModels.ContextualPane;
|
public clusterLibraryPane: ViewModels.ContextualPane;
|
||||||
public gitHubReposPane: ViewModels.ContextualPane;
|
public gitHubReposPane: ViewModels.ContextualPane;
|
||||||
|
public publishNotebookPaneAdapter: ReactAdapter;
|
||||||
|
|
||||||
// features
|
// features
|
||||||
public isGalleryEnabled: ko.Computed<boolean>;
|
public isGalleryPublishEnabled: ko.Computed<boolean>;
|
||||||
public isGitHubPaneEnabled: ko.Observable<boolean>;
|
public isGitHubPaneEnabled: ko.Observable<boolean>;
|
||||||
|
public isPublishNotebookPaneEnabled: ko.Observable<boolean>;
|
||||||
public isHostedDataExplorerEnabled: ko.Computed<boolean>;
|
public isHostedDataExplorerEnabled: ko.Computed<boolean>;
|
||||||
public isRightPanelV2Enabled: ko.Computed<boolean>;
|
public isRightPanelV2Enabled: ko.Computed<boolean>;
|
||||||
public canExceedMaximumValue: ko.Computed<boolean>;
|
public canExceedMaximumValue: ko.Computed<boolean>;
|
||||||
@@ -223,11 +223,7 @@ export default class Explorer implements ViewModels.Explorer {
|
|||||||
// Notebooks
|
// Notebooks
|
||||||
public isNotebookEnabled: ko.Observable<boolean>;
|
public isNotebookEnabled: ko.Observable<boolean>;
|
||||||
public isNotebooksEnabledForAccount: ko.Observable<boolean>;
|
public isNotebooksEnabledForAccount: ko.Observable<boolean>;
|
||||||
private notebookClient: ViewModels.INotebookContainerClient;
|
|
||||||
private notebookContentClient: ViewModels.INotebookContentClient;
|
|
||||||
public notebookServerInfo: ko.Observable<DataModels.NotebookWorkspaceConnectionInfo>;
|
public notebookServerInfo: ko.Observable<DataModels.NotebookWorkspaceConnectionInfo>;
|
||||||
public notebookContentProvider: IContentProvider;
|
|
||||||
public gitHubOAuthService: GitHubOAuthService;
|
|
||||||
public notebookWorkspaceManager: ViewModels.NotebookWorkspaceManager;
|
public notebookWorkspaceManager: ViewModels.NotebookWorkspaceManager;
|
||||||
public sparkClusterManager: ViewModels.SparkClusterManager;
|
public sparkClusterManager: ViewModels.SparkClusterManager;
|
||||||
public sparkClusterConnectionInfo: ko.Observable<DataModels.SparkClusterConnectionInfo>;
|
public sparkClusterConnectionInfo: ko.Observable<DataModels.SparkClusterConnectionInfo>;
|
||||||
@@ -239,6 +235,7 @@ export default class Explorer implements ViewModels.Explorer {
|
|||||||
public isSynapseLinkUpdating: ko.Observable<boolean>;
|
public isSynapseLinkUpdating: ko.Observable<boolean>;
|
||||||
public isNotebookTabActive: ko.Computed<boolean>;
|
public isNotebookTabActive: ko.Computed<boolean>;
|
||||||
public memoryUsageInfo: ko.Observable<DataModels.MemoryUsageInfo>;
|
public memoryUsageInfo: ko.Observable<DataModels.MemoryUsageInfo>;
|
||||||
|
public notebookManager?: any; // This is dynamically loaded
|
||||||
|
|
||||||
private _panes: ViewModels.ContextualPane[] = [];
|
private _panes: ViewModels.ContextualPane[] = [];
|
||||||
private _importExplorerConfigComplete: boolean = false;
|
private _importExplorerConfigComplete: boolean = false;
|
||||||
@@ -247,7 +244,10 @@ export default class Explorer implements ViewModels.Explorer {
|
|||||||
private _isInitializingSparkConnectionInfo: boolean;
|
private _isInitializingSparkConnectionInfo: boolean;
|
||||||
private notebookBasePath: ko.Observable<string>;
|
private notebookBasePath: ko.Observable<string>;
|
||||||
private _arcadiaManager: ViewModels.ArcadiaResourceManager;
|
private _arcadiaManager: ViewModels.ArcadiaResourceManager;
|
||||||
private _filePathToImportAndOpen: string;
|
private notebookToImport: {
|
||||||
|
name: string;
|
||||||
|
content: string;
|
||||||
|
};
|
||||||
|
|
||||||
// React adapters
|
// React adapters
|
||||||
private commandBarComponentAdapter: CommandBarComponentAdapter;
|
private commandBarComponentAdapter: CommandBarComponentAdapter;
|
||||||
@@ -347,7 +347,7 @@ export default class Explorer implements ViewModels.Explorer {
|
|||||||
await this.initNotebooks(this.databaseAccount());
|
await this.initNotebooks(this.databaseAccount());
|
||||||
const workspaces = await this._getArcadiaWorkspaces();
|
const workspaces = await this._getArcadiaWorkspaces();
|
||||||
this.arcadiaWorkspaces(workspaces);
|
this.arcadiaWorkspaces(workspaces);
|
||||||
} else if (this._filePathToImportAndOpen) {
|
} else if (this.notebookToImport) {
|
||||||
// if notebooks is not enabled but the user is trying to do a quickstart setup with notebooks, open the SetupNotebooksPane
|
// if notebooks is not enabled but the user is trying to do a quickstart setup with notebooks, open the SetupNotebooksPane
|
||||||
this._openSetupNotebooksPaneForQuickstart();
|
this._openSetupNotebooksPaneForQuickstart();
|
||||||
}
|
}
|
||||||
@@ -408,8 +408,11 @@ export default class Explorer implements ViewModels.Explorer {
|
|||||||
this.shouldShowShareDialogContents = ko.observable<boolean>(false);
|
this.shouldShowShareDialogContents = ko.observable<boolean>(false);
|
||||||
this.shouldShowDataAccessExpiryDialog = ko.observable<boolean>(false);
|
this.shouldShowDataAccessExpiryDialog = ko.observable<boolean>(false);
|
||||||
this.shouldShowContextSwitchPrompt = 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.isGitHubPaneEnabled = ko.observable<boolean>(false);
|
||||||
|
this.isPublishNotebookPaneEnabled = ko.observable<boolean>(false);
|
||||||
|
|
||||||
this.canExceedMaximumValue = ko.computed<boolean>(() =>
|
this.canExceedMaximumValue = ko.computed<boolean>(() =>
|
||||||
this.isFeatureEnabled(Constants.Features.canExceedMaximumValue)
|
this.isFeatureEnabled(Constants.Features.canExceedMaximumValue)
|
||||||
@@ -937,127 +940,33 @@ export default class Explorer implements ViewModels.Explorer {
|
|||||||
startKey
|
startKey
|
||||||
);
|
);
|
||||||
|
|
||||||
const junoClient = new JunoClient(this.databaseAccount);
|
|
||||||
|
|
||||||
this.isNotebookEnabled = ko.observable(false);
|
this.isNotebookEnabled = ko.observable(false);
|
||||||
this.isNotebookEnabled.subscribe(async (isEnabled: boolean) => {
|
this.isNotebookEnabled.subscribe(async () => {
|
||||||
this.refreshCommandBarButtons();
|
if (!this.notebookManager) {
|
||||||
|
const notebookManagerModule = await import(
|
||||||
|
/* webpackChunkName: "NotebookManager" */ "./Notebook/NotebookManager"
|
||||||
|
);
|
||||||
|
this.notebookManager = new notebookManagerModule.default();
|
||||||
|
this.notebookManager.initialize({
|
||||||
|
container: this,
|
||||||
|
dialogProps: this._dialogProps,
|
||||||
|
notebookBasePath: this.notebookBasePath,
|
||||||
|
resourceTree: this.resourceTree,
|
||||||
|
refreshCommandBarButtons: () => this.refreshCommandBarButtons(),
|
||||||
|
refreshNotebookList: () => this.refreshNotebookList()
|
||||||
|
});
|
||||||
|
|
||||||
this.gitHubOAuthService = new GitHubOAuthService(junoClient);
|
this.gitHubReposPane = this.notebookManager.gitHubReposPane;
|
||||||
|
this.isGitHubPaneEnabled(true);
|
||||||
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.gitHubReposPane = new GitHubReposPane({
|
|
||||||
documentClientUtility: this.documentClientUtility,
|
|
||||||
id: "gitHubReposPane",
|
|
||||||
visible: ko.observable<boolean>(false),
|
|
||||||
container: this,
|
|
||||||
junoClient,
|
|
||||||
gitHubClient
|
|
||||||
});
|
|
||||||
|
|
||||||
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) => {
|
this.refreshCommandBarButtons();
|
||||||
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.refreshNotebookList();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.isSparkEnabled = ko.observable(false);
|
this.isSparkEnabled = ko.observable(false);
|
||||||
this.isSparkEnabled.subscribe((isEnabled: boolean) => this.refreshCommandBarButtons());
|
this.isSparkEnabled.subscribe((isEnabled: boolean) => this.refreshCommandBarButtons());
|
||||||
this.resourceTree = new ResourceTreeAdapter(this, junoClient);
|
this.resourceTree = new ResourceTreeAdapter(this);
|
||||||
this.resourceTreeForResourceToken = new ResourceTreeAdapterForResourceToken(this);
|
this.resourceTreeForResourceToken = new ResourceTreeAdapterForResourceToken(this);
|
||||||
this.notebookServerInfo = ko.observable<DataModels.NotebookWorkspaceConnectionInfo>({
|
this.notebookServerInfo = ko.observable<DataModels.NotebookWorkspaceConnectionInfo>({
|
||||||
notebookServerEndpoint: undefined,
|
notebookServerEndpoint: undefined,
|
||||||
@@ -1887,7 +1796,7 @@ export default class Explorer implements ViewModels.Explorer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public resetNotebookWorkspace() {
|
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";
|
const error = "Attempt to reset notebook workspace, but notebook is not enabled";
|
||||||
Logger.logError(error, "Explorer/resetNotebookWorkspace");
|
Logger.logError(error, "Explorer/resetNotebookWorkspace");
|
||||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error);
|
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error);
|
||||||
@@ -1994,7 +1903,7 @@ export default class Explorer implements ViewModels.Explorer {
|
|||||||
this._closeModalDialog();
|
this._closeModalDialog();
|
||||||
const id = NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.InProgress, "Resetting notebook workspace");
|
const id = NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.InProgress, "Resetting notebook workspace");
|
||||||
try {
|
try {
|
||||||
await this.notebookClient.resetWorkspace();
|
await this.notebookManager?.notebookClient.resetWorkspace();
|
||||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, "Successfully reset notebook workspace");
|
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, "Successfully reset notebook workspace");
|
||||||
TelemetryProcessor.traceSuccess(Action.ResetNotebookWorkspace);
|
TelemetryProcessor.traceSuccess(Action.ResetNotebookWorkspace);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -2563,17 +2472,17 @@ export default class Explorer implements ViewModels.Explorer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private uploadFile(name: string, content: string, parent: NotebookContentItem): Promise<NotebookContentItem> {
|
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";
|
const error = "Attempt to upload notebook, but notebook is not enabled";
|
||||||
Logger.logError(error, "Explorer/uploadFile");
|
Logger.logError(error, "Explorer/uploadFile");
|
||||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error);
|
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error);
|
||||||
throw new Error(error);
|
throw new Error(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
const promise = this.notebookContentClient.uploadFileAsync(name, content, parent);
|
const promise = this.notebookManager?.notebookContentClient.uploadFileAsync(name, content, parent);
|
||||||
promise
|
promise
|
||||||
.then(() => this.resourceTree.triggerRender())
|
.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;
|
return promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2582,11 +2491,7 @@ export default class Explorer implements ViewModels.Explorer {
|
|||||||
const item = NotebookUtil.createNotebookContentItem(name, path, "file");
|
const item = NotebookUtil.createNotebookContentItem(name, path, "file");
|
||||||
const parent = this.resourceTree.myNotebooksContentRoot;
|
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
|
|
||||||
}
|
|
||||||
|
|
||||||
const existingItem = _.find(parent.children, node => node.name === name);
|
const existingItem = _.find(parent.children, node => node.name === name);
|
||||||
if (existingItem) {
|
if (existingItem) {
|
||||||
return this.openNotebook(existingItem);
|
return this.openNotebook(existingItem);
|
||||||
@@ -2597,33 +2502,38 @@ export default class Explorer implements ViewModels.Explorer {
|
|||||||
return this.openNotebook(uploadedItem);
|
return this.openNotebook(uploadedItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
this._filePathToImportAndOpen = path; // we'll try opening this path later on
|
|
||||||
return Promise.resolve(false);
|
return Promise.resolve(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async importAndOpenFromGallery(path: string, newName: string, content: any): Promise<boolean> {
|
public async importAndOpenContent(name: string, content: string): Promise<boolean> {
|
||||||
const name = newName;
|
|
||||||
const parent = this.resourceTree.myNotebooksContentRoot;
|
const parent = this.resourceTree.myNotebooksContentRoot;
|
||||||
|
|
||||||
if (parent && this.isNotebookEnabled() && this.notebookClient) {
|
if (parent && parent.children && this.isNotebookEnabled() && this.notebookManager?.notebookClient) {
|
||||||
if (this._filePathToImportAndOpen === path) {
|
if (this.notebookToImport && this.notebookToImport.name === name && this.notebookToImport.content === content) {
|
||||||
this._filePathToImportAndOpen = undefined; // we don't want to try opening this path again
|
this.notebookToImport = undefined; // we don't want to try opening this notebook again
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingItem = _.find(parent.children, node => node.name === name);
|
const existingItem = _.find(parent.children, node => node.name === name);
|
||||||
if (existingItem) {
|
if (existingItem) {
|
||||||
this.showOkModalDialog("Download failed", "Notebook with the same name already exists.");
|
return this.openNotebook(existingItem);
|
||||||
return Promise.reject(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const uploadedItem = await this.uploadFile(name, content, parent);
|
const uploadedItem = await this.uploadFile(name, content, parent);
|
||||||
return this.openNotebook(uploadedItem);
|
return this.openNotebook(uploadedItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
this._filePathToImportAndOpen = path; // we'll try opening this path later on
|
this.notebookToImport = { name, content }; // we'll try opening this notebook later on
|
||||||
return Promise.resolve(false);
|
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 {
|
public showOkModalDialog(title: string, msg: string): void {
|
||||||
this._dialogProps({
|
this._dialogProps({
|
||||||
isModal: true,
|
isModal: true,
|
||||||
@@ -2756,7 +2666,7 @@ export default class Explorer implements ViewModels.Explorer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public renameNotebook(notebookFile: NotebookContentItem): Q.Promise<NotebookContentItem> {
|
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";
|
const error = "Attempt to rename notebook, but notebook is not enabled";
|
||||||
Logger.logError(error, "Explorer/renameNotebook");
|
Logger.logError(error, "Explorer/renameNotebook");
|
||||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error);
|
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error);
|
||||||
@@ -2784,7 +2694,7 @@ export default class Explorer implements ViewModels.Explorer {
|
|||||||
paneTitle: "Rename Notebook",
|
paneTitle: "Rename Notebook",
|
||||||
submitButtonLabel: "Rename",
|
submitButtonLabel: "Rename",
|
||||||
defaultInput: FileSystemUtil.stripExtension(notebookFile.name, "ipynb"),
|
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 => {
|
.then(newNotebookFile => {
|
||||||
this.openedTabs()
|
this.openedTabs()
|
||||||
@@ -2806,7 +2716,7 @@ export default class Explorer implements ViewModels.Explorer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public onCreateDirectory(parent: NotebookContentItem): Q.Promise<NotebookContentItem> {
|
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";
|
const error = "Attempt to create notebook directory, but notebook is not enabled";
|
||||||
Logger.logError(error, "Explorer/onCreateDirectory");
|
Logger.logError(error, "Explorer/onCreateDirectory");
|
||||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error);
|
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error);
|
||||||
@@ -2821,32 +2731,32 @@ export default class Explorer implements ViewModels.Explorer {
|
|||||||
paneTitle: "Create new directory",
|
paneTitle: "Create new directory",
|
||||||
submitButtonLabel: "Create",
|
submitButtonLabel: "Create",
|
||||||
defaultInput: "",
|
defaultInput: "",
|
||||||
onSubmit: (input: string) => this.notebookContentClient.createDirectory(parent, input)
|
onSubmit: (input: string) => this.notebookManager?.notebookContentClient.createDirectory(parent, input)
|
||||||
});
|
});
|
||||||
result.then(() => this.resourceTree.triggerRender());
|
result.then(() => this.resourceTree.triggerRender());
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public readFile(notebookFile: NotebookContentItem): Promise<string> {
|
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";
|
const error = "Attempt to read file, but notebook is not enabled";
|
||||||
Logger.logError(error, "Explorer/downloadFile");
|
Logger.logError(error, "Explorer/downloadFile");
|
||||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error);
|
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error);
|
||||||
throw new 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> {
|
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";
|
const error = "Attempt to download file, but notebook is not enabled";
|
||||||
Logger.logError(error, "Explorer/downloadFile");
|
Logger.logError(error, "Explorer/downloadFile");
|
||||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error);
|
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error);
|
||||||
throw new Error(error);
|
throw new Error(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.notebookContentClient.readFileContent(notebookFile.path).then(
|
return this.notebookManager?.notebookContentClient.readFileContent(notebookFile.path).then(
|
||||||
(content: string) => {
|
(content: string) => {
|
||||||
const blob = new Blob([content], { type: "octet/stream" });
|
const blob = new Blob([content], { type: "octet/stream" });
|
||||||
if (navigator.msSaveBlob) {
|
if (navigator.msSaveBlob) {
|
||||||
@@ -2866,7 +2776,7 @@ export default class Explorer implements ViewModels.Explorer {
|
|||||||
downloadLink.remove();
|
downloadLink.remove();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error => {
|
(error: any) => {
|
||||||
NotificationConsoleUtils.logConsoleMessage(
|
NotificationConsoleUtils.logConsoleMessage(
|
||||||
ConsoleDataType.Error,
|
ConsoleDataType.Error,
|
||||||
`Could not download notebook ${JSON.stringify(error)}`
|
`Could not download notebook ${JSON.stringify(error)}`
|
||||||
@@ -3013,18 +2923,18 @@ export default class Explorer implements ViewModels.Explorer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private refreshNotebookList = async (): Promise<void> => {
|
private refreshNotebookList = async (): Promise<void> => {
|
||||||
if (!this.isNotebookEnabled() || !this.notebookContentClient) {
|
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.resourceTree.initialize();
|
await this.resourceTree.initialize();
|
||||||
if (this._filePathToImportAndOpen) {
|
if (this.notebookToImport) {
|
||||||
this.importAndOpen(this._filePathToImportAndOpen);
|
this.importAndOpenContent(this.notebookToImport.name, this.notebookToImport.content);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public deleteNotebookFile(item: NotebookContentItem): Promise<void> {
|
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";
|
const error = "Attempt to delete notebook file, but notebook is not enabled";
|
||||||
Logger.logError(error, "Explorer/deleteNotebookFile");
|
Logger.logError(error, "Explorer/deleteNotebookFile");
|
||||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error);
|
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error);
|
||||||
@@ -3055,11 +2965,11 @@ export default class Explorer implements ViewModels.Explorer {
|
|||||||
return Promise.reject();
|
return Promise.reject();
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.notebookContentClient.deleteContentItem(item).then(
|
return this.notebookManager?.notebookContentClient.deleteContentItem(item).then(
|
||||||
() => {
|
() => {
|
||||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, `Successfully deleted: ${item.path}`);
|
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, `Successfully deleted: ${item.path}`);
|
||||||
},
|
},
|
||||||
reason => {
|
(reason: any) => {
|
||||||
NotificationConsoleUtils.logConsoleMessage(
|
NotificationConsoleUtils.logConsoleMessage(
|
||||||
ConsoleDataType.Error,
|
ConsoleDataType.Error,
|
||||||
`Failed to delete "${item.path}": ${JSON.stringify(reason)}`
|
`Failed to delete "${item.path}": ${JSON.stringify(reason)}`
|
||||||
@@ -3072,7 +2982,7 @@ export default class Explorer implements ViewModels.Explorer {
|
|||||||
* This creates a new notebook file, then opens the notebook
|
* This creates a new notebook file, then opens the notebook
|
||||||
*/
|
*/
|
||||||
public onNewNotebookClicked(parent?: NotebookContentItem): void {
|
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";
|
const error = "Attempt to create new notebook, but notebook is not enabled";
|
||||||
Logger.logError(error, "Explorer/onNewNotebookClicked");
|
Logger.logError(error, "Explorer/onNewNotebookClicked");
|
||||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error);
|
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error);
|
||||||
@@ -3092,7 +3002,7 @@ export default class Explorer implements ViewModels.Explorer {
|
|||||||
dataExplorerArea: Constants.Areas.Notebook
|
dataExplorerArea: Constants.Areas.Notebook
|
||||||
});
|
});
|
||||||
|
|
||||||
this.notebookContentClient
|
this.notebookManager?.notebookContentClient
|
||||||
.createNewNotebookFile(parent)
|
.createNewNotebookFile(parent)
|
||||||
.then((newFile: NotebookContentItem) => {
|
.then((newFile: NotebookContentItem) => {
|
||||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, `Successfully created: ${newFile.name}`);
|
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, `Successfully created: ${newFile.name}`);
|
||||||
@@ -3108,7 +3018,7 @@ export default class Explorer implements ViewModels.Explorer {
|
|||||||
return this.openNotebook(newFile);
|
return this.openNotebook(newFile);
|
||||||
})
|
})
|
||||||
.then(() => this.resourceTree.triggerRender())
|
.then(() => this.resourceTree.triggerRender())
|
||||||
.catch(reason => {
|
.catch((reason: any) => {
|
||||||
const error = `Failed to create a new notebook: ${reason}`;
|
const error = `Failed to create a new notebook: ${reason}`;
|
||||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error);
|
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error);
|
||||||
TelemetryProcessor.traceFailure(
|
TelemetryProcessor.traceFailure(
|
||||||
@@ -3158,14 +3068,14 @@ export default class Explorer implements ViewModels.Explorer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public refreshContentItem(item: NotebookContentItem): Promise<void> {
|
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";
|
const error = "Attempt to refresh notebook list, but notebook is not enabled";
|
||||||
Logger.logError(error, "Explorer/refreshContentItem");
|
Logger.logError(error, "Explorer/refreshContentItem");
|
||||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error);
|
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error);
|
||||||
return Promise.reject(new Error(error));
|
return Promise.reject(new Error(error));
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.notebookContentClient.updateItemChildren(item);
|
return this.notebookManager?.notebookContentClient.updateItemChildren(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getNotebookBasePath(): string {
|
public getNotebookBasePath(): string {
|
||||||
@@ -3234,7 +3144,7 @@ export default class Explorer implements ViewModels.Explorer {
|
|||||||
newTab.onTabClick();
|
newTab.onTabClick();
|
||||||
}
|
}
|
||||||
|
|
||||||
public openGallery() {
|
public async openGallery(notebookUrl?: string, galleryItem?: IGalleryItem, isFavorite?: boolean) {
|
||||||
let title: string;
|
let title: string;
|
||||||
let hashLocation: string;
|
let hashLocation: string;
|
||||||
|
|
||||||
@@ -3249,25 +3159,34 @@ export default class Explorer implements ViewModels.Explorer {
|
|||||||
if (openedTabs[i].hashLocation() == hashLocation) {
|
if (openedTabs[i].hashLocation() == hashLocation) {
|
||||||
openedTabs[i].onTabClick();
|
openedTabs[i].onTabClick();
|
||||||
openedTabs[i].onActivate();
|
openedTabs[i].onActivate();
|
||||||
|
(openedTabs[i] as any).updateGalleryParams(notebookUrl, galleryItem, isFavorite);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!this.galleryTab) {
|
||||||
|
this.galleryTab = await import(/* webpackChunkName: "GalleryTab" */ "./Tabs/GalleryTab");
|
||||||
|
}
|
||||||
|
|
||||||
const newTab = new this.galleryTab.default({
|
const newTab = new this.galleryTab.default({
|
||||||
|
// GalleryTabOptions
|
||||||
account: CosmosClient.databaseAccount(),
|
account: CosmosClient.databaseAccount(),
|
||||||
|
container: this,
|
||||||
|
junoClient: this.notebookManager?.junoClient,
|
||||||
|
notebookUrl,
|
||||||
|
galleryItem,
|
||||||
|
isFavorite,
|
||||||
|
// TabOptions
|
||||||
tabKind: ViewModels.CollectionTabKind.Gallery,
|
tabKind: ViewModels.CollectionTabKind.Gallery,
|
||||||
node: null,
|
|
||||||
title: title,
|
title: title,
|
||||||
tabPath: title,
|
tabPath: title,
|
||||||
documentClientUtility: null,
|
documentClientUtility: null,
|
||||||
collection: null,
|
|
||||||
selfLink: null,
|
selfLink: null,
|
||||||
hashLocation: hashLocation,
|
|
||||||
isActive: ko.observable(false),
|
isActive: ko.observable(false),
|
||||||
|
hashLocation: hashLocation,
|
||||||
|
onUpdateTabsButtons: this.onUpdateTabsButtons,
|
||||||
isTabsContentExpanded: ko.observable(true),
|
isTabsContentExpanded: ko.observable(true),
|
||||||
onLoadStartKey: null,
|
onLoadStartKey: null,
|
||||||
onUpdateTabsButtons: this.onUpdateTabsButtons,
|
|
||||||
container: this,
|
|
||||||
openedTabs: this.openedTabs()
|
openedTabs: this.openedTabs()
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -3277,16 +3196,14 @@ export default class Explorer implements ViewModels.Explorer {
|
|||||||
newTab.onTabClick();
|
newTab.onTabClick();
|
||||||
}
|
}
|
||||||
|
|
||||||
public openNotebookViewer(
|
public async openNotebookViewer(notebookUrl: string) {
|
||||||
notebookUrl: string,
|
const title = path.basename(notebookUrl);
|
||||||
notebookMetadata: DataModels.NotebookMetadata,
|
|
||||||
onNotebookMetadataChange: (newNotebookMetadata: DataModels.NotebookMetadata) => Promise<void>,
|
|
||||||
isLikedNotebook: boolean
|
|
||||||
) {
|
|
||||||
const notebookName = path.basename(notebookUrl);
|
|
||||||
const title = notebookName;
|
|
||||||
const hashLocation = notebookUrl;
|
const hashLocation = notebookUrl;
|
||||||
|
|
||||||
|
if (!this.notebookViewerTab) {
|
||||||
|
this.notebookViewerTab = await import(/* webpackChunkName: "NotebookViewerTab" */ "./Tabs/NotebookViewerTab");
|
||||||
|
}
|
||||||
|
|
||||||
const notebookViewerTabModule = this.notebookViewerTab;
|
const notebookViewerTabModule = this.notebookViewerTab;
|
||||||
|
|
||||||
let isNotebookViewerOpen = (tab: ViewModels.Tab) => {
|
let isNotebookViewerOpen = (tab: ViewModels.Tab) => {
|
||||||
@@ -3322,11 +3239,7 @@ export default class Explorer implements ViewModels.Explorer {
|
|||||||
onUpdateTabsButtons: this.onUpdateTabsButtons,
|
onUpdateTabsButtons: this.onUpdateTabsButtons,
|
||||||
container: this,
|
container: this,
|
||||||
openedTabs: this.openedTabs(),
|
openedTabs: this.openedTabs(),
|
||||||
notebookUrl: notebookUrl,
|
notebookUrl
|
||||||
notebookName: notebookName,
|
|
||||||
notebookMetadata: notebookMetadata,
|
|
||||||
onNotebookMetadataChange: onNotebookMetadataChange,
|
|
||||||
isLikedNotebook: isLikedNotebook
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.openedTabs.push(newTab);
|
this.openedTabs.push(newTab);
|
||||||
@@ -3439,6 +3352,21 @@ export default class Explorer implements ViewModels.Explorer {
|
|||||||
this._openSetupNotebooksPaneForQuickstart();
|
this._openSetupNotebooksPaneForQuickstart();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.importAndOpen(path);
|
// We still use github urls like https://github.com/Azure-Samples/cosmos-notebooks/blob/master/CSharp_quickstarts/GettingStarted_CSharp.ipynb
|
||||||
|
// when launching a notebook quickstart from Portal. In future we should just use gallery id and use Juno to fetch instead of directly
|
||||||
|
// calling GitHub. For now convert this url to a raw url and download content.
|
||||||
|
const gitHubInfo = fromContentUri(path);
|
||||||
|
if (gitHubInfo) {
|
||||||
|
const rawUrl = toRawContentUri(gitHubInfo.owner, gitHubInfo.repo, gitHubInfo.branch, gitHubInfo.path);
|
||||||
|
const response = await fetch(rawUrl);
|
||||||
|
if (response.status === Constants.HttpStatusCodes.OK) {
|
||||||
|
this.notebookToImport = {
|
||||||
|
name: NotebookUtil.getName(path),
|
||||||
|
content: await response.text()
|
||||||
|
};
|
||||||
|
|
||||||
|
this.importAndOpenContent(this.notebookToImport.name, this.notebookToImport.content);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import * as ViewModels from "../../../Contracts/ViewModels";
|
|||||||
import * as DataModels from "../../../Contracts/DataModels";
|
import * as DataModels from "../../../Contracts/DataModels";
|
||||||
import * as StorageUtility from "../../../Shared/StorageUtility";
|
import * as StorageUtility from "../../../Shared/StorageUtility";
|
||||||
import GraphTab from "../../Tabs/GraphTab";
|
import GraphTab from "../../Tabs/GraphTab";
|
||||||
import DocumentClientUtilityBase from "../../../Common/DocumentClientUtilityBase";
|
|
||||||
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
|
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
|
||||||
|
|
||||||
describe("Check whether query result is vertex array", () => {
|
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", () => {
|
describe("Create proper pkid pair", () => {
|
||||||
it("should enclose string pk with quotes", () => {
|
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", () => {
|
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 => {
|
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 => {
|
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 => {
|
const isVisible = (selector: string): boolean => {
|
||||||
@@ -293,7 +356,7 @@ describe("GraphExplorer", () => {
|
|||||||
describe("Load Graph button", () => {
|
describe("Load Graph button", () => {
|
||||||
beforeEach(async done => {
|
beforeEach(async done => {
|
||||||
const backendResponses: BackendResponses = {};
|
const backendResponses: BackendResponses = {};
|
||||||
backendResponses["g.V()"] = backendResponses['g.V("1")'] = {
|
backendResponses["g.V()"] = backendResponses["g.V('1')"] = {
|
||||||
response: [{ id: "1", type: "vertex" }],
|
response: [{ id: "1", type: "vertex" }],
|
||||||
isLast: false
|
isLast: false
|
||||||
};
|
};
|
||||||
@@ -341,7 +404,7 @@ describe("GraphExplorer", () => {
|
|||||||
describe("Execute Gremlin Query button", () => {
|
describe("Execute Gremlin Query button", () => {
|
||||||
beforeEach(done => {
|
beforeEach(done => {
|
||||||
const backendResponses: BackendResponses = {};
|
const backendResponses: BackendResponses = {};
|
||||||
backendResponses["g.V()"] = backendResponses['g.V("2")'] = {
|
backendResponses["g.V()"] = backendResponses["g.V('2')"] = {
|
||||||
response: [{ id: "2", type: "vertex" }],
|
response: [{ id: "2", type: "vertex" }],
|
||||||
isLast: false
|
isLast: false
|
||||||
};
|
};
|
||||||
@@ -411,7 +474,7 @@ describe("GraphExplorer", () => {
|
|||||||
beforeEach(done => {
|
beforeEach(done => {
|
||||||
const backendResponses: BackendResponses = {};
|
const backendResponses: BackendResponses = {};
|
||||||
// TODO Make this less dependent on spaces, order and quotes
|
// 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: [
|
response: [
|
||||||
{
|
{
|
||||||
id: node1Id,
|
id: node1Id,
|
||||||
@@ -667,7 +730,7 @@ describe("GraphExplorer", () => {
|
|||||||
describe("when isGraphAutoVizDisabled setting is true (autoviz disabled)", () => {
|
describe("when isGraphAutoVizDisabled setting is true (autoviz disabled)", () => {
|
||||||
beforeEach(done => {
|
beforeEach(done => {
|
||||||
const backendResponses: BackendResponses = {};
|
const backendResponses: BackendResponses = {};
|
||||||
backendResponses["g.V()"] = backendResponses['g.V("3")'] = {
|
backendResponses["g.V()"] = backendResponses["g.V('3')"] = {
|
||||||
response: [{ id: "3", type: "vertex" }],
|
response: [{ id: "3", type: "vertex" }],
|
||||||
isLast: true
|
isLast: true
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -327,8 +327,8 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
|||||||
* @param id
|
* @param id
|
||||||
*/
|
*/
|
||||||
public static generatePkIdPair(pk: PartitionKeyValueType, id: string) {
|
public static generatePkIdPair(pk: PartitionKeyValueType, id: string) {
|
||||||
const pkStr = typeof pk === "string" ? `"${pk}"` : `${pk}`;
|
const pkStr = typeof pk === "string" ? `'${pk}'` : `${pk}`;
|
||||||
return `[${pkStr}, "${GraphUtil.escapeDoubleQuotes(id)}"]`;
|
return `[${pkStr}, '${GraphUtil.escapeSingleQuotes(id)}']`;
|
||||||
}
|
}
|
||||||
|
|
||||||
public updateVertexProperties(editedProperties: EditedProperties): Q.Promise<GremlinClient.GremlinRequestResult> {
|
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;
|
const pk = v.properties[this.props.collectionPartitionKeyProperty][0].value;
|
||||||
return GraphExplorer.generatePkIdPair(pk, v.id);
|
return GraphExplorer.generatePkIdPair(pk, v.id);
|
||||||
} else {
|
} 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 not partitioned, return 'id'.
|
||||||
* If collection is partitioned, return pk-id pair.
|
* If collection is partitioned, return pk-id pair.
|
||||||
|
* public for testing purposes
|
||||||
* @param vertex
|
* @param vertex
|
||||||
* @return id
|
* @return id
|
||||||
*/
|
*/
|
||||||
private getPkIdFromDocumentId(d: DataModels.DocumentId): string {
|
public static getPkIdFromDocumentId(d: DataModels.DocumentId, collectionPartitionKeyProperty: string): string {
|
||||||
if (this.props.collectionPartitionKeyProperty && d.hasOwnProperty(this.props.collectionPartitionKeyProperty)) {
|
let { id } = d;
|
||||||
const pk = (d as any)[this.props.collectionPartitionKeyProperty];
|
if (typeof id !== "string") {
|
||||||
return GraphExplorer.generatePkIdPair(pk, d.id);
|
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 {
|
||||||
|
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 {
|
} else {
|
||||||
return `"${GraphUtil.escapeDoubleQuotes(d.id)}"`;
|
return `'${GraphUtil.escapeSingleQuotes(id)}'`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1769,7 +1788,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
|||||||
const documents = results.documents || [];
|
const documents = results.documents || [];
|
||||||
return documents.map(
|
return documents.map(
|
||||||
(item: DataModels.DocumentId) => {
|
(item: DataModels.DocumentId) => {
|
||||||
return this.getPkIdFromDocumentId(item);
|
return GraphExplorer.getPkIdFromDocumentId(item, this.props.collectionPartitionKeyProperty);
|
||||||
},
|
},
|
||||||
(reason: any) => {
|
(reason: any) => {
|
||||||
// Failure
|
// Failure
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import * as sinon from "sinon";
|
import * as sinon from "sinon";
|
||||||
import { GremlinClient, GremlinClientParameters } from "./GremlinClient";
|
import { GremlinClient, GremlinClientParameters } from "./GremlinClient";
|
||||||
import { NotificationConsoleUtils } from "../../../Utils/NotificationConsoleUtils";
|
import { NotificationConsoleUtils } from "../../../Utils/NotificationConsoleUtils";
|
||||||
import { Logger } from "../../../Common/Logger";
|
import * as Logger from "../../../Common/Logger";
|
||||||
|
|
||||||
describe("Gremlin Client", () => {
|
describe("Gremlin Client", () => {
|
||||||
const emptyParams: GremlinClientParameters = {
|
const emptyParams: GremlinClientParameters = {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { GremlinSimpleClient, Result } from "./GremlinSimpleClient";
|
|||||||
import { NotificationConsoleUtils } from "../../../Utils/NotificationConsoleUtils";
|
import { NotificationConsoleUtils } from "../../../Utils/NotificationConsoleUtils";
|
||||||
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
|
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
|
||||||
import { HashMap } from "../../../Common/HashMap";
|
import { HashMap } from "../../../Common/HashMap";
|
||||||
import { Logger } from "../../../Common/Logger";
|
import * as Logger from "../../../Common/Logger";
|
||||||
|
|
||||||
export interface GremlinClientParameters {
|
export interface GremlinClientParameters {
|
||||||
endpoint: string;
|
endpoint: string;
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ export class QueryContainerComponent extends React.Component<
|
|||||||
submitFct={(inputValue: string, selection: InputTypeaheadComponent.Item) =>
|
submitFct={(inputValue: string, selection: InputTypeaheadComponent.Item) =>
|
||||||
this.onSubmit(inputValue, selection)
|
this.onSubmit(inputValue, selection)
|
||||||
}
|
}
|
||||||
|
useTextarea={true}
|
||||||
/>
|
/>
|
||||||
{this.renderQueryInputButton()}
|
{this.renderQueryInputButton()}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import * as ViewModels from "../../../Contracts/ViewModels";
|
|||||||
import { CommandBarComponentButtonFactory } from "./CommandBarComponentButtonFactory";
|
import { CommandBarComponentButtonFactory } from "./CommandBarComponentButtonFactory";
|
||||||
import { ExplorerStub } from "../../OpenActionsStubs";
|
import { ExplorerStub } from "../../OpenActionsStubs";
|
||||||
import { GitHubOAuthService } from "../../../GitHub/GitHubOAuthService";
|
import { GitHubOAuthService } from "../../../GitHub/GitHubOAuthService";
|
||||||
|
import NotebookManager from "../../Notebook/NotebookManager";
|
||||||
|
|
||||||
describe("CommandBarComponentButtonFactory tests", () => {
|
describe("CommandBarComponentButtonFactory tests", () => {
|
||||||
let mockExplorer: ViewModels.Explorer;
|
let mockExplorer: ViewModels.Explorer;
|
||||||
@@ -18,7 +19,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||||||
mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false);
|
mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false);
|
||||||
mockExplorer.isPreferredApiCassandra = ko.computed<boolean>(() => false);
|
mockExplorer.isPreferredApiCassandra = ko.computed<boolean>(() => false);
|
||||||
mockExplorer.isSparkEnabled = ko.observable(true);
|
mockExplorer.isSparkEnabled = ko.observable(true);
|
||||||
mockExplorer.isGalleryEnabled = ko.computed<boolean>(() => false);
|
mockExplorer.isGalleryPublishEnabled = ko.computed<boolean>(() => false);
|
||||||
mockExplorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
|
mockExplorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
|
||||||
mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
|
mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
|
||||||
});
|
});
|
||||||
@@ -80,7 +81,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||||||
mockExplorer.isPreferredApiTable = ko.computed(() => true);
|
mockExplorer.isPreferredApiTable = ko.computed(() => true);
|
||||||
mockExplorer.isPreferredApiCassandra = ko.computed<boolean>(() => false);
|
mockExplorer.isPreferredApiCassandra = ko.computed<boolean>(() => false);
|
||||||
mockExplorer.isSparkEnabled = ko.observable(true);
|
mockExplorer.isSparkEnabled = ko.observable(true);
|
||||||
mockExplorer.isGalleryEnabled = ko.computed<boolean>(() => false);
|
mockExplorer.isGalleryPublishEnabled = ko.computed<boolean>(() => false);
|
||||||
mockExplorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
|
mockExplorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
|
||||||
mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
|
mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
|
||||||
});
|
});
|
||||||
@@ -160,7 +161,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||||||
mockExplorer.isPreferredApiTable = ko.computed(() => true);
|
mockExplorer.isPreferredApiTable = ko.computed(() => true);
|
||||||
mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false);
|
mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false);
|
||||||
mockExplorer.isSparkEnabled = ko.observable(true);
|
mockExplorer.isSparkEnabled = ko.observable(true);
|
||||||
mockExplorer.isGalleryEnabled = ko.computed<boolean>(() => false);
|
mockExplorer.isGalleryPublishEnabled = ko.computed<boolean>(() => false);
|
||||||
mockExplorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
|
mockExplorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
|
||||||
mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
|
mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
|
||||||
});
|
});
|
||||||
@@ -246,8 +247,9 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||||||
mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
|
mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
|
||||||
mockExplorer.isNotebooksEnabledForAccount = ko.observable(false);
|
mockExplorer.isNotebooksEnabledForAccount = ko.observable(false);
|
||||||
mockExplorer.isRunningOnNationalCloud = ko.observable(false);
|
mockExplorer.isRunningOnNationalCloud = ko.observable(false);
|
||||||
mockExplorer.isGalleryEnabled = ko.computed<boolean>(() => false);
|
mockExplorer.isGalleryPublishEnabled = ko.computed<boolean>(() => false);
|
||||||
mockExplorer.gitHubOAuthService = new GitHubOAuthService(undefined);
|
mockExplorer.notebookManager = new NotebookManager();
|
||||||
|
mockExplorer.notebookManager.gitHubOAuthService = new GitHubOAuthService(undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -268,7 +270,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||||||
|
|
||||||
it("Notebooks is enabled and GitHubOAuthService is logged in - manage github settings button should be visible", () => {
|
it("Notebooks is enabled and GitHubOAuthService is logged in - manage github settings button should be visible", () => {
|
||||||
mockExplorer.isNotebookEnabled = ko.observable(true);
|
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 buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
|
||||||
const manageGitHubSettingsBtn = buttons.find(
|
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 NewNotebookIcon from "../../../../images/notebook/Notebook-new.svg";
|
||||||
import ResetWorkspaceIcon from "../../../../images/notebook/Notebook-reset-workspace.svg";
|
import ResetWorkspaceIcon from "../../../../images/notebook/Notebook-reset-workspace.svg";
|
||||||
import LibraryManageIcon from "../../../../images/notebook/Spark-library-manage.svg";
|
import LibraryManageIcon from "../../../../images/notebook/Spark-library-manage.svg";
|
||||||
import GalleryIcon from "../../../../images/GalleryIcon.svg";
|
|
||||||
import GitHubIcon from "../../../../images/github.svg";
|
import GitHubIcon from "../../../../images/github.svg";
|
||||||
import SynapseIcon from "../../../../images/synapse-link.svg";
|
import SynapseIcon from "../../../../images/synapse-link.svg";
|
||||||
import { config, Platform } from "../../../Config";
|
import { config, Platform } from "../../../Config";
|
||||||
@@ -64,7 +63,7 @@ export class CommandBarComponentButtonFactory {
|
|||||||
];
|
];
|
||||||
buttons.push(newNotebookButton);
|
buttons.push(newNotebookButton);
|
||||||
|
|
||||||
if (container.gitHubOAuthService) {
|
if (container.notebookManager?.gitHubOAuthService) {
|
||||||
buttons.push(CommandBarComponentButtonFactory.createManageGitHubAccountButton(container));
|
buttons.push(CommandBarComponentButtonFactory.createManageGitHubAccountButton(container));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -87,10 +86,6 @@ export class CommandBarComponentButtonFactory {
|
|||||||
buttons.push(CommandBarComponentButtonFactory.createOpenTerminalButton(container));
|
buttons.push(CommandBarComponentButtonFactory.createOpenTerminalButton(container));
|
||||||
|
|
||||||
buttons.push(CommandBarComponentButtonFactory.createNotebookWorkspaceResetButton(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
|
// 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 {
|
private static createOpenMongoTerminalButton(container: ViewModels.Explorer): ViewModels.NavbarButtonConfig {
|
||||||
const label = "Open Mongo Shell";
|
const label = "Open Mongo Shell";
|
||||||
const tooltip =
|
const tooltip =
|
||||||
@@ -654,7 +636,7 @@ export class CommandBarComponentButtonFactory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static createManageGitHubAccountButton(container: ViewModels.Explorer): ViewModels.NavbarButtonConfig {
|
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";
|
const label = connectedToGitHub ? "Manage GitHub settings" : "Connect to GitHub";
|
||||||
return {
|
return {
|
||||||
iconSrc: GitHubIcon,
|
iconSrc: GitHubIcon,
|
||||||
|
|||||||
@@ -36,11 +36,12 @@ export class CommandBarUtil {
|
|||||||
|
|
||||||
const result: ICommandBarItemProps = {
|
const result: ICommandBarItemProps = {
|
||||||
iconProps: {
|
iconProps: {
|
||||||
iconType: IconType.image,
|
|
||||||
style: {
|
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,
|
onClick: btn.onCommandClick,
|
||||||
key: `${btn.commandButtonLabel}${index}`,
|
key: `${btn.commandButtonLabel}${index}`,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { NotebookContentRecordProps, selectors } from "@nteract/core";
|
|||||||
* A bunch of utilities to interact with nteract
|
* A bunch of utilities to interact with nteract
|
||||||
*/
|
*/
|
||||||
export default class NTeractUtil {
|
export default class NTeractUtil {
|
||||||
public static getCurrentCellType(content: NotebookContentRecordProps): "markdown" | "code" | "raw" {
|
public static getCurrentCellType(content: NotebookContentRecordProps): "markdown" | "code" | "raw" | undefined {
|
||||||
if (!content) {
|
if (!content) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
} from "@nteract/core";
|
} from "@nteract/core";
|
||||||
import * as Immutable from "immutable";
|
import * as Immutable from "immutable";
|
||||||
import { Provider } from "react-redux";
|
import { Provider } from "react-redux";
|
||||||
import { CellType, CellId } from "@nteract/commutable";
|
import { CellType, CellId, toJS } from "@nteract/commutable";
|
||||||
import { Store, AnyAction } from "redux";
|
import { Store, AnyAction } from "redux";
|
||||||
|
|
||||||
import "./NotebookComponent.less";
|
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 {
|
public setContent(name: string, content: any): void {
|
||||||
this.getStore().dispatch(
|
this.getStore().dispatch(
|
||||||
actions.fetchContentFulfilled({
|
actions.fetchContentFulfilled({
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { ServerConfig, IContentProvider, FileType, IContent, IGetParams } from "
|
|||||||
import { Observable } from "rxjs";
|
import { Observable } from "rxjs";
|
||||||
import { AjaxResponse } from "rxjs/ajax";
|
import { AjaxResponse } from "rxjs/ajax";
|
||||||
import { GitHubContentProvider } from "../../../GitHub/GitHubContentProvider";
|
import { GitHubContentProvider } from "../../../GitHub/GitHubContentProvider";
|
||||||
import { GitHubUtils } from "../../../Utils/GitHubUtils";
|
import * as GitHubUtils from "../../../Utils/GitHubUtils";
|
||||||
|
|
||||||
export class NotebookContentProvider implements IContentProvider {
|
export class NotebookContentProvider implements IContentProvider {
|
||||||
constructor(private gitHubContentProvider: GitHubContentProvider, private jupyterContentProvider: IContentProvider) {}
|
constructor(private gitHubContentProvider: GitHubContentProvider, private jupyterContentProvider: IContentProvider) {}
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ import { Notebook } from "../../../Common/Constants";
|
|||||||
import { CellId } from "@nteract/commutable";
|
import { CellId } from "@nteract/commutable";
|
||||||
|
|
||||||
export interface CdbRecordProps {
|
export interface CdbRecordProps {
|
||||||
databaseAccountName: string;
|
databaseAccountName: string | undefined;
|
||||||
defaultExperience: string;
|
defaultExperience: string | undefined;
|
||||||
kernelRestartDelayMs: number;
|
kernelRestartDelayMs: number;
|
||||||
hoveredCellId: CellId;
|
hoveredCellId: CellId | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CdbRecord = Immutable.RecordOf<CdbRecordProps>;
|
export type CdbRecord = Immutable.RecordOf<CdbRecordProps>;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import * as ViewModels from "../../Contracts/ViewModels";
|
|||||||
import * as Constants from "../../Common/Constants";
|
import * as Constants from "../../Common/Constants";
|
||||||
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
|
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
|
||||||
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils";
|
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils";
|
||||||
import { Logger } from "../../Common/Logger";
|
import * as Logger from "../../Common/Logger";
|
||||||
|
|
||||||
export class NotebookContainerClient implements ViewModels.INotebookContainerClient {
|
export class NotebookContainerClient implements ViewModels.INotebookContainerClient {
|
||||||
private reconnectingNotificationId: string;
|
private reconnectingNotificationId: string;
|
||||||
|
|||||||
167
src/Explorer/Notebook/NotebookManager.ts
Normal file
167
src/Explorer/Notebook/NotebookManager.ts
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
/*
|
||||||
|
* 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 * 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(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);
|
||||||
|
|
||||||
|
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,127 +0,0 @@
|
|||||||
import { IGitHubRepo, IGitHubBranch } from "../../GitHub/GitHubClient";
|
|
||||||
|
|
||||||
export const SamplesRepo: IGitHubRepo = {
|
|
||||||
name: "cosmos-notebooks",
|
|
||||||
owner: "Azure-Samples",
|
|
||||||
private: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SamplesBranch: IGitHubBranch = {
|
|
||||||
name: "master"
|
|
||||||
};
|
|
||||||
|
|
||||||
export const isSamplesCall = (owner: string, repo: string, branch?: string): boolean => {
|
|
||||||
return owner === SamplesRepo.owner && repo === SamplesRepo.name && (!branch || branch === SamplesBranch.name);
|
|
||||||
};
|
|
||||||
|
|
||||||
// GitHub API calls have a rate limit of 5000 requests per hour. So if we get high traffic on Data Explorer
|
|
||||||
// loading samples exceed that limit. Using this hard coded response for samples until we fix that.
|
|
||||||
export const SamplesContentsQueryResponse = {
|
|
||||||
repository: {
|
|
||||||
owner: {
|
|
||||||
login: "Azure-Samples"
|
|
||||||
},
|
|
||||||
name: "cosmos-notebooks",
|
|
||||||
isPrivate: false,
|
|
||||||
ref: {
|
|
||||||
name: "master",
|
|
||||||
target: {
|
|
||||||
history: {
|
|
||||||
nodes: [
|
|
||||||
{
|
|
||||||
oid: "cda7facb9e039b173f3376200c26c859896e7974",
|
|
||||||
message:
|
|
||||||
"Merge pull request #45 from Azure-Samples/users/deborahc/pythonSampleUpdates\n\nAdd bokeh version to notebook",
|
|
||||||
committer: {
|
|
||||||
date: "2020-05-28T11:28:01-07:00"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
object: {
|
|
||||||
entries: [
|
|
||||||
{
|
|
||||||
name: ".github",
|
|
||||||
type: "tree",
|
|
||||||
object: {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: ".gitignore",
|
|
||||||
type: "blob",
|
|
||||||
object: {
|
|
||||||
oid: "3e759b75bf455ac809d0987d369aab89137b5689",
|
|
||||||
byteSize: 5582
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "1. GettingStarted.ipynb",
|
|
||||||
type: "blob",
|
|
||||||
object: {
|
|
||||||
oid: "0732ff5366e4aefdc4c378c61cbd968664f0acec",
|
|
||||||
byteSize: 3933
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "2. Visualization.ipynb",
|
|
||||||
type: "blob",
|
|
||||||
object: {
|
|
||||||
oid: "6b16b0740a77afdd38a95bc6c3ebd0f2f17d9465",
|
|
||||||
byteSize: 820317
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "3. RequestUnits.ipynb",
|
|
||||||
type: "blob",
|
|
||||||
object: {
|
|
||||||
oid: "252b79a4adc81e9f2ffde453231b695d75e270e8",
|
|
||||||
byteSize: 9490
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "4. Indexing.ipynb",
|
|
||||||
type: "blob",
|
|
||||||
object: {
|
|
||||||
oid: "e10dd67bd1c55c345226769e4f80e43659ef9cd5",
|
|
||||||
byteSize: 10394
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "5. StoredProcedures.ipynb",
|
|
||||||
type: "blob",
|
|
||||||
object: {
|
|
||||||
oid: "949941949920de4d2d111149e2182e9657cc8134",
|
|
||||||
byteSize: 11818
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "6. GlobalDistribution.ipynb",
|
|
||||||
type: "blob",
|
|
||||||
object: {
|
|
||||||
oid: "b91c31dacacbc9e35750d9054063dda4a5309f3b",
|
|
||||||
byteSize: 11375
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "7. IoTAnomalyDetection.ipynb",
|
|
||||||
type: "blob",
|
|
||||||
object: {
|
|
||||||
oid: "82057ae52a67721a5966e2361317f5dfbd0ee595",
|
|
||||||
byteSize: 377939
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "All_API_quickstarts",
|
|
||||||
type: "tree",
|
|
||||||
object: {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "CSharp_quickstarts",
|
|
||||||
type: "tree",
|
|
||||||
object: {}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { NotebookUtil } from "./NotebookUtil";
|
import { NotebookUtil } from "./NotebookUtil";
|
||||||
import { GitHubUtils } from "../../Utils/GitHubUtils";
|
import * as GitHubUtils from "../../Utils/GitHubUtils";
|
||||||
|
|
||||||
const fileName = "file";
|
const fileName = "file";
|
||||||
const notebookName = "file.ipynb";
|
const notebookName = "file.ipynb";
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import path from "path";
|
|||||||
import { ImmutableNotebook } from "@nteract/commutable";
|
import { ImmutableNotebook } from "@nteract/commutable";
|
||||||
import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem";
|
import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem";
|
||||||
import { StringUtils } from "../../Utils/StringUtils";
|
import { StringUtils } from "../../Utils/StringUtils";
|
||||||
import { GitHubUtils } from "../../Utils/GitHubUtils";
|
import * as GitHubUtils from "../../Utils/GitHubUtils";
|
||||||
|
|
||||||
// Must match rx-jupyter' FileType
|
// Must match rx-jupyter' FileType
|
||||||
export type FileType = "directory" | "file" | "notebook";
|
export type FileType = "directory" | "file" | "notebook";
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ import Q from "q";
|
|||||||
import { ArcadiaWorkspaceItem } from "./Controls/Arcadia/ArcadiaMenuPicker";
|
import { ArcadiaWorkspaceItem } from "./Controls/Arcadia/ArcadiaMenuPicker";
|
||||||
import { CassandraTableKey, CassandraTableKeys, TableDataClient } from "../../src/Explorer/Tables/TableDataClient";
|
import { CassandraTableKey, CassandraTableKeys, TableDataClient } from "../../src/Explorer/Tables/TableDataClient";
|
||||||
import { ConsoleData } from "../../src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
|
import { ConsoleData } from "../../src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
|
||||||
import { GitHubOAuthService } from "../GitHub/GitHubOAuthService";
|
|
||||||
import { IContentProvider } from "@nteract/core";
|
|
||||||
import { MostRecentActivity } from "./MostRecentActivity/MostRecentActivity";
|
import { MostRecentActivity } from "./MostRecentActivity/MostRecentActivity";
|
||||||
import { NotebookContentItem } from "./Notebook/NotebookContentItem";
|
import { NotebookContentItem } from "./Notebook/NotebookContentItem";
|
||||||
import { PlatformType } from "../../src/PlatformType";
|
import { PlatformType } from "../../src/PlatformType";
|
||||||
@@ -22,6 +20,8 @@ import { UploadFilePane } from "./Panes/UploadFilePane";
|
|||||||
import { UploadItemsPaneAdapter } from "./Panes/UploadItemsPaneAdapter";
|
import { UploadItemsPaneAdapter } from "./Panes/UploadItemsPaneAdapter";
|
||||||
import { Versions } from "../../src/Contracts/ExplorerContracts";
|
import { Versions } from "../../src/Contracts/ExplorerContracts";
|
||||||
import { CollectionCreationDefaults } from "../Shared/Constants";
|
import { CollectionCreationDefaults } from "../Shared/Constants";
|
||||||
|
import { IGalleryItem } from "../Juno/JunoClient";
|
||||||
|
import { ReactAdapter } from "../Bindings/ReactBindingHandler";
|
||||||
|
|
||||||
export class ExplorerStub implements ViewModels.Explorer {
|
export class ExplorerStub implements ViewModels.Explorer {
|
||||||
public flight: ko.Observable<string>;
|
public flight: ko.Observable<string>;
|
||||||
@@ -96,8 +96,9 @@ export class ExplorerStub implements ViewModels.Explorer {
|
|||||||
public setupNotebooksPane: SetupNotebooksPane;
|
public setupNotebooksPane: SetupNotebooksPane;
|
||||||
public setupSparkClusterPane: ViewModels.ContextualPane;
|
public setupSparkClusterPane: ViewModels.ContextualPane;
|
||||||
public manageSparkClusterPane: ViewModels.ContextualPane;
|
public manageSparkClusterPane: ViewModels.ContextualPane;
|
||||||
public isGalleryEnabled: ko.Computed<boolean>;
|
public isGalleryPublishEnabled: ko.Computed<boolean>;
|
||||||
public isGitHubPaneEnabled: ko.Observable<boolean>;
|
public isGitHubPaneEnabled: ko.Observable<boolean>;
|
||||||
|
public isPublishNotebookPaneEnabled: ko.Observable<boolean>;
|
||||||
public isRightPanelV2Enabled: ko.Computed<boolean>;
|
public isRightPanelV2Enabled: ko.Computed<boolean>;
|
||||||
public canExceedMaximumValue: ko.Computed<boolean>;
|
public canExceedMaximumValue: ko.Computed<boolean>;
|
||||||
public isHostedDataExplorerEnabled: ko.Computed<boolean>;
|
public isHostedDataExplorerEnabled: ko.Computed<boolean>;
|
||||||
@@ -111,19 +112,19 @@ export class ExplorerStub implements ViewModels.Explorer {
|
|||||||
public arcadiaToken: ko.Observable<string>;
|
public arcadiaToken: ko.Observable<string>;
|
||||||
public notebookWorkspaceManager: ViewModels.NotebookWorkspaceManager;
|
public notebookWorkspaceManager: ViewModels.NotebookWorkspaceManager;
|
||||||
public sparkClusterManager: ViewModels.SparkClusterManager;
|
public sparkClusterManager: ViewModels.SparkClusterManager;
|
||||||
public notebookContentProvider: IContentProvider;
|
|
||||||
public gitHubOAuthService: GitHubOAuthService;
|
|
||||||
public notebookServerInfo: ko.Observable<DataModels.NotebookWorkspaceConnectionInfo>;
|
public notebookServerInfo: ko.Observable<DataModels.NotebookWorkspaceConnectionInfo>;
|
||||||
public sparkClusterConnectionInfo: ko.Observable<DataModels.SparkClusterConnectionInfo>;
|
public sparkClusterConnectionInfo: ko.Observable<DataModels.SparkClusterConnectionInfo>;
|
||||||
public libraryManagePane: ViewModels.ContextualPane;
|
public libraryManagePane: ViewModels.ContextualPane;
|
||||||
public clusterLibraryPane: ViewModels.ContextualPane;
|
public clusterLibraryPane: ViewModels.ContextualPane;
|
||||||
public gitHubReposPane: ViewModels.ContextualPane;
|
public gitHubReposPane: ViewModels.ContextualPane;
|
||||||
|
public publishNotebookPaneAdapter: ReactAdapter;
|
||||||
public arcadiaWorkspaces: ko.ObservableArray<ArcadiaWorkspaceItem>;
|
public arcadiaWorkspaces: ko.ObservableArray<ArcadiaWorkspaceItem>;
|
||||||
public hasStorageAnalyticsAfecFeature: ko.Observable<boolean>;
|
public hasStorageAnalyticsAfecFeature: ko.Observable<boolean>;
|
||||||
public isSynapseLinkUpdating: ko.Observable<boolean>;
|
public isSynapseLinkUpdating: ko.Observable<boolean>;
|
||||||
public isNotebookTabActive: ko.Computed<boolean>;
|
public isNotebookTabActive: ko.Computed<boolean>;
|
||||||
public memoryUsageInfo: ko.Observable<DataModels.MemoryUsageInfo>;
|
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 openNotebookViewer: (notebookUrl: string) => void;
|
||||||
public resourceTokenDatabaseId: ko.Observable<string>;
|
public resourceTokenDatabaseId: ko.Observable<string>;
|
||||||
public resourceTokenCollectionId: ko.Observable<string>;
|
public resourceTokenCollectionId: ko.Observable<string>;
|
||||||
@@ -331,7 +332,11 @@ export class ExplorerStub implements ViewModels.Explorer {
|
|||||||
throw new Error("Not implemented");
|
throw new Error("Not implemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
public importAndOpenFromGallery(path: string, newName: string, content: any): Promise<boolean> {
|
public importAndOpenContent(name: string, content: string): Promise<boolean> {
|
||||||
|
throw new Error("Not implemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
public publishNotebook(name: string, content: string): void {
|
||||||
throw new Error("Not implemented");
|
throw new Error("Not implemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import * as ViewModels from "../../Contracts/ViewModels";
|
|||||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||||
import { Areas } from "../../Common/Constants";
|
import { Areas } from "../../Common/Constants";
|
||||||
import { ContextualPaneBase } from "./ContextualPaneBase";
|
import { ContextualPaneBase } from "./ContextualPaneBase";
|
||||||
import { Logger } from "../../Common/Logger";
|
import * as Logger from "../../Common/Logger";
|
||||||
import { QueriesGridComponentAdapter } from "../Controls/QueriesGridReactComponent/QueriesGridComponentAdapter";
|
import { QueriesGridComponentAdapter } from "../Controls/QueriesGridReactComponent/QueriesGridComponentAdapter";
|
||||||
import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { ContextualPaneBase } from "./ContextualPaneBase";
|
|||||||
import { ClusterLibraryGridAdapter } from "../Controls/LibraryManagement/ClusterLibraryGridAdapter";
|
import { ClusterLibraryGridAdapter } from "../Controls/LibraryManagement/ClusterLibraryGridAdapter";
|
||||||
import { ClusterLibraryGridProps, ClusterLibraryItem } from "../Controls/LibraryManagement/ClusterLibraryGrid";
|
import { ClusterLibraryGridProps, ClusterLibraryItem } from "../Controls/LibraryManagement/ClusterLibraryGrid";
|
||||||
import { Library, SparkCluster, SparkClusterLibrary } from "../../Contracts/DataModels";
|
import { Library, SparkCluster, SparkClusterLibrary } from "../../Contracts/DataModels";
|
||||||
import { Logger } from "../../Common/Logger";
|
import * as Logger from "../../Common/Logger";
|
||||||
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils";
|
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils";
|
||||||
|
|
||||||
export class ClusterLibraryPane extends ContextualPaneBase {
|
export class ClusterLibraryPane extends ContextualPaneBase {
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import _ from "underscore";
|
import _ from "underscore";
|
||||||
import { Areas, HttpStatusCodes } from "../../Common/Constants";
|
import { Areas, HttpStatusCodes } from "../../Common/Constants";
|
||||||
import { Logger } from "../../Common/Logger";
|
import * as Logger from "../../Common/Logger";
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
import * as ViewModels from "../../Contracts/ViewModels";
|
||||||
import { GitHubClient, IGitHubPageInfo, IGitHubRepo } from "../../GitHub/GitHubClient";
|
import { GitHubClient, IGitHubPageInfo, IGitHubRepo } from "../../GitHub/GitHubClient";
|
||||||
import { IPinnedRepo, JunoClient } from "../../Juno/JunoClient";
|
import { IPinnedRepo, JunoClient } from "../../Juno/JunoClient";
|
||||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||||
import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||||
import { GitHubUtils } from "../../Utils/GitHubUtils";
|
import * as GitHubUtils from "../../Utils/GitHubUtils";
|
||||||
import { JunoUtils } from "../../Utils/JunoUtils";
|
import { JunoUtils } from "../../Utils/JunoUtils";
|
||||||
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils";
|
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils";
|
||||||
import { AuthorizeAccessComponent } from "../Controls/GitHub/AuthorizeAccessComponent";
|
import { AuthorizeAccessComponent } from "../Controls/GitHub/AuthorizeAccessComponent";
|
||||||
@@ -132,12 +132,13 @@ export class GitHubReposPane extends ContextualPaneBase {
|
|||||||
|
|
||||||
private getOAuthScope(): string {
|
private getOAuthScope(): string {
|
||||||
return (
|
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 {
|
private setup(forceShowConnectToGitHub = false): void {
|
||||||
forceShowConnectToGitHub || !this.container.gitHubOAuthService.isLoggedIn()
|
forceShowConnectToGitHub || !this.container.notebookManager?.gitHubOAuthService.isLoggedIn()
|
||||||
? this.setupForConnectToGitHub()
|
? this.setupForConnectToGitHub()
|
||||||
: this.setupForManageRepos();
|
: this.setupForManageRepos();
|
||||||
}
|
}
|
||||||
@@ -294,7 +295,7 @@ export class GitHubReposPane extends ContextualPaneBase {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await this.junoClient.getPinnedRepos(
|
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) {
|
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
|
||||||
throw new Error(`Received HTTP ${response.status} when fetching pinned repos`);
|
throw new Error(`Received HTTP ${response.status} when fetching pinned repos`);
|
||||||
@@ -350,7 +351,7 @@ export class GitHubReposPane extends ContextualPaneBase {
|
|||||||
dataExplorerArea: Areas.Notebook,
|
dataExplorerArea: Areas.Notebook,
|
||||||
scopesSelected: scope
|
scopesSelected: scope
|
||||||
});
|
});
|
||||||
this.container.gitHubOAuthService.startOAuth(scope);
|
this.container.notebookManager?.gitHubOAuthService.startOAuth(scope);
|
||||||
}
|
}
|
||||||
|
|
||||||
private triggerRender(): void {
|
private triggerRender(): void {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
LibraryManageGridProps
|
LibraryManageGridProps
|
||||||
} from "../Controls/LibraryManagement/LibraryManage";
|
} from "../Controls/LibraryManagement/LibraryManage";
|
||||||
import { Library } from "../../Contracts/DataModels";
|
import { Library } from "../../Contracts/DataModels";
|
||||||
import { Logger } from "../../Common/Logger";
|
import * as Logger from "../../Common/Logger";
|
||||||
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils";
|
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils";
|
||||||
|
|
||||||
export class LibraryManagePane extends ContextualPaneBase {
|
export class LibraryManagePane extends ContextualPaneBase {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import * as Constants from "../../Common/Constants";
|
|||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
import * as ViewModels from "../../Contracts/ViewModels";
|
||||||
import { ContextualPaneBase } from "./ContextualPaneBase";
|
import { ContextualPaneBase } from "./ContextualPaneBase";
|
||||||
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
|
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
|
||||||
import { Logger } from "../../Common/Logger";
|
import * as Logger from "../../Common/Logger";
|
||||||
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils";
|
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils";
|
||||||
|
|
||||||
export class LoadQueryPane extends ContextualPaneBase implements ViewModels.LoadQueryPane {
|
export class LoadQueryPane extends ContextualPaneBase implements ViewModels.LoadQueryPane {
|
||||||
|
|||||||
164
src/Explorer/Panes/PublishNotebookPaneAdapter.tsx
Normal file
164
src/Explorer/Panes/PublishNotebookPaneAdapter.tsx
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
import ko from "knockout";
|
||||||
|
import { ITextFieldProps, Stack, Text, TextField } from "office-ui-fabric-react";
|
||||||
|
import * as React from "react";
|
||||||
|
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
|
||||||
|
import * as Logger from "../../Common/Logger";
|
||||||
|
import * as ViewModels from "../../Contracts/ViewModels";
|
||||||
|
import { JunoClient } from "../../Juno/JunoClient";
|
||||||
|
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils";
|
||||||
|
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
|
||||||
|
import { FileSystemUtil } from "../Notebook/FileSystemUtil";
|
||||||
|
import { GenericRightPaneComponent, GenericRightPaneProps } from "./GenericRightPaneComponent";
|
||||||
|
|
||||||
|
export class PublishNotebookPaneAdapter implements ReactAdapter {
|
||||||
|
parameters: ko.Observable<number>;
|
||||||
|
private isOpened: boolean;
|
||||||
|
private isExecuting: boolean;
|
||||||
|
private formError: string;
|
||||||
|
private formErrorDetail: string;
|
||||||
|
|
||||||
|
private name: string;
|
||||||
|
private author: string;
|
||||||
|
private content: string;
|
||||||
|
private description: string;
|
||||||
|
private tags: string;
|
||||||
|
private thumbnailUrl: string;
|
||||||
|
|
||||||
|
constructor(private container: ViewModels.Explorer, private junoClient: JunoClient) {
|
||||||
|
this.parameters = ko.observable(Date.now());
|
||||||
|
this.reset();
|
||||||
|
this.triggerRender();
|
||||||
|
}
|
||||||
|
|
||||||
|
public renderComponent(): JSX.Element {
|
||||||
|
if (!this.isOpened) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props: GenericRightPaneProps = {
|
||||||
|
container: this.container,
|
||||||
|
content: this.createContent(),
|
||||||
|
formError: this.formError,
|
||||||
|
formErrorDetail: this.formErrorDetail,
|
||||||
|
id: "publishnotebookpane",
|
||||||
|
isExecuting: this.isExecuting,
|
||||||
|
title: "Publish to gallery",
|
||||||
|
submitButtonText: "Publish",
|
||||||
|
onClose: () => this.close(),
|
||||||
|
onSubmit: () => this.submit()
|
||||||
|
};
|
||||||
|
|
||||||
|
return <GenericRightPaneComponent {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
public triggerRender(): void {
|
||||||
|
window.requestAnimationFrame(() => this.parameters(Date.now()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public open(name: string, author: string, content: string): void {
|
||||||
|
this.name = name;
|
||||||
|
this.author = author;
|
||||||
|
this.content = content;
|
||||||
|
|
||||||
|
this.isOpened = true;
|
||||||
|
this.triggerRender();
|
||||||
|
}
|
||||||
|
|
||||||
|
public close(): void {
|
||||||
|
this.reset();
|
||||||
|
this.triggerRender();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async submit(): Promise<void> {
|
||||||
|
const notificationId = NotificationConsoleUtils.logConsoleMessage(
|
||||||
|
ConsoleDataType.InProgress,
|
||||||
|
`Publishing ${this.name} to gallery`
|
||||||
|
);
|
||||||
|
this.isExecuting = true;
|
||||||
|
this.triggerRender();
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!this.name || !this.description || !this.author) {
|
||||||
|
throw new Error("Name, description, and author are required");
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await this.junoClient.publishNotebook(
|
||||||
|
this.name,
|
||||||
|
this.description,
|
||||||
|
this.tags?.split(","),
|
||||||
|
this.author,
|
||||||
|
this.thumbnailUrl,
|
||||||
|
this.content
|
||||||
|
);
|
||||||
|
if (!response.data) {
|
||||||
|
throw new Error(`Received HTTP ${response.status} when publishing ${name} to gallery`);
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, `Published ${name} to gallery`);
|
||||||
|
} catch (error) {
|
||||||
|
this.formError = `Failed to publish ${this.name} to gallery`;
|
||||||
|
this.formErrorDetail = `${error}`;
|
||||||
|
|
||||||
|
const message = `${this.formError}: ${this.formErrorDetail}`;
|
||||||
|
Logger.logError(message, "PublishNotebookPaneAdapter/submit");
|
||||||
|
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message);
|
||||||
|
return;
|
||||||
|
} finally {
|
||||||
|
NotificationConsoleUtils.clearInProgressMessageWithId(notificationId);
|
||||||
|
this.isExecuting = false;
|
||||||
|
this.triggerRender();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private createContent = (): JSX.Element => {
|
||||||
|
const descriptionPara1 =
|
||||||
|
"This notebook has your data. Please make sure you delete any sensitive data/output before publishing.";
|
||||||
|
const descriptionPara2 = `Would you like to publish and share ${FileSystemUtil.stripExtension(
|
||||||
|
this.name,
|
||||||
|
"ipynb"
|
||||||
|
)} to the gallery?`;
|
||||||
|
const descriptionProps: ITextFieldProps = {
|
||||||
|
label: "Description",
|
||||||
|
ariaLabel: "Description",
|
||||||
|
multiline: true,
|
||||||
|
rows: 3,
|
||||||
|
required: true,
|
||||||
|
onChange: (event, newValue) => (this.description = newValue)
|
||||||
|
};
|
||||||
|
const tagsProps: ITextFieldProps = {
|
||||||
|
label: "Tags",
|
||||||
|
ariaLabel: "Tags",
|
||||||
|
placeholder: "Optional tag 1, Optional tag 2",
|
||||||
|
onChange: (event, newValue) => (this.tags = newValue)
|
||||||
|
};
|
||||||
|
const thumbnailProps: ITextFieldProps = {
|
||||||
|
label: "Cover image url",
|
||||||
|
ariaLabel: "Cover image url",
|
||||||
|
onChange: (event, newValue) => (this.thumbnailUrl = newValue)
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="panelContent">
|
||||||
|
<Stack className="paneMainContent" tokens={{ childrenGap: 20 }}>
|
||||||
|
<Text>{descriptionPara1}</Text>
|
||||||
|
<Text>{descriptionPara2}</Text>
|
||||||
|
<TextField {...descriptionProps} />
|
||||||
|
<TextField {...tagsProps} />
|
||||||
|
<TextField {...thumbnailProps} />
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
private reset = (): void => {
|
||||||
|
this.isOpened = false;
|
||||||
|
this.isExecuting = false;
|
||||||
|
this.formError = undefined;
|
||||||
|
this.formErrorDetail = undefined;
|
||||||
|
this.name = undefined;
|
||||||
|
this.author = undefined;
|
||||||
|
this.content = undefined;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ import * as Constants from "../../Common/Constants";
|
|||||||
import * as Entities from "./Entities";
|
import * as Entities from "./Entities";
|
||||||
import EnvironmentUtility from "../../Common/EnvironmentUtility";
|
import EnvironmentUtility from "../../Common/EnvironmentUtility";
|
||||||
import * as HeadersUtility from "../../Common/HeadersUtility";
|
import * as HeadersUtility from "../../Common/HeadersUtility";
|
||||||
import { Logger } from "../../Common/Logger";
|
import * as Logger from "../../Common/Logger";
|
||||||
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils";
|
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils";
|
||||||
import * as TableConstants from "./Constants";
|
import * as TableConstants from "./Constants";
|
||||||
import * as TableEntityProcessor from "./TableEntityProcessor";
|
import * as TableEntityProcessor from "./TableEntityProcessor";
|
||||||
|
|||||||
@@ -1,45 +1,151 @@
|
|||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
|
||||||
import TabsBase from "./TabsBase";
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
|
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
|
||||||
import { GalleryViewerContainerComponent } from "../Controls/NotebookGallery/GalleryViewerComponent";
|
import * as ViewModels from "../../Contracts/ViewModels";
|
||||||
|
import { IGalleryItem, JunoClient } from "../../Juno/JunoClient";
|
||||||
|
import * as GalleryUtils from "../../Utils/GalleryUtils";
|
||||||
|
import {
|
||||||
|
GalleryTab as GalleryViewerTab,
|
||||||
|
GalleryViewerComponent,
|
||||||
|
GalleryViewerComponentProps,
|
||||||
|
SortBy
|
||||||
|
} from "../Controls/NotebookGallery/GalleryViewerComponent";
|
||||||
|
import {
|
||||||
|
NotebookViewerComponent,
|
||||||
|
NotebookViewerComponentProps
|
||||||
|
} from "../Controls/NotebookViewer/NotebookViewerComponent";
|
||||||
|
import TabsBase from "./TabsBase";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notebook gallery tab
|
* Notebook gallery tab
|
||||||
*/
|
*/
|
||||||
|
interface GalleryComponentAdapterProps {
|
||||||
|
container: ViewModels.Explorer;
|
||||||
|
junoClient: JunoClient;
|
||||||
|
notebookUrl: string;
|
||||||
|
galleryItem: IGalleryItem;
|
||||||
|
isFavorite: boolean;
|
||||||
|
selectedTab: GalleryViewerTab;
|
||||||
|
sortBy: SortBy;
|
||||||
|
searchText: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GalleryComponentAdapterState {
|
||||||
|
notebookUrl: string;
|
||||||
|
galleryItem: IGalleryItem;
|
||||||
|
isFavorite: boolean;
|
||||||
|
selectedTab: GalleryViewerTab;
|
||||||
|
sortBy: SortBy;
|
||||||
|
searchText: string;
|
||||||
|
}
|
||||||
|
|
||||||
class GalleryComponentAdapter implements ReactAdapter {
|
class GalleryComponentAdapter implements ReactAdapter {
|
||||||
public parameters: ko.Computed<boolean>;
|
public parameters: ko.Observable<number>;
|
||||||
constructor(private getContainer: () => ViewModels.Explorer) {}
|
private state: GalleryComponentAdapterState;
|
||||||
|
|
||||||
|
constructor(private props: GalleryComponentAdapterProps) {
|
||||||
|
this.parameters = ko.observable<number>(Date.now());
|
||||||
|
this.state = {
|
||||||
|
notebookUrl: props.notebookUrl,
|
||||||
|
galleryItem: props.galleryItem,
|
||||||
|
isFavorite: props.isFavorite,
|
||||||
|
selectedTab: props.selectedTab,
|
||||||
|
sortBy: props.sortBy,
|
||||||
|
searchText: props.searchText
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public renderComponent(): JSX.Element {
|
public renderComponent(): JSX.Element {
|
||||||
return this.parameters() ? <GalleryViewerContainerComponent container={this.getContainer()} /> : <></>;
|
if (this.state.notebookUrl) {
|
||||||
|
const props: NotebookViewerComponentProps = {
|
||||||
|
container: this.props.container,
|
||||||
|
junoClient: this.props.junoClient,
|
||||||
|
notebookUrl: this.state.notebookUrl,
|
||||||
|
galleryItem: this.state.galleryItem,
|
||||||
|
isFavorite: this.state.isFavorite,
|
||||||
|
backNavigationText: GalleryUtils.getTabTitle(this.state.selectedTab),
|
||||||
|
onBackClick: this.onBackClick,
|
||||||
|
onTagClick: this.loadTaggedItems
|
||||||
|
};
|
||||||
|
|
||||||
|
return <NotebookViewerComponent {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props: GalleryViewerComponentProps = {
|
||||||
|
container: this.props.container,
|
||||||
|
junoClient: this.props.junoClient,
|
||||||
|
selectedTab: this.state.selectedTab,
|
||||||
|
sortBy: this.state.sortBy,
|
||||||
|
searchText: this.state.searchText,
|
||||||
|
onSelectedTabChange: this.onSelectedTabChange,
|
||||||
|
onSortByChange: this.onSortByChange,
|
||||||
|
onSearchTextChange: this.onSearchTextChange
|
||||||
|
};
|
||||||
|
|
||||||
|
return <GalleryViewerComponent {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public setState(state: Partial<GalleryComponentAdapterState>): void {
|
||||||
|
this.state = Object.assign(this.state, state);
|
||||||
|
window.requestAnimationFrame(() => this.parameters(Date.now()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private onBackClick = (): void => {
|
||||||
|
this.props.container.openGallery();
|
||||||
|
};
|
||||||
|
|
||||||
|
private loadTaggedItems = (tag: string): void => {
|
||||||
|
this.setState({
|
||||||
|
notebookUrl: undefined,
|
||||||
|
searchText: tag
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private onSelectedTabChange = (selectedTab: GalleryViewerTab): void => {
|
||||||
|
this.state.selectedTab = selectedTab;
|
||||||
|
};
|
||||||
|
|
||||||
|
private onSortByChange = (sortBy: SortBy): void => {
|
||||||
|
this.state.sortBy = sortBy;
|
||||||
|
};
|
||||||
|
|
||||||
|
private onSearchTextChange = (searchText: string): void => {
|
||||||
|
this.state.searchText = searchText;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class GalleryTab extends TabsBase implements ViewModels.Tab {
|
export default class GalleryTab extends TabsBase implements ViewModels.Tab {
|
||||||
private container: ViewModels.Explorer;
|
private container: ViewModels.Explorer;
|
||||||
|
private galleryComponentAdapterProps: GalleryComponentAdapterProps;
|
||||||
private galleryComponentAdapter: GalleryComponentAdapter;
|
private galleryComponentAdapter: GalleryComponentAdapter;
|
||||||
|
|
||||||
constructor(options: ViewModels.GalleryTabOptions) {
|
constructor(options: ViewModels.GalleryTabOptions) {
|
||||||
super(options);
|
super(options);
|
||||||
this.container = options.container;
|
|
||||||
this.galleryComponentAdapter = new GalleryComponentAdapter(() => this.getContainer());
|
|
||||||
|
|
||||||
this.galleryComponentAdapter.parameters = ko.computed<boolean>(() => {
|
this.container = options.container;
|
||||||
return this.isTemplateReady() && this.container.isNotebookEnabled();
|
this.galleryComponentAdapterProps = {
|
||||||
});
|
container: options.container,
|
||||||
|
junoClient: options.junoClient,
|
||||||
|
notebookUrl: options.notebookUrl,
|
||||||
|
galleryItem: options.galleryItem,
|
||||||
|
isFavorite: options.isFavorite,
|
||||||
|
selectedTab: GalleryViewerTab.OfficialSamples,
|
||||||
|
sortBy: SortBy.MostViewed,
|
||||||
|
searchText: undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
this.galleryComponentAdapter = new GalleryComponentAdapter(this.galleryComponentAdapterProps);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getContainer(): ViewModels.Explorer {
|
protected getContainer(): ViewModels.Explorer {
|
||||||
return this.container;
|
return this.container;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getTabsButtons(): ViewModels.NavbarButtonConfig[] {
|
public updateGalleryParams(notebookUrl?: string, galleryItem?: IGalleryItem, isFavorite?: boolean): void {
|
||||||
return [];
|
this.galleryComponentAdapter.setState({
|
||||||
}
|
notebookUrl,
|
||||||
|
galleryItem,
|
||||||
protected buildCommandBarOptions(): void {
|
isFavorite
|
||||||
this.updateNavbarWithTabsButtons();
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
updateDocument
|
updateDocument
|
||||||
} from "../../Common/MongoProxyClient";
|
} from "../../Common/MongoProxyClient";
|
||||||
import { extractPartitionKey } from "@azure/cosmos";
|
import { extractPartitionKey } from "@azure/cosmos";
|
||||||
import { Logger } from "../../Common/Logger";
|
import * as Logger from "../../Common/Logger";
|
||||||
import { PartitionKeyDefinition } from "@azure/cosmos";
|
import { PartitionKeyDefinition } from "@azure/cosmos";
|
||||||
|
|
||||||
export default class MongoDocumentsTab extends DocumentsTab implements ViewModels.DocumentsTab {
|
export default class MongoDocumentsTab extends DocumentsTab implements ViewModels.DocumentsTab {
|
||||||
@@ -177,10 +177,9 @@ export default class MongoDocumentsTab extends DocumentsTab implements ViewModel
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
reason => {
|
reason => {
|
||||||
|
this.isExecutionError(true);
|
||||||
const message = ErrorParserUtility.parse(reason)[0].message;
|
const message = ErrorParserUtility.parse(reason)[0].message;
|
||||||
window.alert(message);
|
window.alert(message);
|
||||||
this.isExecutionError(true);
|
|
||||||
console.error(reason);
|
|
||||||
TelemetryProcessor.traceFailure(
|
TelemetryProcessor.traceFailure(
|
||||||
Action.UpdateDocument,
|
Action.UpdateDocument,
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export default class NotebookTabV2 extends TabsBase implements ViewModels.Tab {
|
|||||||
connectionInfo: this.container.notebookServerInfo(),
|
connectionInfo: this.container.notebookServerInfo(),
|
||||||
databaseAccountName: this.container.databaseAccount().name,
|
databaseAccountName: this.container.databaseAccount().name,
|
||||||
defaultExperience: this.container.defaultExperience(),
|
defaultExperience: this.container.defaultExperience(),
|
||||||
contentProvider: this.container.notebookContentProvider
|
contentProvider: this.container.notebookManager?.notebookContentProvider
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,6 +112,7 @@ export default class NotebookTabV2 extends TabsBase implements ViewModels.Tab {
|
|||||||
const availableKernels = NotebookTabV2.clientManager.getAvailableKernelSpecs();
|
const availableKernels = NotebookTabV2.clientManager.getAvailableKernelSpecs();
|
||||||
|
|
||||||
const saveLabel = "Save";
|
const saveLabel = "Save";
|
||||||
|
const publishLabel = "Publish to gallery";
|
||||||
const workspaceLabel = "No Workspace";
|
const workspaceLabel = "No Workspace";
|
||||||
const kernelLabel = "No Kernel";
|
const kernelLabel = "No Kernel";
|
||||||
const runLabel = "Run";
|
const runLabel = "Run";
|
||||||
@@ -142,7 +143,27 @@ export default class NotebookTabV2 extends TabsBase implements ViewModels.Tab {
|
|||||||
commandButtonLabel: saveLabel,
|
commandButtonLabel: saveLabel,
|
||||||
hasPopup: false,
|
hasPopup: false,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
ariaLabel: saveLabel
|
ariaLabel: saveLabel,
|
||||||
|
children: this.container.isGalleryPublishEnabled()
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
iconName: "Save",
|
||||||
|
onCommandClick: () => this.notebookComponentAdapter.notebookSave(),
|
||||||
|
commandButtonLabel: saveLabel,
|
||||||
|
hasPopup: false,
|
||||||
|
disabled: false,
|
||||||
|
ariaLabel: saveLabel
|
||||||
|
},
|
||||||
|
{
|
||||||
|
iconName: "PublishContent",
|
||||||
|
onCommandClick: () => this.publishToGallery(),
|
||||||
|
commandButtonLabel: publishLabel,
|
||||||
|
hasPopup: false,
|
||||||
|
disabled: false,
|
||||||
|
ariaLabel: publishLabel
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: undefined
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
iconSrc: null,
|
iconSrc: null,
|
||||||
@@ -425,6 +446,11 @@ export default class NotebookTabV2 extends TabsBase implements ViewModels.Tab {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private publishToGallery = () => {
|
||||||
|
const notebookContent = this.notebookComponentAdapter.getContent();
|
||||||
|
this.container.publishNotebook(notebookContent.name, notebookContent.content);
|
||||||
|
};
|
||||||
|
|
||||||
private traceTelemetry(actionType: number) {
|
private traceTelemetry(actionType: number) {
|
||||||
TelemetryProcessor.trace(actionType, ActionModifiers.Mark, {
|
TelemetryProcessor.trace(actionType, ActionModifiers.Mark, {
|
||||||
databaseAccountName: this.container.databaseAccount() && this.container.databaseAccount().name,
|
databaseAccountName: this.container.databaseAccount() && this.container.databaseAccount().name,
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
|
||||||
import * as DataModels from "../../Contracts/DataModels";
|
|
||||||
import TabsBase from "./TabsBase";
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
|
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
|
||||||
import { NotebookViewerComponent } from "../Controls/NotebookViewer/NotebookViewerComponent";
|
import * as ViewModels from "../../Contracts/ViewModels";
|
||||||
|
import {
|
||||||
|
NotebookViewerComponent,
|
||||||
|
NotebookViewerComponentProps
|
||||||
|
} from "../Controls/NotebookViewer/NotebookViewerComponent";
|
||||||
|
import TabsBase from "./TabsBase";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notebook Viewer tab
|
* Notebook Viewer tab
|
||||||
@@ -12,48 +14,32 @@ import { NotebookViewerComponent } from "../Controls/NotebookViewer/NotebookView
|
|||||||
class NotebookViewerComponentAdapter implements ReactAdapter {
|
class NotebookViewerComponentAdapter implements ReactAdapter {
|
||||||
// parameters: true: show, false: hide
|
// parameters: true: show, false: hide
|
||||||
public parameters: ko.Computed<boolean>;
|
public parameters: ko.Computed<boolean>;
|
||||||
constructor(
|
constructor(private notebookUrl: string) {}
|
||||||
private notebookUrl: string,
|
|
||||||
private notebookName: string,
|
|
||||||
private container: ViewModels.Explorer,
|
|
||||||
private notebookMetadata: DataModels.NotebookMetadata,
|
|
||||||
private onNotebookMetadataChange: (newNotebookMetadata: DataModels.NotebookMetadata) => Promise<void>,
|
|
||||||
private isLikedNotebook: boolean
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public renderComponent(): JSX.Element {
|
public renderComponent(): JSX.Element {
|
||||||
return this.parameters() ? (
|
const props: NotebookViewerComponentProps = {
|
||||||
<NotebookViewerComponent
|
notebookUrl: this.notebookUrl,
|
||||||
notebookUrl={this.notebookUrl}
|
backNavigationText: undefined,
|
||||||
notebookMetadata={this.notebookMetadata}
|
onBackClick: undefined,
|
||||||
notebookName={this.notebookName}
|
onTagClick: undefined
|
||||||
container={this.container}
|
};
|
||||||
onNotebookMetadataChange={this.onNotebookMetadataChange}
|
|
||||||
isLikedNotebook={this.isLikedNotebook}
|
return this.parameters() ? <NotebookViewerComponent {...props} /> : <></>;
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class NotebookViewerTab extends TabsBase implements ViewModels.Tab {
|
export default class NotebookViewerTab extends TabsBase implements ViewModels.Tab {
|
||||||
private container: ViewModels.Explorer;
|
private container: ViewModels.Explorer;
|
||||||
public notebookViewerComponentAdapter: NotebookViewerComponentAdapter;
|
|
||||||
public notebookUrl: string;
|
public notebookUrl: string;
|
||||||
|
|
||||||
|
public notebookViewerComponentAdapter: NotebookViewerComponentAdapter;
|
||||||
|
|
||||||
constructor(options: ViewModels.NotebookViewerTabOptions) {
|
constructor(options: ViewModels.NotebookViewerTabOptions) {
|
||||||
super(options);
|
super(options);
|
||||||
this.container = options.container;
|
this.container = options.container;
|
||||||
this.notebookUrl = options.notebookUrl;
|
this.notebookUrl = options.notebookUrl;
|
||||||
this.notebookViewerComponentAdapter = new NotebookViewerComponentAdapter(
|
|
||||||
options.notebookUrl,
|
this.notebookViewerComponentAdapter = new NotebookViewerComponentAdapter(options.notebookUrl);
|
||||||
options.notebookName,
|
|
||||||
options.container,
|
|
||||||
options.notebookMetadata,
|
|
||||||
options.onNotebookMetadataChange,
|
|
||||||
options.isLikedNotebook
|
|
||||||
);
|
|
||||||
|
|
||||||
this.notebookViewerComponentAdapter.parameters = ko.computed<boolean>(() => {
|
this.notebookViewerComponentAdapter.parameters = ko.computed<boolean>(() => {
|
||||||
if (this.isTemplateReady() && this.container.isNotebookEnabled()) {
|
if (this.isTemplateReady() && this.container.isNotebookEnabled()) {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import * as ErrorParserUtility from "../../Common/ErrorParserUtility";
|
|||||||
import TabsBase from "./TabsBase";
|
import TabsBase from "./TabsBase";
|
||||||
import { HashMap } from "../../Common/HashMap";
|
import { HashMap } from "../../Common/HashMap";
|
||||||
import * as HeadersUtility from "../../Common/HeadersUtility";
|
import * as HeadersUtility from "../../Common/HeadersUtility";
|
||||||
import { Logger } from "../../Common/Logger";
|
import * as Logger from "../../Common/Logger";
|
||||||
import { Splitter, SplitterBounds, SplitterDirection } from "../../Common/Splitter";
|
import { Splitter, SplitterBounds, SplitterDirection } from "../../Common/Splitter";
|
||||||
import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||||
import ExecuteQueryIcon from "../../../images/ExecuteQuery.svg";
|
import ExecuteQueryIcon from "../../../images/ExecuteQuery.svg";
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import UploadWorker from "worker-loader!../../workers/upload";
|
|||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
import * as Constants from "../../Common/Constants";
|
import * as Constants from "../../Common/Constants";
|
||||||
import { CosmosClient } from "../../Common/CosmosClient";
|
import { CosmosClient } from "../../Common/CosmosClient";
|
||||||
import { Logger } from "../../Common/Logger";
|
import * as Logger from "../../Common/Logger";
|
||||||
import * as DataModels from "../../Contracts/DataModels";
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
import * as ViewModels from "../../Contracts/ViewModels";
|
||||||
import { PlatformType } from "../../PlatformType";
|
import { PlatformType } from "../../PlatformType";
|
||||||
@@ -1409,7 +1409,7 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
*/
|
*/
|
||||||
public getLabel(): string {
|
public getLabel(): string {
|
||||||
if (this.container.isPreferredApiTable()) {
|
if (this.container.isPreferredApiTable()) {
|
||||||
return "Entitites";
|
return "Entities";
|
||||||
} else if (this.container.isPreferredApiCassandra()) {
|
} else if (this.container.isPreferredApiCassandra()) {
|
||||||
return "Rows";
|
return "Rows";
|
||||||
} else if (this.container.isPreferredApiGraph()) {
|
} else if (this.container.isPreferredApiGraph()) {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import Collection from "./Collection";
|
|||||||
import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||||
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils";
|
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils";
|
||||||
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
|
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
|
||||||
import { Logger } from "../../Common/Logger";
|
import * as Logger from "../../Common/Logger";
|
||||||
|
|
||||||
export default class Database implements ViewModels.Database {
|
export default class Database implements ViewModels.Database {
|
||||||
public nodeKind: string;
|
public nodeKind: string;
|
||||||
|
|||||||
@@ -18,13 +18,14 @@ import FileIcon from "../../../images/notebook/file-cosmos.svg";
|
|||||||
import { ArrayHashMap } from "../../Common/ArrayHashMap";
|
import { ArrayHashMap } from "../../Common/ArrayHashMap";
|
||||||
import { NotebookUtil } from "../Notebook/NotebookUtil";
|
import { NotebookUtil } from "../Notebook/NotebookUtil";
|
||||||
import _ from "underscore";
|
import _ from "underscore";
|
||||||
import { StringUtils } from "../../Utils/StringUtils";
|
import { IPinnedRepo } from "../../Juno/JunoClient";
|
||||||
import { JunoClient, IPinnedRepo } from "../../Juno/JunoClient";
|
|
||||||
import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||||
import { Areas } from "../../Common/Constants";
|
import { Areas } from "../../Common/Constants";
|
||||||
import { GitHubUtils } from "../../Utils/GitHubUtils";
|
import * as GitHubUtils from "../../Utils/GitHubUtils";
|
||||||
import { SamplesRepo, SamplesBranch } from "../Notebook/NotebookSamples";
|
import GalleryIcon from "../../../images/GalleryIcon.svg";
|
||||||
|
import { Callout, Text, Link, DirectionalHint, Stack, ICalloutProps, ILinkProps } from "office-ui-fabric-react";
|
||||||
|
import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility";
|
||||||
|
|
||||||
export class ResourceTreeAdapter implements ReactAdapter {
|
export class ResourceTreeAdapter implements ReactAdapter {
|
||||||
private static readonly DataTitle = "DATA";
|
private static readonly DataTitle = "DATA";
|
||||||
@@ -33,17 +34,15 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
|||||||
|
|
||||||
public parameters: ko.Observable<number>;
|
public parameters: ko.Observable<number>;
|
||||||
|
|
||||||
public sampleNotebooksContentRoot: NotebookContentItem;
|
public galleryContentRoot: NotebookContentItem;
|
||||||
public myNotebooksContentRoot: NotebookContentItem;
|
public myNotebooksContentRoot: NotebookContentItem;
|
||||||
public gitHubNotebooksContentRoot: NotebookContentItem;
|
public gitHubNotebooksContentRoot: NotebookContentItem;
|
||||||
|
|
||||||
private pinnedReposSubscription: ko.Subscription;
|
|
||||||
|
|
||||||
private koSubsDatabaseIdMap: ArrayHashMap<ko.Subscription>; // database id -> ko subs
|
private koSubsDatabaseIdMap: ArrayHashMap<ko.Subscription>; // database id -> ko subs
|
||||||
private koSubsCollectionIdMap: ArrayHashMap<ko.Subscription>; // collection id -> ko subs
|
private koSubsCollectionIdMap: ArrayHashMap<ko.Subscription>; // collection id -> ko subs
|
||||||
private databaseCollectionIdMap: ArrayHashMap<string>; // database id -> collection ids
|
private databaseCollectionIdMap: ArrayHashMap<string>; // database id -> collection ids
|
||||||
|
|
||||||
public constructor(private container: ViewModels.Explorer, private junoClient: JunoClient) {
|
public constructor(private container: ViewModels.Explorer) {
|
||||||
this.parameters = ko.observable(Date.now());
|
this.parameters = ko.observable(Date.now());
|
||||||
|
|
||||||
this.container.selectedNode.subscribe((newValue: any) => this.triggerRender());
|
this.container.selectedNode.subscribe((newValue: any) => this.triggerRender());
|
||||||
@@ -72,14 +71,18 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
|||||||
|
|
||||||
if (this.container.isNotebookEnabled()) {
|
if (this.container.isNotebookEnabled()) {
|
||||||
return (
|
return (
|
||||||
<AccordionComponent>
|
<>
|
||||||
<AccordionItemComponent title={ResourceTreeAdapter.DataTitle} isExpanded={!this.gitHubNotebooksContentRoot}>
|
<AccordionComponent>
|
||||||
<TreeComponent className="dataResourceTree" rootNode={dataRootNode} />
|
<AccordionItemComponent title={ResourceTreeAdapter.DataTitle} isExpanded={!this.gitHubNotebooksContentRoot}>
|
||||||
</AccordionItemComponent>
|
<TreeComponent className="dataResourceTree" rootNode={dataRootNode} />
|
||||||
<AccordionItemComponent title={ResourceTreeAdapter.NotebooksTitle}>
|
</AccordionItemComponent>
|
||||||
<TreeComponent className="notebookResourceTree" rootNode={notebooksRootNode} />
|
<AccordionItemComponent title={ResourceTreeAdapter.NotebooksTitle}>
|
||||||
</AccordionItemComponent>
|
<TreeComponent className="notebookResourceTree" rootNode={notebooksRootNode} />
|
||||||
</AccordionComponent>
|
</AccordionItemComponent>
|
||||||
|
</AccordionComponent>
|
||||||
|
|
||||||
|
{this.galleryContentRoot && this.buildGalleryCallout()}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return <TreeComponent className="dataResourceTree" rootNode={dataRootNode} />;
|
return <TreeComponent className="dataResourceTree" rootNode={dataRootNode} />;
|
||||||
@@ -89,14 +92,11 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
|||||||
public async initialize(): Promise<void[]> {
|
public async initialize(): Promise<void[]> {
|
||||||
const refreshTasks: Promise<void>[] = [];
|
const refreshTasks: Promise<void>[] = [];
|
||||||
|
|
||||||
this.sampleNotebooksContentRoot = {
|
this.galleryContentRoot = {
|
||||||
name: "Sample Notebooks (View Only)",
|
name: "Gallery",
|
||||||
path: GitHubUtils.toContentUri(SamplesRepo.owner, SamplesRepo.name, SamplesBranch.name, ""),
|
path: "Gallery",
|
||||||
type: NotebookContentItemType.Directory
|
type: NotebookContentItemType.File
|
||||||
};
|
};
|
||||||
refreshTasks.push(
|
|
||||||
this.container.refreshContentItem(this.sampleNotebooksContentRoot).then(() => this.triggerRender())
|
|
||||||
);
|
|
||||||
|
|
||||||
this.myNotebooksContentRoot = {
|
this.myNotebooksContentRoot = {
|
||||||
name: "My Notebooks",
|
name: "My Notebooks",
|
||||||
@@ -111,14 +111,12 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.container.gitHubOAuthService?.isLoggedIn()) {
|
if (this.container.notebookManager?.gitHubOAuthService.isLoggedIn()) {
|
||||||
this.gitHubNotebooksContentRoot = {
|
this.gitHubNotebooksContentRoot = {
|
||||||
name: "GitHub repos",
|
name: "GitHub repos",
|
||||||
path: ResourceTreeAdapter.PseudoDirPath,
|
path: ResourceTreeAdapter.PseudoDirPath,
|
||||||
type: NotebookContentItemType.Directory
|
type: NotebookContentItemType.Directory
|
||||||
};
|
};
|
||||||
|
|
||||||
refreshTasks.push(this.refreshGitHubReposAndTriggerRender(this.gitHubNotebooksContentRoot));
|
|
||||||
} else {
|
} else {
|
||||||
this.gitHubNotebooksContentRoot = undefined;
|
this.gitHubNotebooksContentRoot = undefined;
|
||||||
}
|
}
|
||||||
@@ -126,10 +124,10 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
|||||||
return Promise.all(refreshTasks);
|
return Promise.all(refreshTasks);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async refreshGitHubReposAndTriggerRender(item: NotebookContentItem): Promise<void> {
|
public initializeGitHubRepos(pinnedRepos: IPinnedRepo[]): void {
|
||||||
const updateGitHubReposAndRender = (pinnedRepos: IPinnedRepo[]) => {
|
if (this.gitHubNotebooksContentRoot) {
|
||||||
item.children = [];
|
this.gitHubNotebooksContentRoot.children = [];
|
||||||
pinnedRepos.forEach(pinnedRepo => {
|
pinnedRepos?.forEach(pinnedRepo => {
|
||||||
const repoFullName = GitHubUtils.toRepoFullName(pinnedRepo.owner, pinnedRepo.name);
|
const repoFullName = GitHubUtils.toRepoFullName(pinnedRepo.owner, pinnedRepo.name);
|
||||||
const repoTreeItem: NotebookContentItem = {
|
const repoTreeItem: NotebookContentItem = {
|
||||||
name: repoFullName,
|
name: repoFullName,
|
||||||
@@ -146,20 +144,11 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
item.children.push(repoTreeItem);
|
this.gitHubNotebooksContentRoot.children.push(repoTreeItem);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.triggerRender();
|
this.triggerRender();
|
||||||
};
|
|
||||||
|
|
||||||
if (this.pinnedReposSubscription) {
|
|
||||||
this.pinnedReposSubscription.dispose();
|
|
||||||
}
|
}
|
||||||
this.pinnedReposSubscription = this.junoClient.subscribeToPinnedRepos(pinnedRepos =>
|
|
||||||
updateGitHubReposAndRender(pinnedRepos)
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.junoClient.getPinnedRepos(this.container.gitHubOAuthService?.getTokenObservable()()?.scope);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildDataTree(): TreeNode {
|
private buildDataTree(): TreeNode {
|
||||||
@@ -347,12 +336,11 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
|||||||
let notebooksTree: TreeNode = {
|
let notebooksTree: TreeNode = {
|
||||||
label: undefined,
|
label: undefined,
|
||||||
isExpanded: true,
|
isExpanded: true,
|
||||||
isLeavesParentsSeparate: true,
|
|
||||||
children: []
|
children: []
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.sampleNotebooksContentRoot) {
|
if (this.galleryContentRoot) {
|
||||||
notebooksTree.children.push(this.buildSampleNotebooksTree());
|
notebooksTree.children.push(this.buildGalleryNotebooksTree());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.myNotebooksContentRoot) {
|
if (this.myNotebooksContentRoot) {
|
||||||
@@ -368,55 +356,63 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
|||||||
return notebooksTree;
|
return notebooksTree;
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildSampleNotebooksTree(): TreeNode {
|
private buildGalleryCallout(): JSX.Element {
|
||||||
const sampleNotebooksTree: TreeNode = this.buildNotebookDirectoryNode(
|
if (
|
||||||
this.sampleNotebooksContentRoot,
|
LocalStorageUtility.hasItem(StorageKey.GalleryCalloutDismissed) &&
|
||||||
(item: NotebookContentItem) => {
|
LocalStorageUtility.getEntryBoolean(StorageKey.GalleryCalloutDismissed)
|
||||||
const databaseAccountName: string = this.container.databaseAccount() && this.container.databaseAccount().name;
|
) {
|
||||||
const defaultExperience: string = this.container.defaultExperience && this.container.defaultExperience();
|
return undefined;
|
||||||
const dataExplorerArea: string = Areas.ResourceTree;
|
}
|
||||||
|
|
||||||
const startKey: number = TelemetryProcessor.traceStart(Action.OpenSampleNotebook, {
|
const calloutProps: ICalloutProps = {
|
||||||
databaseAccountName,
|
calloutMaxWidth: 350,
|
||||||
defaultExperience,
|
ariaLabel: "New gallery",
|
||||||
dataExplorerArea
|
role: "alertdialog",
|
||||||
});
|
gapSpace: 0,
|
||||||
|
target: ".galleryHeader",
|
||||||
this.container.importAndOpen(item.path).then(hasOpened => {
|
directionalHint: DirectionalHint.leftTopEdge,
|
||||||
if (hasOpened) {
|
onDismiss: () => {
|
||||||
this.pushItemToMostRecent(item);
|
LocalStorageUtility.setEntryBoolean(StorageKey.GalleryCalloutDismissed, true);
|
||||||
TelemetryProcessor.traceSuccess(
|
this.triggerRender();
|
||||||
Action.OpenSampleNotebook,
|
|
||||||
{
|
|
||||||
databaseAccountName,
|
|
||||||
defaultExperience,
|
|
||||||
dataExplorerArea
|
|
||||||
},
|
|
||||||
startKey
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
TelemetryProcessor.traceFailure(
|
|
||||||
Action.OpenSampleNotebook,
|
|
||||||
{
|
|
||||||
databaseAccountName,
|
|
||||||
defaultExperience,
|
|
||||||
dataExplorerArea
|
|
||||||
},
|
|
||||||
startKey
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
false,
|
setInitialFocus: true
|
||||||
false
|
};
|
||||||
);
|
|
||||||
|
|
||||||
sampleNotebooksTree.isExpanded = true;
|
const openGalleryProps: ILinkProps = {
|
||||||
// Remove children starting with "."
|
onClick: () => {
|
||||||
sampleNotebooksTree.children = sampleNotebooksTree.children.filter(
|
LocalStorageUtility.setEntryBoolean(StorageKey.GalleryCalloutDismissed, true);
|
||||||
node => !StringUtils.startsWith(node.label, ".")
|
this.container.openGallery();
|
||||||
|
this.triggerRender();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Callout {...calloutProps}>
|
||||||
|
<Stack tokens={{ childrenGap: 10, padding: 20 }}>
|
||||||
|
<Text variant="xLarge" block>
|
||||||
|
New gallery
|
||||||
|
</Text>
|
||||||
|
<Text block>
|
||||||
|
Sample notebooks are now combined in gallery. View and try out samples provided by Microsoft and other
|
||||||
|
contributors.
|
||||||
|
</Text>
|
||||||
|
<Link {...openGalleryProps}>Open gallery</Link>
|
||||||
|
</Stack>
|
||||||
|
</Callout>
|
||||||
);
|
);
|
||||||
return sampleNotebooksTree;
|
}
|
||||||
|
|
||||||
|
private buildGalleryNotebooksTree(): TreeNode {
|
||||||
|
return {
|
||||||
|
label: "Gallery",
|
||||||
|
iconSrc: GalleryIcon,
|
||||||
|
className: "notebookHeader galleryHeader",
|
||||||
|
onClick: () => this.container.openGallery(),
|
||||||
|
isSelected: () => {
|
||||||
|
const activeTab = this.container.findActiveTab();
|
||||||
|
return activeTab && activeTab.tabKind === ViewModels.CollectionTabKind.Gallery;
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildMyNotebooksTree(): TreeNode {
|
private buildMyNotebooksTree(): TreeNode {
|
||||||
@@ -467,7 +463,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
|||||||
defaultExperience: this.container.defaultExperience && this.container.defaultExperience(),
|
defaultExperience: this.container.defaultExperience && this.container.defaultExperience(),
|
||||||
dataExplorerArea: Areas.Notebook
|
dataExplorerArea: Areas.Notebook
|
||||||
});
|
});
|
||||||
this.container.gitHubOAuthService.logout();
|
this.container.notebookManager?.gitHubOAuthService.logout();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,19 +1,33 @@
|
|||||||
import * as ReactDOM from "react-dom";
|
|
||||||
import "bootstrap/dist/css/bootstrap.css";
|
import "bootstrap/dist/css/bootstrap.css";
|
||||||
import { CosmosClient } from "../Common/CosmosClient";
|
|
||||||
import { GalleryViewerComponent } from "../Explorer/Controls/NotebookGallery/GalleryViewerComponent";
|
|
||||||
import { JunoUtils } from "../Utils/JunoUtils";
|
|
||||||
import { initializeIcons } from "office-ui-fabric-react/lib/Icons";
|
import { initializeIcons } from "office-ui-fabric-react/lib/Icons";
|
||||||
|
import * as React from "react";
|
||||||
|
import * as ReactDOM from "react-dom";
|
||||||
|
import { initializeConfiguration } from "../Config";
|
||||||
|
import {
|
||||||
|
GalleryTab,
|
||||||
|
GalleryViewerComponent,
|
||||||
|
GalleryViewerComponentProps,
|
||||||
|
SortBy
|
||||||
|
} from "../Explorer/Controls/NotebookGallery/GalleryViewerComponent";
|
||||||
|
import { JunoClient } from "../Juno/JunoClient";
|
||||||
|
import * as GalleryUtils from "../Utils/GalleryUtils";
|
||||||
|
|
||||||
const onInit = async () => {
|
const onInit = async () => {
|
||||||
initializeIcons();
|
initializeIcons();
|
||||||
const officialSamplesData = await JunoUtils.getOfficialSampleNotebooks(CosmosClient.authorizationToken());
|
await initializeConfiguration();
|
||||||
const galleryViewerComponent = new GalleryViewerComponent({
|
const galleryViewerProps = GalleryUtils.getGalleryViewerProps(window.location.search);
|
||||||
officialSamplesData: officialSamplesData,
|
|
||||||
likedNotebookData: undefined,
|
const props: GalleryViewerComponentProps = {
|
||||||
container: undefined
|
junoClient: new JunoClient(),
|
||||||
});
|
selectedTab: galleryViewerProps.selectedTab || GalleryTab.OfficialSamples,
|
||||||
ReactDOM.render(galleryViewerComponent.render(), document.getElementById("galleryContent"));
|
sortBy: galleryViewerProps.sortBy || SortBy.MostViewed,
|
||||||
|
searchText: galleryViewerProps.searchText,
|
||||||
|
onSelectedTabChange: undefined,
|
||||||
|
onSortByChange: undefined,
|
||||||
|
onSearchTextChange: undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
ReactDOM.render(<GalleryViewerComponent {...props} />, document.getElementById("galleryContent"));
|
||||||
};
|
};
|
||||||
|
|
||||||
// Entry point
|
// Entry point
|
||||||
|
|||||||
@@ -8,6 +8,6 @@
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="galleryComponentContainer" id="galleryContent"></div>
|
<div class="galleryContent" id="galleryContent"></div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,110 +0,0 @@
|
|||||||
import { HttpStatusCodes } from "../Common/Constants";
|
|
||||||
import { GitHubClient, IGitHubFile } from "./GitHubClient";
|
|
||||||
import { SamplesRepo, SamplesBranch, SamplesContentsQueryResponse } from "../Explorer/Notebook/NotebookSamples";
|
|
||||||
|
|
||||||
const invalidTokenCallback = jest.fn();
|
|
||||||
// Use a dummy token to get around API rate limit (something which doesn't affect the API quota for AZURESAMPLESCOSMOSDBPAT in Config.ts)
|
|
||||||
const gitHubClient = new GitHubClient("cd1906b9534362fab6ce45d6db6c76b59e55bc50", invalidTokenCallback);
|
|
||||||
|
|
||||||
const validateGitHubFile = (file: IGitHubFile) => {
|
|
||||||
expect(file.branch).toEqual(SamplesBranch);
|
|
||||||
expect(file.commit).toBeDefined();
|
|
||||||
expect(file.name).toBeDefined();
|
|
||||||
expect(file.path).toBeDefined();
|
|
||||||
expect(file.repo).toEqual(SamplesRepo);
|
|
||||||
expect(file.type).toBeDefined();
|
|
||||||
|
|
||||||
switch (file.type) {
|
|
||||||
case "blob":
|
|
||||||
expect(file.sha).toBeDefined();
|
|
||||||
expect(file.size).toBeDefined();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "tree":
|
|
||||||
expect(file.sha).toBeUndefined();
|
|
||||||
expect(file.size).toBeUndefined();
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
throw new Error(`Unsupported github file type: ${file.type}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
describe("GitHubClient", () => {
|
|
||||||
it("getRepoAsync returns valid repo", async () => {
|
|
||||||
const response = await gitHubClient.getRepoAsync(SamplesRepo.owner, SamplesRepo.name);
|
|
||||||
expect(response).toEqual({
|
|
||||||
status: HttpStatusCodes.OK,
|
|
||||||
data: SamplesRepo
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("getReposAsync returns repos for authenticated user", async () => {
|
|
||||||
const response = await gitHubClient.getReposAsync(1);
|
|
||||||
expect(response.status).toBe(HttpStatusCodes.OK);
|
|
||||||
expect(response.data).toBeDefined();
|
|
||||||
expect(response.data.length).toBe(1);
|
|
||||||
expect(response.pageInfo).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("getBranchesAsync returns branches for a repo", async () => {
|
|
||||||
const response = await gitHubClient.getBranchesAsync(SamplesRepo.owner, SamplesRepo.name, 1);
|
|
||||||
expect(response.status).toBe(HttpStatusCodes.OK);
|
|
||||||
expect(response.data).toEqual([SamplesBranch]);
|
|
||||||
expect(response.pageInfo).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("getContentsAsync returns files in the repo", async () => {
|
|
||||||
const response = await gitHubClient.getContentsAsync(SamplesRepo.owner, SamplesRepo.name, SamplesBranch.name);
|
|
||||||
expect(response.status).toBe(HttpStatusCodes.OK);
|
|
||||||
expect(response.data).toBeDefined();
|
|
||||||
|
|
||||||
const data = response.data as IGitHubFile[];
|
|
||||||
expect(data.length).toBeGreaterThan(0);
|
|
||||||
data.forEach(content => validateGitHubFile(content));
|
|
||||||
});
|
|
||||||
|
|
||||||
it("getContentsAsync returns files in a dir", async () => {
|
|
||||||
const samplesDir = SamplesContentsQueryResponse.repository.object.entries.find(file => file.type === "tree");
|
|
||||||
const response = await gitHubClient.getContentsAsync(
|
|
||||||
SamplesRepo.owner,
|
|
||||||
SamplesRepo.name,
|
|
||||||
SamplesBranch.name,
|
|
||||||
samplesDir.name
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(response.status).toBe(HttpStatusCodes.OK);
|
|
||||||
expect(response.data).toBeDefined();
|
|
||||||
|
|
||||||
const data = response.data as IGitHubFile[];
|
|
||||||
expect(data.length).toBeGreaterThan(0);
|
|
||||||
data.forEach(content => validateGitHubFile(content));
|
|
||||||
});
|
|
||||||
|
|
||||||
it("getContentsAsync returns a file", async () => {
|
|
||||||
const samplesFile = SamplesContentsQueryResponse.repository.object.entries.find(file => file.type === "blob");
|
|
||||||
const response = await gitHubClient.getContentsAsync(
|
|
||||||
SamplesRepo.owner,
|
|
||||||
SamplesRepo.name,
|
|
||||||
SamplesBranch.name,
|
|
||||||
samplesFile.name
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(response.status).toBe(HttpStatusCodes.OK);
|
|
||||||
expect(response.data).toBeDefined();
|
|
||||||
|
|
||||||
const file = response.data as IGitHubFile;
|
|
||||||
expect(file.type).toBe("blob");
|
|
||||||
validateGitHubFile(file);
|
|
||||||
expect(file.content).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("getBlobAsync returns file content", async () => {
|
|
||||||
const samplesFile = SamplesContentsQueryResponse.repository.object.entries.find(file => file.type === "blob");
|
|
||||||
const response = await gitHubClient.getBlobAsync(SamplesRepo.owner, SamplesRepo.name, samplesFile.object.oid);
|
|
||||||
|
|
||||||
expect(response.status).toBe(HttpStatusCodes.OK);
|
|
||||||
expect(response.data).toBeDefined();
|
|
||||||
expect(typeof response.data).toBe("string");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
import { Octokit } from "@octokit/rest";
|
import { Octokit } from "@octokit/rest";
|
||||||
import { HttpStatusCodes } from "../Common/Constants";
|
import { HttpStatusCodes } from "../Common/Constants";
|
||||||
import { Logger } from "../Common/Logger";
|
import * as Logger from "../Common/Logger";
|
||||||
import UrlUtility from "../Common/UrlUtility";
|
import UrlUtility from "../Common/UrlUtility";
|
||||||
import { isSamplesCall, SamplesContentsQueryResponse } from "../Explorer/Notebook/NotebookSamples";
|
|
||||||
import { NotebookUtil } from "../Explorer/Notebook/NotebookUtil";
|
import { NotebookUtil } from "../Explorer/Notebook/NotebookUtil";
|
||||||
|
|
||||||
export interface IGitHubPageInfo {
|
export interface IGitHubPageInfo {
|
||||||
@@ -225,8 +224,8 @@ export class GitHubClient {
|
|||||||
private static readonly SelfErrorCode = 599;
|
private static readonly SelfErrorCode = 599;
|
||||||
private ocktokit: Octokit;
|
private ocktokit: Octokit;
|
||||||
|
|
||||||
constructor(token: string, private errorCallback: (error: any) => void) {
|
constructor(private errorCallback: (error: any) => void) {
|
||||||
this.initOctokit(token);
|
this.initOctokit();
|
||||||
}
|
}
|
||||||
|
|
||||||
public setToken(token: string): void {
|
public setToken(token: string): void {
|
||||||
@@ -310,18 +309,13 @@ export class GitHubClient {
|
|||||||
path?: string
|
path?: string
|
||||||
): Promise<IGitHubResponse<IGitHubFile | IGitHubFile[]>> {
|
): Promise<IGitHubResponse<IGitHubFile | IGitHubFile[]>> {
|
||||||
try {
|
try {
|
||||||
let response: ContentsQueryResponse;
|
const response = (await this.ocktokit.graphql(contentsQuery, {
|
||||||
if (isSamplesCall(owner, repo, branch) && !path) {
|
owner,
|
||||||
response = SamplesContentsQueryResponse;
|
repo,
|
||||||
} else {
|
ref: `refs/heads/${branch}`,
|
||||||
response = (await this.ocktokit.graphql(contentsQuery, {
|
path: path || undefined,
|
||||||
owner,
|
objectExpression: `refs/heads/${branch}:${path || ""}`
|
||||||
repo,
|
} as ContentsQueryParams)) as ContentsQueryResponse;
|
||||||
ref: `refs/heads/${branch}`,
|
|
||||||
path: path || undefined,
|
|
||||||
objectExpression: `refs/heads/${branch}:${path || ""}`
|
|
||||||
} as ContentsQueryParams)) as ContentsQueryResponse;
|
|
||||||
}
|
|
||||||
|
|
||||||
let data: IGitHubFile | IGitHubFile[];
|
let data: IGitHubFile | IGitHubFile[];
|
||||||
const entries = response.repository.object.entries;
|
const entries = response.repository.object.entries;
|
||||||
@@ -495,7 +489,7 @@ export class GitHubClient {
|
|||||||
return { status: response.status, data: <string>(<unknown>response.data) };
|
return { status: response.status, data: <string>(<unknown>response.data) };
|
||||||
}
|
}
|
||||||
|
|
||||||
private async initOctokit(token: string) {
|
private async initOctokit(token?: string) {
|
||||||
this.ocktokit = new Octokit({
|
this.ocktokit = new Octokit({
|
||||||
auth: token,
|
auth: token,
|
||||||
log: {
|
log: {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user