Merge branch 'master' into locfiles/486ca1c2-3f14-4217-8aae-5aae878d249c

This commit is contained in:
sunghyunkang1111
2026-08-28 10:25:33 -05:00
committed by GitHub
234 changed files with 2909 additions and 22557 deletions
+53 -123
View File
@@ -20,10 +20,10 @@ jobs:
if: github.ref == 'refs/heads/master'
steps:
- uses: actions/checkout@v4
- name: Use Node.js 18.x
- name: Use Node.js 22.x
uses: actions/setup-node@v4
with:
node-version: 18.x
node-version: 22.x
- run: npm ci
- run: node utils/codeMetrics.js
env:
@@ -33,10 +33,10 @@ jobs:
name: "Compile TypeScript"
steps:
- uses: actions/checkout@v4
- name: Use Node.js 18.x
- name: Use Node.js 22.x
uses: actions/setup-node@v4
with:
node-version: 18.x
node-version: 22.x
- run: npm ci
- run: npm run compile
- run: npm run compile:strict
@@ -45,10 +45,10 @@ jobs:
name: "Check Format"
steps:
- uses: actions/checkout@v4
- name: Use Node.js 18.x
- name: Use Node.js 22.x
uses: actions/setup-node@v4
with:
node-version: 18.x
node-version: 22.x
- run: npm ci
- run: npm run format:check
lint:
@@ -56,10 +56,10 @@ jobs:
name: "Lint"
steps:
- uses: actions/checkout@v4
- name: Use Node.js 18.x
- name: Use Node.js 22.x
uses: actions/setup-node@v4
with:
node-version: 18.x
node-version: 22.x
- run: npm ci
- run: npm run lint
unittest:
@@ -67,10 +67,10 @@ jobs:
name: "Unit Tests"
steps:
- uses: actions/checkout@v4
- name: Use Node.js 18.x
- name: Use Node.js 22.x
uses: actions/setup-node@v4
with:
node-version: 18.x
node-version: 22.x
- run: npm ci
- run: npm run test
build:
@@ -78,10 +78,10 @@ jobs:
name: "Build"
steps:
- uses: actions/checkout@v4
- name: Use Node.js 18.x
- name: Use Node.js 22.x
uses: actions/setup-node@v4
with:
node-version: 18.x
node-version: 22.x
- run: npm ci
- run: npm run build:contracts
- name: Restore Build Cache
@@ -101,13 +101,13 @@ jobs:
- name: "Az CLI login"
uses: azure/login@v1
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.PREVIEW_SUBSCRIPTION_ID }}
client-id: ${{ secrets.E2ETESTS_CLIENT_ID }}
tenant-id: ${{ secrets.E2ETESTS_TENANT_ID }}
subscription-id: ${{ secrets.E2ETESTS_SUBSCRIPTION_ID }}
- name: Upload build to preview blob storage
run: az storage blob upload-batch -d '$web' -s 'dist' --account-name ${{ secrets.PREVIEW_STORAGE_ACCOUNT_NAME }} --destination-path "${{github.event.pull_request.head.sha || github.sha}}" --auth-mode login --overwrite true
run: az storage blob upload-batch -d '$web' -s 'dist' --account-name ${{ secrets.E2ETESTS_STORAGEACCOUNT_NAME }} --destination-path "${{github.event.pull_request.head.sha || github.sha}}" --auth-mode login --overwrite true
- name: Upload preview config to blob storage
run: az storage blob upload -c '$web' -f ./preview/config.json --account-name ${{ secrets.PREVIEW_STORAGE_ACCOUNT_NAME }} --name "${{github.event.pull_request.head.sha || github.sha}}/config.json" --auth-mode login --overwrite true
run: az storage blob upload -c '$web' -f ./preview/config.json --account-name ${{ secrets.E2ETESTS_STORAGEACCOUNT_NAME }} --name "${{github.event.pull_request.head.sha || github.sha}}/config.json" --auth-mode login --overwrite true
nuget:
name: Publish Nuget
if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')
@@ -162,7 +162,10 @@ jobs:
runs-on: ubuntu-latest
env:
NODE_TLS_REJECT_UNAUTHORIZED: 0
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
AZURE_SUBSCRIPTION_ID: ${{ secrets.E2ETESTS_SUBSCRIPTION_ID }}
AZURE_TENANT_ID: ${{ secrets.E2ETESTS_TENANT_ID }}
DE_ACCOUNT_PREFIX: ${{ secrets.E2ETESTS_ACCOUNT_PREFIX }}
DE_TEST_RESOURCE_GROUP: ${{ secrets.E2ETESTS_RESOURCEGROUP_NAME }}
strategy:
fail-fast: false
matrix:
@@ -170,129 +173,56 @@ jobs:
shardTotal: [20]
steps:
- uses: actions/checkout@v4
- name: Use Node.js 18.x
- name: Use Node.js 22.x
uses: actions/setup-node@v4
with:
node-version: 18.x
node-version: 22.x
- run: npm ci
- run: npx playwright install --with-deps
- name: "Az CLI login"
uses: Azure/login@v2
with:
client-id: ${{ secrets.E2E_TESTS_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
client-id: ${{ secrets.E2ETESTS_CLIENT_ID }}
tenant-id: ${{ secrets.E2ETESTS_TENANT_ID }}
subscription-id: ${{ secrets.E2ETESTS_SUBSCRIPTION_ID }}
# We can't use MSAL within playwright so we acquire tokens prior to running the tests
- name: "Acquire RBAC tokens for test accounts"
uses: azure/cli@v2
env:
DE_ACCOUNT_PREFIX: ${{ secrets.E2ETESTS_ACCOUNT_PREFIX }}
DE_TEST_RESOURCE_GROUP: ${{ secrets.E2ETESTS_RESOURCEGROUP_NAME }}
with:
azcliversion: latest
inlineScript: |
SHARD_INDEX=${{ matrix.shardIndex }}
echo PLAYWRIGHT_SHARD_INDEX=$SHARD_INDEX >> $GITHUB_ENV
NOSQL_TESTACCOUNT_1_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql-1.documents.azure.com/.default" -o tsv --query accessToken)
echo "::add-mask::$NOSQL_TESTACCOUNT_1_TOKEN"
echo NOSQL_TESTACCOUNT_1_TOKEN=$NOSQL_TESTACCOUNT_1_TOKEN >> $GITHUB_ENV
NOSQL_TESTACCOUNT_2_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql-2.documents.azure.com/.default" -o tsv --query accessToken)
echo "::add-mask::$NOSQL_TESTACCOUNT_2_TOKEN"
echo NOSQL_TESTACCOUNT_2_TOKEN=$NOSQL_TESTACCOUNT_2_TOKEN >> $GITHUB_ENV
NOSQL_TESTACCOUNT_3_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql-3.documents.azure.com/.default" -o tsv --query accessToken)
echo "::add-mask::$NOSQL_TESTACCOUNT_3_TOKEN"
echo NOSQL_TESTACCOUNT_3_TOKEN=$NOSQL_TESTACCOUNT_3_TOKEN >> $GITHUB_ENV
NOSQL_TESTACCOUNT_4_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql-4.documents.azure.com/.default" -o tsv --query accessToken)
echo "::add-mask::$NOSQL_TESTACCOUNT_4_TOKEN"
echo NOSQL_TESTACCOUNT_4_TOKEN=$NOSQL_TESTACCOUNT_4_TOKEN >> $GITHUB_ENV
NOSQL_TESTACCOUNT_5_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql-5.documents.azure.com/.default" -o tsv --query accessToken)
echo "::add-mask::$NOSQL_TESTACCOUNT_5_TOKEN"
echo NOSQL_TESTACCOUNT_5_TOKEN=$NOSQL_TESTACCOUNT_5_TOKEN >> $GITHUB_ENV
NOSQL_TESTACCOUNT_6_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql-6.documents.azure.com/.default" -o tsv --query accessToken)
echo "::add-mask::$NOSQL_TESTACCOUNT_6_TOKEN"
echo NOSQL_TESTACCOUNT_6_TOKEN=$NOSQL_TESTACCOUNT_6_TOKEN >> $GITHUB_ENV
NOSQL_TESTACCOUNT_7_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql-7.documents.azure.com/.default" -o tsv --query accessToken)
echo "::add-mask::$NOSQL_TESTACCOUNT_7_TOKEN"
echo NOSQL_TESTACCOUNT_7_TOKEN=$NOSQL_TESTACCOUNT_7_TOKEN >> $GITHUB_ENV
NOSQL_TESTACCOUNT_8_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql-8.documents.azure.com/.default" -o tsv --query accessToken)
echo "::add-mask::$NOSQL_TESTACCOUNT_8_TOKEN"
echo NOSQL_TESTACCOUNT_8_TOKEN=$NOSQL_TESTACCOUNT_8_TOKEN >> $GITHUB_ENV
NOSQL_TESTACCOUNT_9_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql-9.documents.azure.com/.default" -o tsv --query accessToken)
echo "::add-mask::$NOSQL_TESTACCOUNT_9_TOKEN"
echo NOSQL_TESTACCOUNT_9_TOKEN=$NOSQL_TESTACCOUNT_9_TOKEN >> $GITHUB_ENV
NOSQL_TESTACCOUNT_10_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql-10.documents.azure.com/.default" -o tsv --query accessToken)
echo "::add-mask::$NOSQL_TESTACCOUNT_10_TOKEN"
echo NOSQL_TESTACCOUNT_10_TOKEN=$NOSQL_TESTACCOUNT_10_TOKEN >> $GITHUB_ENV
NOSQL_TESTACCOUNT_11_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql-11.documents.azure.com/.default" -o tsv --query accessToken)
echo "::add-mask::$NOSQL_TESTACCOUNT_11_TOKEN"
echo NOSQL_TESTACCOUNT_11_TOKEN=$NOSQL_TESTACCOUNT_11_TOKEN >> $GITHUB_ENV
NOSQL_TESTACCOUNT_12_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql-12.documents.azure.com/.default" -o tsv --query accessToken)
echo "::add-mask::$NOSQL_TESTACCOUNT_12_TOKEN"
echo NOSQL_TESTACCOUNT_12_TOKEN=$NOSQL_TESTACCOUNT_12_TOKEN >> $GITHUB_ENV
NOSQL_TESTACCOUNT_13_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql-13.documents.azure.com/.default" -o tsv --query accessToken)
echo "::add-mask::$NOSQL_TESTACCOUNT_13_TOKEN"
echo NOSQL_TESTACCOUNT_13_TOKEN=$NOSQL_TESTACCOUNT_13_TOKEN >> $GITHUB_ENV
NOSQL_TESTACCOUNT_14_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql-14.documents.azure.com/.default" -o tsv --query accessToken)
echo "::add-mask::$NOSQL_TESTACCOUNT_14_TOKEN"
echo NOSQL_TESTACCOUNT_14_TOKEN=$NOSQL_TESTACCOUNT_14_TOKEN >> $GITHUB_ENV
NOSQL_TESTACCOUNT_15_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql-15.documents.azure.com/.default" -o tsv --query accessToken)
echo "::add-mask::$NOSQL_TESTACCOUNT_15_TOKEN"
echo NOSQL_TESTACCOUNT_15_TOKEN=$NOSQL_TESTACCOUNT_15_TOKEN >> $GITHUB_ENV
NOSQL_TESTACCOUNT_16_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql-16.documents.azure.com/.default" -o tsv --query accessToken)
echo "::add-mask::$NOSQL_TESTACCOUNT_16_TOKEN"
echo NOSQL_TESTACCOUNT_16_TOKEN=$NOSQL_TESTACCOUNT_16_TOKEN >> $GITHUB_ENV
NOSQL_TESTACCOUNT_17_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql-17.documents.azure.com/.default" -o tsv --query accessToken)
echo "::add-mask::$NOSQL_TESTACCOUNT_17_TOKEN"
echo NOSQL_TESTACCOUNT_17_TOKEN=$NOSQL_TESTACCOUNT_17_TOKEN >> $GITHUB_ENV
NOSQL_TESTACCOUNT_18_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql-18.documents.azure.com/.default" -o tsv --query accessToken)
echo "::add-mask::$NOSQL_TESTACCOUNT_18_TOKEN"
echo NOSQL_TESTACCOUNT_18_TOKEN=$NOSQL_TESTACCOUNT_18_TOKEN >> $GITHUB_ENV
NOSQL_TESTACCOUNT_19_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql-19.documents.azure.com/.default" -o tsv --query accessToken)
echo "::add-mask::$NOSQL_TESTACCOUNT_19_TOKEN"
echo NOSQL_TESTACCOUNT_19_TOKEN=$NOSQL_TESTACCOUNT_19_TOKEN >> $GITHUB_ENV
NOSQL_TESTACCOUNT_20_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql-20.documents.azure.com/.default" -o tsv --query accessToken)
echo "::add-mask::$NOSQL_TESTACCOUNT_20_TOKEN"
echo NOSQL_TESTACCOUNT_20_TOKEN=$NOSQL_TESTACCOUNT_20_TOKEN >> $GITHUB_ENV
NOSQL_READONLY_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql-readonly.documents.azure.com/.default" -o tsv --query accessToken)
NOSQL_TESTACCOUNT=${DE_ACCOUNT_PREFIX}-de-test-sql-${SHARD_INDEX}.documents.azure.com
NOSQL_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://$NOSQL_TESTACCOUNT/.default" -o tsv --query accessToken)
echo "::add-mask::$NOSQL_TESTACCOUNT_TOKEN"
echo NOSQL_TESTACCOUNT_TOKEN=$NOSQL_TESTACCOUNT_TOKEN >> $GITHUB_ENV
NOSQL_READONLY_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://$DE_ACCOUNT_PREFIX-de-test-sql-readonly.documents.azure.com/.default" -o tsv --query accessToken)
echo "::add-mask::$NOSQL_READONLY_TESTACCOUNT_TOKEN"
echo NOSQL_READONLY_TESTACCOUNT_TOKEN=$NOSQL_READONLY_TESTACCOUNT_TOKEN >> $GITHUB_ENV
NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql-containercopyonly.documents.azure.com/.default" -o tsv --query accessToken)
NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://${DE_ACCOUNT_PREFIX}-de-test-sql-containercopy.documents.azure.com/.default" -o tsv --query accessToken)
echo "::add-mask::$NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN"
echo NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN=$NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN >> $GITHUB_ENV
TABLE_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-tables.documents.azure.com/.default" -o tsv --query accessToken)
TABLE_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://${DE_ACCOUNT_PREFIX}-de-test-table-1.documents.azure.com/.default" -o tsv --query accessToken)
echo "::add-mask::$TABLE_TESTACCOUNT_TOKEN"
echo TABLE_TESTACCOUNT_TOKEN=$TABLE_TESTACCOUNT_TOKEN >> $GITHUB_ENV
GREMLIN_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-gremlin.documents.azure.com/.default" -o tsv --query accessToken)
GREMLIN_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://${DE_ACCOUNT_PREFIX}-de-test-gremlin-1.documents.azure.com/.default" -o tsv --query accessToken)
echo "::add-mask::$GREMLIN_TESTACCOUNT_TOKEN"
echo GREMLIN_TESTACCOUNT_TOKEN=$GREMLIN_TESTACCOUNT_TOKEN >> $GITHUB_ENV
CASSANDRA_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-cassandra.documents.azure.com/.default" -o tsv --query accessToken)
CASSANDRA_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://${DE_ACCOUNT_PREFIX}-de-test-cassandra-1.documents.azure.com/.default" -o tsv --query accessToken)
echo "::add-mask::$CASSANDRA_TESTACCOUNT_TOKEN"
echo CASSANDRA_TESTACCOUNT_TOKEN=$CASSANDRA_TESTACCOUNT_TOKEN >> $GITHUB_ENV
MONGO_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-mongo.documents.azure.com/.default" -o tsv --query accessToken)
MONGO_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://${DE_ACCOUNT_PREFIX}-de-test-mongo-1.documents.azure.com/.default" -o tsv --query accessToken)
echo "::add-mask::$MONGO_TESTACCOUNT_TOKEN"
echo MONGO_TESTACCOUNT_TOKEN=$MONGO_TESTACCOUNT_TOKEN >> $GITHUB_ENV
MONGO32_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-mongo32.documents.azure.com/.default" -o tsv --query accessToken)
MONGO32_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://${DE_ACCOUNT_PREFIX}-de-test-mongo32-1.documents.azure.com/.default" -o tsv --query accessToken)
echo "::add-mask::$MONGO32_TESTACCOUNT_TOKEN"
echo MONGO32_TESTACCOUNT_TOKEN=$MONGO32_TESTACCOUNT_TOKEN >> $GITHUB_ENV
MONGO_READONLY_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-mongo-readonly.documents.azure.com/.default" -o tsv --query accessToken)
MONGO_READONLY_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://${DE_ACCOUNT_PREFIX}-de-test-mongo-readonly.documents.azure.com/.default" -o tsv --query accessToken)
echo "::add-mask::$MONGO_READONLY_TESTACCOUNT_TOKEN"
echo MONGO_READONLY_TESTACCOUNT_TOKEN=$MONGO_READONLY_TESTACCOUNT_TOKEN >> $GITHUB_ENV
- name: List test files for shard ${{ matrix['shardIndex'] }} of ${{ matrix['shardTotal']}}
@@ -303,9 +233,9 @@ jobs:
if: ${{ !cancelled() }}
uses: Azure/login@v2
with:
client-id: ${{ secrets.E2E_TESTS_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
client-id: ${{ secrets.E2ETESTS_CLIENT_ID }}
tenant-id: ${{ secrets.E2ETESTS_TENANT_ID }}
subscription-id: ${{ secrets.E2ETESTS_SUBSCRIPTION_ID }}
- name: Upload shard blob-report to Azure Storage
if: ${{ !cancelled() }}
env:
@@ -313,7 +243,7 @@ jobs:
SHARD: ${{ matrix.shardIndex }}
run: |
az storage blob upload-batch \
--account-name ${{ secrets.PREVIEW_STORAGE_ACCOUNT_NAME }} \
--account-name ${{ secrets.E2ETESTS_STORAGEACCOUNT_NAME }} \
--auth-mode login \
-d playwright-reports \
-s blob-report \
@@ -335,16 +265,16 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 18
node-version: 22.x
- name: Install dependencies
run: npm ci
- name: "Az CLI login"
uses: Azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.PREVIEW_SUBSCRIPTION_ID }}
client-id: ${{ secrets.E2ETESTS_CLIENT_ID }}
tenant-id: ${{ secrets.E2ETESTS_TENANT_ID }}
subscription-id: ${{ secrets.E2ETESTS_SUBSCRIPTION_ID }}
- name: Download all shard reports from Azure Storage
env:
@@ -352,7 +282,7 @@ jobs:
run: |
mkdir -p all-blob-reports
az storage blob download-batch \
--account-name ${{ secrets.PREVIEW_STORAGE_ACCOUNT_NAME }} \
--account-name ${{ secrets.E2ETESTS_STORAGEACCOUNT_NAME }} \
--auth-mode login \
-s playwright-reports \
--pattern "${KEY}/shards/*" \
@@ -375,7 +305,7 @@ jobs:
KEY: ${{ github.run_id }}-${{ github.run_attempt }}
run: |
az storage blob upload \
--account-name ${{ secrets.PREVIEW_STORAGE_ACCOUNT_NAME }} \
--account-name ${{ secrets.E2ETESTS_STORAGEACCOUNT_NAME }} \
--auth-mode login \
-c playwright-reports \
-n "${KEY}/report.zip" \
@@ -388,7 +318,7 @@ jobs:
KEY: ${{ github.run_id }}-${{ github.run_attempt }}
run: |
az storage blob delete-batch \
--account-name ${{ secrets.PREVIEW_STORAGE_ACCOUNT_NAME }} \
--account-name ${{ secrets.E2ETESTS_STORAGEACCOUNT_NAME }} \
--auth-mode login \
-s playwright-reports \
--pattern "${KEY}/shards/*"
@@ -398,7 +328,7 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR: ${{ github.event.pull_request.number }}
SA_ID: /subscriptions/${{ secrets.PREVIEW_SUBSCRIPTION_ID }}/resourceGroups/dataexplorer-preview/providers/Microsoft.Storage/storageAccounts/dataexplorerpreview
SA_ID: /subscriptions/${{ secrets.E2ETESTS_SUBSCRIPTION_ID }}/resourceGroups/${{ secrets.E2ETESTS_RESOURCEGROUP_NAME }}/providers/Microsoft.Storage/storageAccounts/${{ secrets.E2ETESTS_STORAGEACCOUNT_NAME }}
KEY: ${{ github.run_id }}-${{ github.run_attempt }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
+7 -7
View File
@@ -20,20 +20,20 @@ jobs:
name: "Cleanup Test Database Accounts"
runs-on: ubuntu-latest
env:
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
AZURE_SUBSCRIPTION_ID: ${{ secrets.E2ETESTS_SUBSCRIPTION_ID }}
E2ETESTS_RESOURCEGROUP_NAME : ${{ secrets.E2ETESTS_RESOURCEGROUP_NAME }}
steps:
- uses: actions/checkout@v4
- name: "Az CLI login"
uses: azure/login@v2
with:
client-id: ${{ secrets.E2E_TESTS_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
client-id: ${{ secrets.E2ETESTS_CLIENT_ID }}
tenant-id: ${{ secrets.E2ETESTS_TENANT_ID }}
subscription-id: ${{ secrets.E2ETESTS_SUBSCRIPTION_ID }}
- name: Use Node.js 20.x
- name: Use Node.js 22.x
uses: actions/setup-node@v4
with:
node-version: 20.x
node-version: 22.x
- run: npm ci
- run: node utils/cleanupDBs.js
+1
View File
@@ -6,6 +6,7 @@ UI for Azure Cosmos DB. Powers the [Azure Portal](https://portal.azure.com/), ht
## Getting Started
- Install Node.js 22.x.
- `npm install`
- `npm run build`
+319
View File
@@ -0,0 +1,319 @@
# Implementation Plan: Remove Phoenix & Notebooks
## Problem statement
Cosmos Explorer contains a large, deeply-integrated **notebooks** feature backed by
the **Phoenix** compute-container service and the **Juno** service, plus a **GitHub**
integration used to pin/browse notebook repositories. This functionality is being
retired. The goal is to remove all notebooks/Phoenix/Juno/GitHub-for-notebooks code,
dependencies, UI surfaces, telemetry, and configuration, while **preserving the
database shell terminals** (Mongo / Cassandra / Postgres / VCoreMongo / Cosmos DB NoSQL), which today
share the Terminal infrastructure with notebooks.
This document is the implementation plan only. No code changes are made here.
## Scope decisions (confirmed)
- **Database shells**: Keep the Mongo/Cassandra/Postgres/VCoreMongo/Cosmos DB NoSQL shells, but migrate
them to use the **CloudShell** path exclusively. Remove the legacy Phoenix
notebook-server shell path.
- **GitHub integration**: Remove entirely (it exists only for notebook pinned repos).
- **Schema Analyzer** (`src/Explorer/Notebook/SchemaAnalyzer`): Remove.
- **Phasing**: Every phase must leave the app in a **buildable, shippable** state
(build + lint + strict compile + unit tests green; shells still work).
- **Localization**: Remove notebook/GitHub strings from **all** resource files —
`src/Localization/en/Resources.json` **and** every non-English locale
(`src/Localization/<locale>/Resources.json`). (This deletion is an exception to the
usual convention of editing only the English file.)
## Prior art / related commits
This is a continuation of an in-progress removal effort. Reference commits:
- `7295d63a` — Remove gallery.html and all associated gallery functionality (#2474)
- `a36467f4` — Remove Phoenix `getDbAccountAllowedStatus`; `isPhoenixNotebooks`/
`isPhoenixFeatures` now always `false` (#2472)
- `31385950` — removed NotebookViewer file (#2281)
> **Note:** An unmerged branch `users/jawelton/remove-notebooks-terminal-052126`
> already contains related work (`5989c77c` "Remove terminal.html webpack entry point
> and notebooks terminal code", `c7f9d7e3` "Switch VCore Mongo quickstart to use
> CloudShell terminal"). These are **not** in `master`. Reconcile with that branch
> before/while starting Phase 1 to avoid duplicate or conflicting work.
## Current-state survey (what exists today)
**Core directories / files**
- `src/Phoenix/PhoenixClient.ts` — container allocation, heartbeat, status polling.
- `src/Juno/JunoClient.ts` (+ test) — pinned-repo / notebook metadata service client.
- `src/Explorer/Notebook/` — the bulk of the feature:
- `useNotebook.ts` (Zustand store), `NotebookManager.tsx`, `NotebookContentClient.ts`,
`NotebookClientV2.ts`, `NotebookContainerClient.ts`, `NotebookContentItem.ts`,
`NotebookUtil.ts`, `NTeractUtil.ts`, `FileSystemUtil.ts`
- `NotebookComponent/` (nteract redux store, epics, reducers, content providers)
- `NotebookRenderer/` (nteract cell rendering, decorators, outputs)
- `SchemaAnalyzer/`, `SecurityWarningBar/`
- `src/Explorer/Controls/Notebook/NotebookTerminalComponent.tsx` (+ less/test)
- `src/Explorer/Controls/NotebookViewer/` — read-only viewer + metadata.
- `src/Explorer/Tabs/NotebookV2Tab.ts`, `NotebookTabBase.ts`, `SchemaAnalyzerTab.ts`
- `src/Explorer/Panes/CopyNotebookPane/`
- `src/Explorer/Tabs/ShellAdapters/NotebookTerminalComponentAdapter.tsx`
- `src/CellOutputViewer/` — webpack entry `cellOutputViewer`.
- `src/Utils/NotebookConfigurationUtils.ts`, `src/hooks/useNotebookSnapshotStore.ts`
- `src/Utils/arm/generatedClients/cosmosNotebooks/` — generated ARM client.
**GitHub integration (notebook-only)**
- `src/GitHub/` (`GitHubClient.ts`, `GitHubContentProvider.ts`, `GitHubOAuthService.ts`,
`GitHubConnector.ts`), `src/Utils/GitHubUtils.ts`
- `src/Explorer/Controls/GitHub/` (AuthorizeAccess, AddRepo, GitHubRepos components)
- `src/Explorer/Panes/GitHubReposPanel/`
- webpack entry `connectToGitHub` + `src/connectToGitHub.html`
**Integration / glue points (edited, not deleted)**
- `src/Explorer/Explorer.tsx``phoenixClient`, `notebookManager`, `gitHubOAuthService`,
`initNotebooks`, `initiateAndRefreshNotebookList`, `allocateContainer`,
`openNotebook*`, `openNotebookTerminal`, `createNotebookContentItemFile`, etc.
- `src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx` (+ adapter, test)
— New Notebook / Open Terminal / shell buttons branching on `isShellEnabled`.
- `src/Explorer/Tree/treeNodeUtil.tsx` / `ResourceTreeAdapter.tsx` / `ResourceTree.tsx`
— "My Notebooks" / "GitHub" tree nodes; `isNotebookEnabled` plumbing.
- `src/Explorer/SplashScreen/SplashScreen.tsx` — notebook cards + `openNotebookTerminal`
for Postgres/VCoreMongo shells.
- `src/Explorer/Tabs/TerminalTab.tsx` — chooses CloudShell vs notebook-server adapter.
- `src/Explorer/OpenActions/OpenActions.tsx`, `src/Explorer/ContextMenuButtonFactory.tsx`,
`src/Explorer/Tree/Collection.ts`, `src/Explorer/useSelectedNode.ts`,
`src/Explorer/MostRecentActivity/MostRecentActivity.ts`
- `src/hooks/useKnockoutExplorer.ts`, `src/hooks/useTabs.ts`
- `src/ConfigContext.ts`, `src/Common/Constants.ts`, `src/Contracts/DataModels.ts`,
`src/Contracts/ViewModels.ts`, `src/Contracts/ActionContracts.ts`,
`src/Platform/Hosted/extractFeatures.ts` (+ test)
- `src/Shared/Telemetry/TelemetryConstants.ts`
- `src/Localization/en/Resources.json` **and all non-English** `src/Localization/<locale>/Resources.json`
- `webpack.config.js``cellOutputViewer`, `connectToGitHub` entries + HTML plugins.
- `package.json``@nteract/*`, `@jupyterlab/*`, `@phosphor/widgets`, `rx-jupyter`,
and other notebook-only dependencies.
**Critical coupling — Terminal / shells**
- `TerminalTab` uses `CloudShellTerminalComponentAdapter` when
`userContext.features.enableCloudShell`, otherwise `NotebookTerminalComponentAdapter`
(which needs a Phoenix-allocated notebook server + `terminal.html` iframe).
- Command-bar/splash shell buttons branch on
`useNotebook.getState().isShellEnabled || userContext.features.enableCloudShell`.
- `Explorer.openNotebookTerminal(...)` is the shared entry for opening shells and must
be retained (and rewired to CloudShell-only) — only its notebook-server behavior is
removed.
## Phased approach
Each phase is independently buildable and shippable. Within each phase, **all
references to removed code are also removed** so the tree compiles. After every phase
run: `npm run compile`, `npm run compile:strict`, `npm run lint`, `npm run format:check`,
`npm test`, and a webpack build (`npm run build:ci`); manually verify the five shells
still open.
### Phase 1 — Decouple database shells to CloudShell-only ✅ COMPLETED
> **Status:** Completed and merged in `d19c7e0c` ("Remove Phoenix & Notebooks — Phase 1:
> Decouple database shells to CloudShell (#2513)").
Remove the legacy Phoenix notebook-server terminal path so shells no longer depend on
notebook provisioning.
- Rewire `TerminalTab` to always use `CloudShellTerminalComponentAdapter`; delete the
`NotebookTerminalComponentAdapter` branch and `getNotebookServerInfo`.
- Delete `src/Explorer/Tabs/ShellAdapters/NotebookTerminalComponentAdapter.tsx` and
`src/Explorer/Controls/Notebook/NotebookTerminalComponent.tsx` (+ less/test/snapshot).
- Simplify shell buttons in `CommandBarComponentButtonFactory` and `SplashScreen` to
drop the `isShellEnabled` branch (CloudShell path only); keep `openNotebookTerminal`.
- Verify whether the `terminal`/`terminal.html` webpack entry is still needed by
CloudShell. If unused, remove it and `src/Terminal/`; otherwise keep.
- **Outcome:** Shells run purely on CloudShell. Phoenix no longer needed for terminals.
### Phase 2 — Remove the in-app notebook authoring & rendering experience ✅ COMPLETED
> **Status:** Implemented on branch `users/jawelton/removenotebooks-phase2-061526`. Full
> verification sweep is green: `compile`, `compile:strict`, `lint` (0 errors), `format:check`,
> `test` (1945 passing), and `build:ci` (webpack) all pass.
>
> **Implementation notes / deviations:**
> - `src/Explorer/Panes/StringInputPane/` was also deleted — it became unused once
> `Explorer.renameNotebook` / `onCreateDirectory` were removed.
> - Removing notebook-only npm deps stripped several **phantom (transitively-hoisted)**
> packages that surviving code still imports, so these were re-added as explicit direct
> dependencies: `xterm@4.19.0` + `xterm-addon-fit@0.5.0` (CloudShell), `d3-collection@1.0.7`
> (Graph `D3ForceGraph`), and `@nteract/myths@0.1.9` (needed by kept `@nteract/core`).
> - Jest snapshots regenerated: `SettingsComponent`, `AddGlobalSecondaryIndexPanel`,
> `GitHubReposPanel` (all just dropped the removed `copyNotebook` function from the
> serialized Explorer); deleted-component snapshots removed with their dirs.
> - `CommandBarComponentButtonFactory` / `ContextMenuButtonFactory` required no notebook-button
> removal — they only contained shell entries (kept).
Delete the notebook tabs, the nteract rendering engine, panes, the read-only viewer, and
Schema Analyzer (see decision below), and remove all UI entry points that open notebooks.
> **Cross-phase coupling — revised approach (confirmed):** Several files the original plan
> deferred to later phases are hard-coupled to the deletions above and must be handled here:
> - **Schema Analyzer is pulled forward from Phase 3 into Phase 2.** `SchemaAnalyzer.tsx`
> renders through the nteract engine (`NotebookComponent/loadTransform`,
> `NotebookRenderer/outputs/SandboxOutputs`) and `SchemaAnalyzerTab extends NotebookTabBase`,
> so it cannot compile once the engine is deleted.
> - **`NotebookContentClient.ts` and `FileSystemUtil.ts` are deleted here** (they depend on
> the deleted `@nteract` content providers and only serve removed authoring paths).
> - **`NotebookManager.tsx` / `useNotebook.ts` get minimal edits** (full deletion stays in
> Phase 5): strip the content-provider / content-client / CopyNotebookPane / SchemaAnalyzer
> usages, keeping the GitHub / Juno / `NotebookContainerClient` wiring later phases need.
> - **`NotebookContentItem.ts` and `NotebookUtil.ts` are DEFERRED, not deleted here.** They
> are still used by the surviving `useNotebook` store and the "My Notebooks" tree
> (`ResourceTreeAdapter`), and `NotebookUtil.getName`/`isNotebookFile` are still used by the
> GitHub client/content provider. They are removed in Phase 4/5 with their consumers.
- Delete (engine/content): `src/Explorer/Notebook/NotebookComponent/`,
`src/Explorer/Notebook/NotebookRenderer/`, `src/Explorer/Notebook/SecurityWarningBar/`,
`NotebookClientV2.ts`, `notebookClientV2.test.ts`, `NotebookContentClient.ts`,
`NTeractUtil.ts`, `FileSystemUtil.ts`.
- Delete (tabs/panes/viewer): `src/Explorer/Tabs/NotebookV2Tab.ts`, `NotebookTabBase.ts`,
`src/Explorer/Panes/CopyNotebookPane/`, `src/Explorer/Controls/NotebookViewer/`,
`src/CellOutputViewer/` (+ `cellOutputViewer` webpack entry & HTML/inline plugins).
- Delete (Schema Analyzer, pulled forward): `src/Explorer/Notebook/SchemaAnalyzer/` and
`src/Explorer/Tabs/SchemaAnalyzerTab.ts`; remove its command-bar button and any tree/menu
entry points.
- Edit to decouple survivors: `NotebookManager.tsx` (see note above), `Explorer.tsx`
(remove `openNotebook*`, import/copy/rename/createDir/read/download/upload/delete/refresh
notebook methods + `notebookToImport`; KEEP `openNotebookTerminal` /
`connectToNotebookTerminal` and the Phoenix container methods for Phase 5),
`ResourceTreeAdapter.tsx` (drop `NotebookV2Tab` import + open/copy actions; neutralize node
click handlers — full "My Notebooks" tree removal remains Phase 5), `useTabs.ts` (drop
`NotebookTabV2`), `Contracts/ViewModels.ts` (fix notebook-tab interfaces that break compile;
keep `CollectionTabKind` enum values).
- Remove notebook entry points: "New Notebook"/open-notebook buttons in
`CommandBarComponentButtonFactory` (+ test), `OpenActions.tsx` (`OpenSampleNotebook`),
`ContextMenuButtonFactory.tsx`, splash-screen notebook cards & recent-notebook items
(`MostRecentActivity` `OpenNotebook` type).
- `tsconfig.strict.json`: remove entries for deleted files (keep `NotebookContentItem.ts`).
- Remove from `package.json` ONLY the notebook-only deps with **zero** remaining importers
after the deletions (e.g. `@jupyterlab/*`, `@phosphor/widgets`, and any `@nteract/*` not
still used). DEFER `@nteract/core`, `@nteract/commutable`, and `rx-jupyter` (still imported
by the kept `NotebookManager` / GitHub code) to Phase 4/5.
- Update/delete affected tests; regenerate Jest snapshots (`treeNodeUtil`,
`SettingsComponent`, `StringInputPane`).
- **Outcome:** Notebooks and Schema Analyzer can no longer be authored, opened, or rendered.
### Phase 3 — (folded into Phase 2)
Schema Analyzer removal was pulled forward into Phase 2 because it is rendered by the nteract
engine deleted there. No separate work remains for this phase.
### Phase 4 — Remove GitHub integration ✅ COMPLETED
> **Status:** Implemented on branch `users/jawelton/removenotebooks-phase3-062226`. Full
> verification sweep is green: `compile`, `compile:strict`, `lint` (0 errors), `format:check`,
> `test` (1900 passing), and `build:ci` (webpack) all pass.
>
> **Implementation notes / deviations:**
> - **Localization was a no-op:** there are **zero** GitHub keys in any locale
> `Resources.json`. The GitHub UI strings were hardcoded literals that were deleted with
> their components, so no resource-file edits were needed.
> - **`JunoClient` was trimmed, not deleted** (deletion stays in Phase 6). Removed only the
> GitHub-only members (`cachedPinnedRepos`, `subscribeToPinnedRepos`, `getPinnedRepos`,
> `updatePinnedRepos`, `deleteGitHubInfo`, `getGitHubToken`, `deleteAppAuthorization`,
> `getGitHubClientParams`, `IPinnedRepo`/`IPinnedBranch`) plus the now-orphaned private
> helpers `getNotebooksSubscriptionIdAccountUrl`/`getAccount`/`getSubscriptionId` and the
> `knockout` import. Kept the Schema (`requestSchema`/`getSchema`, used by `Database.tsx`)
> and gallery methods. Deleted `JunoClient.test.ts` (it only covered removed GitHub methods).
> - **`src/Utils/JunoUtils.ts` (+ test) was also deleted** — its `toPinnedRepo`/`toGitHubRepo`
> helpers only converted GitHub pinned-repo shapes and had no other consumers.
> - **`NotebookUtil.ts` (deferred to Phase 5) was edited here** to drop its `GitHubUtils`
> dependency: the dead `github://` content-URI branches in `getFilePath`/`getParentPath`/
> `getName`/`replaceName` were removed (its `isNotebookFile` consumer in `ResourceTreeAdapter`
> is unaffected). `NotebookUtil.test.ts` lost its github-uri cases.
> - **`ConfigContext.ts`:** removed the GitHub-only config fields `GITHUB_CLIENT_ID`,
> `GITHUB_TEST_ENV_CLIENT_ID`, `GITHUB_CLIENT_SECRET` (their only consumers were deleted).
> - **Telemetry deferred (as planned):** the `Action.NotebooksGitHub*` enum members were left
> in `TelemetryConstants.ts` for the Phase 6 cleanup (enum-numbering-safe); only their
> now-deleted usages were removed.
- Delete `src/GitHub/`, `src/Explorer/Controls/GitHub/`,
`src/Explorer/Panes/GitHubReposPanel/`, `src/Utils/GitHubUtils.ts`,
`src/connectToGitHub.html`, and the `connectToGitHub` webpack entry & HTML plugin.
- Remove `gitHubOAuthService`, GitHub pinned-repo wiring, and `gitHubNotebooksContentRoot`
usage from `Explorer.tsx`, `useNotebook.ts`, `NotebookManager.tsx`, and `JunoClient`
pinned-repo methods.
- Remove GitHub-related localization keys from **all** locale files (`en` + non-English).
### Phase 5 — Remove Phoenix and the notebook container/allocation core ✅ COMPLETED
> **Status:** Implemented on branch `users/jawelton/remove-notebooks-phase5-081426`.
> The automated verification sweep is green: `compile`, `compile:strict`, `lint`,
> `format:check`, `test` (1931 passing, 5 skipped), and `build:ci` (webpack) all pass.
>
> **Implementation notes / deviations:**
> - Removed the Phoenix-only connection status and memory tracker command-bar UI, which
> depended entirely on the deleted `useNotebook` container state.
> - Moved the unrelated `isSynapseLinkUpdating` flag from `useNotebook` into the existing
> `useCommandBar` Zustand store and made static command creation react to it.
> - Finished the deferred Phase 1 cleanup by replacing remaining mixed
> `isShellEnabled`/notebook availability checks with the `enableCloudShell` feature gate.
> The non-Phoenix Mongo fallback remains available when CloudShell is disabled.
> - Removed the remaining direct nteract/runtime dependencies with no surviving importers:
> `@nteract/commutable`, `@nteract/core`, `@nteract/fixtures`, `@nteract/myths`, and
> `rx-jupyter`. Added `@types/uuid` explicitly because surviving CloudShell/UserContext
> code had previously received UUID declarations through the removed transitive graph.
> - Preserved the newer Cosmos DB (NoSQL) Cloud Shell added after this roadmap was written;
> all terminal tabs still instantiate `CloudShellTerminalComponentAdapter`.
> - Residual notebook/Phoenix config, contracts, telemetry, localization, Juno, generated
> ARM clients, and images remain intentionally deferred to Phase 6.
- Delete `src/Phoenix/`, `src/Explorer/Notebook/NotebookContainerClient.ts`,
`src/Explorer/Notebook/NotebookManager.tsx`, `src/Explorer/Notebook/useNotebook.ts`,
`src/Explorer/Notebook/NotebookContentItem.ts`, `src/Explorer/Notebook/NotebookUtil.ts`
(+ `NotebookUtil.test.ts`), `src/Utils/NotebookConfigurationUtils.ts`,
`src/hooks/useNotebookSnapshotStore.ts`. (`NotebookContentItem.ts` and `NotebookUtil.ts`
deletion was deferred from Phase 2 because the "My Notebooks" tree/store and the GitHub
client still referenced them; remove them here once those consumers are gone. If GitHub
was removed in Phase 4, `NotebookUtil`'s `getName`/`isNotebookFile` helpers were inlined
there.)
- Remove from `Explorer.tsx`: `phoenixClient`, `notebookManager`, `_isInitializingNotebooks`,
`initNotebooks`, `initiateAndRefreshNotebookList`, `refreshNotebookList`,
`allocateContainer`, container heartbeat/connection logic, and notebook-server URL
feature overrides.
- Remove notebook tree nodes ("My Notebooks") and `isNotebookEnabled` plumbing from
`treeNodeUtil.tsx`, `ResourceTreeAdapter.tsx`, `ResourceTree.tsx`, `Collection.ts`,
`useSelectedNode.ts` (+ update tree snapshots/tests).
- Remove notebook initialization from `useKnockoutExplorer.ts` and notebook tab handling
in `useTabs.ts`.
### Phase 6 — Remove residual clients, config, contracts, telemetry & strings
- Delete `src/Juno/` and `src/Utils/arm/generatedClients/cosmosNotebooks/`.
- Remove notebook fields from `ConfigContext.ts`, `Constants.ts` (Notebook namespace),
`DataModels.ts` (notebook/Phoenix/container interfaces), `ViewModels.ts`,
`ActionContracts.ts`, and notebook feature flags from
`extractFeatures.ts` (+ update test).
- Remove notebook/Phoenix telemetry actions/areas from `TelemetryConstants.ts` (preserve
enum numbering if other systems depend on it — mirror the cautious approach in
`a36467f4`).
- Remove remaining notebook strings from **all** locale `Resources.json` files (`en` +
every non-English locale) and any notebook images (e.g. `images/notebook/`).
- Final full build + test sweep; update `EndpointUtils.ts` (`allowedNotebookServerUrls`)
and any docs/comments referencing notebooks.
## Cross-cutting verification (run after each phase)
```
npm run compile
npm run compile:strict
npm run lint
npm run format:check
npm test
npm run build:ci
```
Plus manual smoke test: open Mongo, Cassandra, Postgres, VCoreMongo, and Cosmos DB NoSQL shells.
## Notes & considerations
- **Strict null checks:** any file edited may need to stay in / be removed from
`tsconfig.strict.json`. Remove deleted files from that list.
- **Snapshots:** several Jest snapshots reference notebook UI
(`treeNodeUtil`, `SettingsComponent`, panel snapshots). Regenerate after edits.
- **Telemetry enum safety:** prior commit `a36467f4` deliberately reverted removal of
enum values to avoid breaking downstream consumers. Prefer leaving enum numeric values
intact unless confirmed safe to remove.
- **`enableCloudShell` feature flag:** confirm it is enabled in all target environments
before removing the Phoenix shell fallback, or shells will break.
- **E2E tests:** check `test/` for notebook/terminal specs to update or remove; shells
may have E2E coverage that needs the CloudShell-only path.
- **Reconcile** with branch `users/jawelton/remove-notebooks-terminal-052126` to avoid
rework, especially in Phase 1.
-1
View File
@@ -70,7 +70,6 @@ module.exports = {
moduleNameMapper: {
"^.*[.](png|gif|less|css)$": "<rootDir>/mockModule",
"(.*)$[.](svg)": "<rootDir>/mockModule/$1",
"@nteract/stateful-components/(.*)$": "<rootDir>/mockModule",
"@fluentui/react/lib/(.*)$": "@fluentui/react/lib-commonjs/$1", // https://github.com/microsoft/fluentui/wiki/Version-8-release-notes
"monaco-editor/(.*)$": "<rootDir>/__mocks__/monaco-editor",
"^dnd-core$": "dnd-core/dist/cjs",
+345 -2707
View File
File diff suppressed because it is too large Load Diff
+19 -38
View File
@@ -3,6 +3,9 @@
"version": "1.0.0",
"description": "Cosmos Explorer",
"main": "index.js",
"engines": {
"node": "22.x"
},
"dependencies": {
"@azure/arm-cosmosdb": "16.4.0",
"@azure/cosmos": "4.7.0",
@@ -13,35 +16,10 @@
"@babel/plugin-proposal-decorators": "7.12.12",
"@fluentui/react": "8.119.0",
"@fluentui/react-components": "9.54.2",
"@jupyterlab/services": "6.0.2",
"@jupyterlab/terminal": "3.6.8",
"@microsoft/applicationinsights-web": "2.6.1",
"@nteract/commutable": "7.5.1",
"@nteract/connected-components": "6.8.2",
"@nteract/core": "15.1.9",
"@nteract/data-explorer": "8.2.12",
"@nteract/directory-listing": "2.0.6",
"@nteract/dropdown-menu": "1.0.1",
"@nteract/editor": "10.1.12",
"@nteract/fixtures": "2.3.0",
"@nteract/iron-icons": "1.0.0",
"@nteract/jupyter-widgets": "2.0.0",
"@nteract/logos": "1.0.0",
"@nteract/markdown": "4.6.0",
"@nteract/monaco-editor": "3.2.2",
"@nteract/octicons": "2.0.0",
"@nteract/outputs": "3.0.9",
"@nteract/presentational-components": "3.4.12",
"@nteract/stateful-components": "1.7.0",
"@nteract/styles": "2.0.2",
"@nteract/transform-geojson": "5.1.8",
"@nteract/transform-model-debug": "5.0.1",
"@nteract/transform-plotly": "6.1.6",
"@nteract/transform-vdom": "4.0.11",
"@nteract/transform-vega": "7.0.6",
"@octokit/request": "8.4.1",
"@octokit/rest": "17.9.2",
"@phosphor/widgets": "1.9.3",
"@testing-library/jest-dom": "6.4.6",
"@types/lodash": "4.14.171",
"@types/mkdirp": "1.0.1",
@@ -55,10 +33,11 @@
"canvas": "3.2.1",
"clean-webpack-plugin": "4.0.0",
"clipboard-copy": "4.0.1",
"copy-webpack-plugin": "11.0.0",
"copy-webpack-plugin": "14.0.0",
"crossroads": "0.12.2",
"css-element-queries": "1.1.1",
"d3": "7.9.0",
"d3-collection": "1.0.7",
"datatables.net-colreorder-dt": "1.7.0",
"datatables.net-dt": "1.13.8",
"date-fns": "1.29.0",
@@ -75,7 +54,7 @@
"i18next-resources-to-backend": "1.2.1",
"iframe-resizer-react": "1.1.0",
"immer": "9.0.6",
"immutable": "4.0.0-rc.12",
"immutable": "4.3.9",
"is-ci": "2.0.0",
"jquery": "3.7.1",
"jquery-typeahead": "2.11.1",
@@ -87,7 +66,7 @@
"mkdirp": "1.0.4",
"monaco-editor": "0.44.0",
"ms": "2.1.3",
"nanoid": "3.3.8",
"nanoid": "3.3.18",
"p-retry": "6.2.1",
"patch-package": "8.0.1",
"plotly.js-cartesian-dist-min": "1.52.3",
@@ -107,17 +86,18 @@
"react-window": "1.8.10",
"react-youtube": "9.0.1",
"reflect-metadata": "0.1.13",
"rx-jupyter": "5.5.12",
"shell-quote": "1.7.3",
"shell-quote": "1.10.0",
"styled-components": "5.0.1",
"swr": "0.4.0",
"terser-webpack-plugin": "5.3.9",
"terser-webpack-plugin": "5.6.1",
"tinykeys": "2.1.0",
"underscore": "1.13.8",
"utility-types": "3.10.0",
"uuid": "9.0.0",
"web-vitals": "4.2.4",
"ws": "8.17.1",
"ws": "8.21.3",
"xterm": "4.19.0",
"xterm-addon-fit": "0.5.0",
"zustand": "3.5.0"
},
"overrides": {
@@ -136,7 +116,7 @@
"@types/react-redux": "7.1.7"
},
"devDependencies": {
"@babel/core": "7.29.0",
"@babel/core": "7.29.6",
"@babel/preset-env": "7.24.7",
"@babel/preset-react": "7.24.7",
"@babel/preset-typescript": "7.24.7",
@@ -154,7 +134,7 @@
"@types/hasher": "0.0.31",
"@types/jest": "29.5.12",
"@types/jquery": "3.5.29",
"@types/node": "18.19.0",
"@types/node": "22.14.1",
"@types/post-robot": "10.0.1",
"@types/q": "1.5.1",
"@types/react": "17.0.44",
@@ -167,13 +147,14 @@
"@types/sinon": "2.3.3",
"@types/styled-components": "5.1.32",
"@types/underscore": "1.7.36",
"@types/uuid": "9.0.8",
"@types/youtube-player": "5.5.6",
"@typescript-eslint/eslint-plugin": "6.7.4",
"@typescript-eslint/parser": "6.7.4",
"@webpack-cli/serve": "2.0.5",
"babel-jest": "29.7.0",
"babel-loader": "8.1.0",
"brace-expansion": "1.1.15",
"brace-expansion": "1.1.18",
"buffer": "5.1.0",
"case-sensitive-paths-webpack-plugin": "2.4.0",
"create-file-webpack": "1.0.2",
@@ -199,7 +180,7 @@
"jest-html-loader": "1.0.0",
"jest-react-hooks-shallow": "1.5.1",
"jest-trx-results-processor": "3.0.2",
"js-yaml": "3.14.2",
"js-yaml": "3.15.1",
"less": "4.5.1",
"less-loader": "11.1.3",
"less-vars-loader": "1.1.0",
@@ -223,8 +204,8 @@
"webpack": "5.104.1",
"webpack-bundle-analyzer": "5.2.0",
"webpack-cli": "5.1.4",
"webpack-dev-server": "5.2.3",
"ws": "8.17.1"
"webpack-dev-server": "5.2.5",
"ws": "8.21.3"
},
"scripts": {
"postinstall": "patch-package && npm run generate:i18n-keys",
-13
View File
@@ -1,13 +0,0 @@
diff --git a/node_modules/@phosphor/virtualdom/lib/index.d.ts b/node_modules/@phosphor/virtualdom/lib/index.d.ts
index 95682b9..73e2daa 100644
--- a/node_modules/@phosphor/virtualdom/lib/index.d.ts
+++ b/node_modules/@phosphor/virtualdom/lib/index.d.ts
@@ -58,7 +58,7 @@ export declare type ElementEventMap = {
ondrop: DragEvent;
ondurationchange: Event;
onemptied: Event;
- onended: MediaStreamErrorEvent;
+ onended: ErrorEvent;
onerror: ErrorEvent;
onfocus: FocusEvent;
oninput: Event;
+2 -2
View File
@@ -5,7 +5,7 @@ const fetch = require("node-fetch");
const backendEndpoint = "https://cdb-ms-mpac-pbe.cosmos.azure.com";
const previewSiteEndpoint = "https://dataexplorer-preview.azurewebsites.net";
const previewStorageWebsiteEndpoint = "https://dataexplorerpreview.z5.web.core.windows.net/";
const previewStorageWebsiteEndpoint = "_REPLACE_STORAGE_WEBSITE_ENDPOINT_";
const githubApiUrl = "https://api.github.com/repos/Azure/cosmos-explorer";
const azurePortalMpacEndpoint = "https://ms.portal.azure.com/";
@@ -46,7 +46,7 @@ const app = express();
app.use("/api", api);
app.use("/proxy", proxy);
app.use("/commit", commit);
app.get("/pull/:pr(\\d+)", (req, res) => {
app.get("/pull/:pr", (req, res) => {
const pr = req.params.pr;
if (!/^\d+$/.test(pr)) {
return res.status(400).send("Invalid pull request number");
+7 -32
View File
@@ -11,10 +11,8 @@
"body-parser": "^2.2.2",
"express": "^5.2.1",
"follow-redirects": "^1.16.0",
"http-proxy-middleware": "^3.0.5",
"node": "^20.19.5",
"node-fetch": "^2.6.1",
"path-to-regexp": "^0.1.13"
"http-proxy-middleware": "^3.0.7",
"node-fetch": "^2.6.1"
}
},
"node_modules/@types/http-proxy": {
@@ -416,9 +414,10 @@
}
},
"node_modules/http-proxy-middleware": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-3.0.5.tgz",
"integrity": "sha512-GLZZm1X38BPY4lkXA01jhwxvDoOkkXqjgVyUzVxiEK4iuRu03PZoYHhHRwxnfhQMDuaxi3vVri0YgSro/1oWqg==",
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-3.0.7.tgz",
"integrity": "sha512-iwbQltVlx8bCrqePUM8C+hllHvdawVhQJaLrj1X7qllkvFQdXFsr16pW/mo9+JDVjN+QO2XUx9jd8SmoFkE5qw==",
"license": "MIT",
"dependencies": {
"@types/http-proxy": "^1.17.15",
"debug": "^4.3.6",
@@ -428,7 +427,7 @@
"micromatch": "^4.0.8"
},
"engines": {
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
"node": "^14.18.0 || ^16.10.0 || >=18.0.0"
}
},
"node_modules/http-proxy-middleware/node_modules/braces": {
@@ -588,20 +587,6 @@
"node": ">= 0.6"
}
},
"node_modules/node": {
"version": "20.19.5",
"hasInstallScript": true,
"license": "ISC",
"dependencies": {
"node-bin-setup": "^1.0.0"
},
"bin": {
"node": "bin/node.exe"
},
"engines": {
"npm": ">=5.0.0"
}
},
"node_modules/node-fetch": {
"version": "2.6.7",
"license": "MIT",
@@ -620,10 +605,6 @@
}
}
},
"node_modules/node/node_modules/node-bin-setup": {
"version": "1.1.4",
"license": "ISC"
},
"node_modules/object-inspect": {
"version": "1.13.4",
"license": "MIT",
@@ -660,12 +641,6 @@
"node": ">= 0.8"
}
},
"node_modules/path-to-regexp": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
"integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
"license": "MIT"
},
"node_modules/picomatch": {
"version": "2.3.1",
"license": "MIT",
+3 -5
View File
@@ -4,7 +4,7 @@
"description": "",
"main": "index.js",
"scripts": {
"deploy": "az webapp up --name \"dataexplorer-preview\" --subscription \"cosmosdb-portalteam-runners\" --resource-group \"dataexplorer-preview\" --runtime \"NODE:20-lts\" --sku P1V2",
"deploy": "az webapp up --name \"dataexplorer-preview\" --subscription \"cosmosdb-portalteam-runners\" --resource-group \"dataexplorer-preview\" --runtime \"NODE:22-lts\" --sku P1V2",
"start": "node index.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
@@ -14,9 +14,7 @@
"body-parser": "^2.2.2",
"express": "^5.2.1",
"follow-redirects": "^1.16.0",
"http-proxy-middleware": "^3.0.5",
"node": "^20.19.5",
"node-fetch": "^2.6.1",
"path-to-regexp": "^0.1.13"
"http-proxy-middleware": "^3.0.7",
"node-fetch": "^2.6.1"
}
}
@@ -1,15 +0,0 @@
.schema-analyzer-cell-outputs {
padding: 10px 2px;
}
// Mimic FluentUI8's DocumentCard style
.schema-analyzer-cell-output {
margin-bottom: 20px;
padding: 14px 20px;
border: 1px solid rgb(237, 235, 233);
}
.schema-analyzer-cell-output:hover {
border-color: rgb(200, 198, 196);
box-shadow: inset 0 0 0 1px rgb(200, 198, 196)
}
-104
View File
@@ -1,104 +0,0 @@
import { createImmutableOutput, JSONObject, OnDiskOutput } from "@nteract/commutable";
// import outputs individually to avoid increasing the bundle size
import { KernelOutputError } from "@nteract/outputs/lib/components/kernel-output-error";
import { Output } from "@nteract/outputs/lib/components/output";
import { StreamText } from "@nteract/outputs/lib/components/stream-text";
import { ContentRef } from "@nteract/types";
import "bootstrap/dist/css/bootstrap.css";
import postRobot from "post-robot";
import * as React from "react";
import * as ReactDOM from "react-dom";
import "../../externals/iframeResizer.contentWindow.min.js"; // Required for iFrameResizer to work
import { SnapshotRequest } from "../Explorer/Notebook/NotebookComponent/types";
import "../Explorer/Notebook/NotebookRenderer/base.css";
import "../Explorer/Notebook/NotebookRenderer/default.css";
import { NotebookUtil } from "../Explorer/Notebook/NotebookUtil";
import "./CellOutputViewer.less";
import { TransformMedia } from "./TransformMedia";
export interface SnapshotResponse {
imageSrc: string;
requestId: string;
}
export interface CellOutputViewerProps {
id: string;
contentRef: ContentRef;
outputsContainerClassName: string;
outputClassName: string;
outputs: OnDiskOutput[];
onMetadataChange: (metadata: JSONObject, mediaType: string, index?: number) => void;
}
const onInit = async () => {
postRobot.on(
"props",
{
window: window.parent,
domain: window.location.origin,
},
(event) => {
// Typescript definition for event is wrong. So read props by casting to <any>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const props = (event as any).data as CellOutputViewerProps;
const outputs = (
<div data-iframe-height className={props.outputsContainerClassName}>
{props.outputs?.map((output, index) => (
<div className={props.outputClassName} key={index}>
<Output output={createImmutableOutput(output)} key={index}>
<TransformMedia
output_type={"display_data"}
id={props.id}
contentRef={props.contentRef}
onMetadataChange={(metadata, mediaType) => props.onMetadataChange(metadata, mediaType, index)}
/>
<TransformMedia
output_type={"execute_result"}
id={props.id}
contentRef={props.contentRef}
onMetadataChange={(metadata, mediaType) => props.onMetadataChange(metadata, mediaType, index)}
/>
<KernelOutputError />
<StreamText />
</Output>
</div>
))}
</div>
);
ReactDOM.render(outputs, document.getElementById("cellOutput"));
},
);
postRobot.on(
"snapshotRequest",
{
window: window.parent,
domain: window.location.origin,
},
async (event): Promise<SnapshotResponse> => {
const topNode = document.getElementById("cellOutput");
if (!topNode) {
const errorMsg = "No top node to snapshot";
return Promise.reject(new Error(errorMsg));
}
// Typescript definition for event is wrong. So read props by casting to <any>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const snapshotRequest = (event as any).data as SnapshotRequest;
const result = await NotebookUtil.takeScreenshotDomToImage(
topNode,
snapshotRequest.aspectRatio,
undefined,
snapshotRequest.downloadFilename,
);
return {
imageSrc: result.imageSrc,
requestId: snapshotRequest.requestId,
};
},
);
};
// Entry point
window.addEventListener("load", onInit);
-138
View File
@@ -1,138 +0,0 @@
import { ImmutableDisplayData, ImmutableExecuteResult, JSONObject } from "@nteract/commutable";
// import outputs individually to avoid increasing the bundle size
import { HTML } from "@nteract/outputs/lib/components/media/html";
import { Image } from "@nteract/outputs/lib/components/media/image";
import { JavaScript } from "@nteract/outputs/lib/components/media/javascript";
import { Json } from "@nteract/outputs/lib/components/media/json";
import { LaTeX } from "@nteract/outputs/lib/components/media/latex";
import { Plain } from "@nteract/outputs/lib/components/media/plain";
import { SVG } from "@nteract/outputs/lib/components/media/svg";
import { ContentRef } from "@nteract/types";
import React, { Suspense } from "react";
const EmptyTransform = (): JSX.Element => <></>;
const displayOrder = [
"application/vnd.jupyter.widget-view+json",
"application/vnd.vega.v5+json",
"application/vnd.vega.v4+json",
"application/vnd.vega.v3+json",
"application/vnd.vega.v2+json",
"application/vnd.vegalite.v4+json",
"application/vnd.vegalite.v3+json",
"application/vnd.vegalite.v2+json",
"application/vnd.vegalite.v1+json",
"application/geo+json",
"application/vnd.plotly.v1+json",
"text/vnd.plotly.v1+html",
"application/x-nteract-model-debug+json",
"application/vnd.dataresource+json",
"application/vdom.v1+json",
"application/json",
"application/javascript",
"text/html",
"text/markdown",
"text/latex",
"image/svg+xml",
"image/gif",
"image/png",
"image/jpeg",
"text/plain",
];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const transformsById = new Map<string, React.ComponentType<any>>([
["text/vnd.plotly.v1+html", React.lazy(() => import("@nteract/transform-plotly"))],
["application/vnd.plotly.v1+json", React.lazy(() => import("@nteract/transform-plotly"))],
["application/geo+json", EmptyTransform], // TODO: The geojson transform will likely need some work because of the basemap URL(s)
["application/x-nteract-model-debug+json", React.lazy(() => import("@nteract/transform-model-debug"))],
["application/vnd.dataresource+json", React.lazy(() => import("@nteract/data-explorer"))],
["application/vnd.jupyter.widget-view+json", React.lazy(() => import("./transforms/WidgetDisplay"))],
["application/vnd.vegalite.v1+json", React.lazy(() => import("./transforms/VegaLite1"))],
["application/vnd.vegalite.v2+json", React.lazy(() => import("./transforms/VegaLite2"))],
["application/vnd.vegalite.v3+json", React.lazy(() => import("./transforms/VegaLite3"))],
["application/vnd.vegalite.v4+json", React.lazy(() => import("./transforms/VegaLite4"))],
["application/vnd.vega.v2+json", React.lazy(() => import("./transforms/Vega2"))],
["application/vnd.vega.v3+json", React.lazy(() => import("./transforms/Vega3"))],
["application/vnd.vega.v4+json", React.lazy(() => import("./transforms/Vega4"))],
["application/vnd.vega.v5+json", React.lazy(() => import("./transforms/Vega5"))],
["application/vdom.v1+json", React.lazy(() => import("@nteract/transform-vdom"))],
["application/json", Json],
["application/javascript", JavaScript],
["text/html", HTML],
["text/markdown", React.lazy(() => import("@nteract/outputs/lib/components/media/markdown"))], // Markdown increases the bundle size so lazy load it
["text/latex", LaTeX],
["image/svg+xml", SVG],
["image/gif", Image],
["image/png", Image],
["image/jpeg", Image],
["text/plain", Plain],
]);
interface TransformMediaProps {
output_type: string;
id: string;
contentRef: ContentRef;
output?: ImmutableDisplayData | ImmutableExecuteResult;
onMetadataChange: (metadata: JSONObject, mediaType: string) => void;
}
export const TransformMedia = (props: TransformMediaProps): JSX.Element => {
const { Media, mediaType, data, metadata } = getMediaInfo(props);
// If we had no valid result, return an empty output
if (!mediaType || !data) {
return <></>;
}
return (
<Suspense fallback={<div>Loading...</div>}>
<Media
onMetadataChange={props.onMetadataChange}
data={data}
metadata={metadata}
contentRef={props.contentRef}
id={props.id}
/>
</Suspense>
);
};
const getMediaInfo = (props: TransformMediaProps) => {
const { output, output_type } = props;
// This component should only be used with display data and execute result
if (!output || !(output_type === "display_data" || output_type === "execute_result")) {
console.warn("connected transform media managed to get a non media bundle output");
return {
Media: EmptyTransform,
};
}
// Find the first mediaType in the output data that we support with a handler
const mediaType = displayOrder.find(
(key) =>
Object.prototype.hasOwnProperty.call(output.data, key) &&
(Object.prototype.hasOwnProperty.call(transformsById, key) || transformsById.get(key)),
);
if (mediaType) {
const metadata = output.metadata.get(mediaType);
const data = output.data[mediaType];
const Media = transformsById.get(mediaType);
return {
Media,
mediaType,
data,
metadata,
};
}
return {
Media: EmptyTransform,
mediaType,
output,
};
};
export default TransformMedia;
@@ -1,12 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="height=device-height, width=device-width, initial-scale=1.0" />
<title>Cell Output Viewer</title>
</head>
<body>
<div class="cellOutput" id="cellOutput"></div>
</body>
</html>
-1
View File
@@ -1 +0,0 @@
export { Vega2 as default } from "@nteract/transform-vega";
-1
View File
@@ -1 +0,0 @@
export { Vega3 as default } from "@nteract/transform-vega";
-1
View File
@@ -1 +0,0 @@
export { Vega4 as default } from "@nteract/transform-vega";
-1
View File
@@ -1 +0,0 @@
export { Vega5 as default } from "@nteract/transform-vega";
@@ -1 +0,0 @@
export { VegaLite1 as default } from "@nteract/transform-vega";
@@ -1 +0,0 @@
export { VegaLite2 as default } from "@nteract/transform-vega";
@@ -1 +0,0 @@
export { VegaLite3 as default } from "@nteract/transform-vega";
@@ -1 +0,0 @@
export { VegaLite4 as default } from "@nteract/transform-vega";
@@ -1 +0,0 @@
export { WidgetDisplay as default } from "@nteract/jupyter-widgets";
+1 -1
View File
@@ -6,7 +6,7 @@ export class EndpointsRegex {
public static readonly mongo = "mongodb://.*:(.*)@(.*).documents.azure.com";
public static readonly mongoCompute = "mongodb://.*:(.*)@(.*).mongo.cosmos.azure.com";
public static readonly sql = "AccountEndpoint=https://(.*).documents.azure.com";
public static readonly table = "TableEndpoint=https://(.*).table.cosmosdb.azure.com";
public static readonly table = "TableEndpoint=https://(.*).table.cosmos(?:db)?.azure.com";
}
export class ApiEndpoints {
+1
View File
@@ -143,6 +143,7 @@ export async function getTokenFromAuthService(
headers: {
"content-type": "application/json",
"x-ms-encrypted-auth-token": userContext.accessToken,
authorization: userContext.accessToken,
},
body: JSON.stringify({
verb,
+4 -1
View File
@@ -22,7 +22,10 @@ const defaultHeaders = {
function authHeaders() {
if (userContext.authType === AuthType.EncryptedToken) {
return { [HttpHeaders.guestAccessToken]: userContext.accessToken };
return {
[HttpHeaders.guestAccessToken]: userContext.accessToken,
[HttpHeaders.authorization]: userContext.accessToken,
};
} else {
const headers: { [key: string]: string } = {
[HttpHeaders.authorization]: userContext.authorizationToken,
+45
View File
@@ -0,0 +1,45 @@
import { configContext } from "../ConfigContext";
import { AccessInputMetadata } from "../Contracts/DataModels";
import { HttpHeaders } from "./Constants";
export async function fetchAccessData(portalToken: string): Promise<AccessInputMetadata> {
const headers = new Headers();
// Portal encrypted token API quirk: The token header must be URL encoded
headers.append(HttpHeaders.guestAccessToken, encodeURIComponent(portalToken));
headers.append(HttpHeaders.authorization, encodeURIComponent(portalToken));
const url: string = `${configContext.PORTAL_BACKEND_ENDPOINT}/api/connectionstring/runtimeproxy/accessinputmetadata`;
const options = {
method: "GET",
headers: headers,
};
return fetch(url, options)
.then((response) => response.json())
.catch((error) => console.error(error));
}
export async function fetchEncryptedToken(connectionString: string): Promise<string> {
const headers = new Headers();
headers.append(HttpHeaders.connectionString, connectionString);
headers.append(HttpHeaders.authorization, connectionString);
const url = configContext.PORTAL_BACKEND_ENDPOINT + "/api/connectionstring/token/generatetoken";
const response = await fetch(url, { headers, method: "POST" });
if (!response.ok) {
throw response;
}
const encryptedTokenResponse: string = await response.json();
return decodeURIComponent(encryptedTokenResponse);
}
export async function isAccountRestrictedForConnectionStringLogin(connectionString: string): Promise<boolean> {
const headers = new Headers();
headers.append(HttpHeaders.connectionString, connectionString);
const url = configContext.PORTAL_BACKEND_ENDPOINT + "/api/guest/accountrestrictions/checkconnectionstringlogin";
const response = await fetch(url, { headers, method: "POST" });
if (!response.ok) {
throw response;
}
return (await response.text()).toLowerCase() === "true";
}
-5
View File
@@ -48,9 +48,6 @@ export interface ConfigContext {
CASSANDRA_PROXY_ENDPOINT: string;
PROXY_PATH?: string;
JUNO_ENDPOINT: string;
GITHUB_CLIENT_ID: string;
GITHUB_TEST_ENV_CLIENT_ID: string;
GITHUB_CLIENT_SECRET?: string; // No need to inject secret for prod. Juno already knows it.
isPhoenixEnabled: boolean;
hostedExplorerURL: string;
armAPIVersion?: string;
@@ -95,8 +92,6 @@ let configContext: Readonly<ConfigContext> = {
CATALOG_API_KEY: "",
ARCADIA_ENDPOINT: "https://workspaceartifacts.projectarcadia.net",
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: "dev.azuresynapse.net",
GITHUB_CLIENT_ID: "6cb2f63cf6f7b5cbdeca", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1189306
GITHUB_TEST_ENV_CLIENT_ID: "b63fc8cbf87fd3c6e2eb", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1777772
JUNO_ENDPOINT: JunoEndpoints.Prod,
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Prod,
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
+1 -1
View File
@@ -160,7 +160,6 @@ export interface Collection extends CollectionBase {
onTableEntitiesClick(): void;
onGraphDocumentsClick(): void;
onMongoDBDocumentsClick(): void;
onSchemaAnalyzerClick(): void;
openTab(): void;
onSettingsClick: () => Promise<void>;
@@ -407,6 +406,7 @@ export enum TerminalKind {
Cassandra = 2,
Postgres = 3,
VCoreMongo = 4,
CosmosDB = 5,
}
export interface DataExplorerInputsFrame {
+3 -10
View File
@@ -28,7 +28,6 @@ import { userContext } from "../UserContext";
import { getCollectionName, getDatabaseName } from "../Utils/APITypeUtils";
import { useSidePanel } from "../hooks/useSidePanel";
import Explorer from "./Explorer";
import { useNotebook } from "./Notebook/useNotebook";
import { DeleteCollectionConfirmationPane } from "./Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane";
import { DeleteDatabaseConfirmationPanel } from "./Panes/DeleteDatabaseConfirmationPanel";
import StoredProcedure from "./Tree/StoredProcedure";
@@ -120,23 +119,17 @@ export const createCollectionContextMenuButton = (
iconSrc: HostedTerminalIcon,
onClick: () => {
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
if (useNotebook.getState().isShellEnabled || userContext.features.enableCloudShell) {
if (userContext.features.enableCloudShell) {
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
} else {
selectedCollection && selectedCollection.onNewMongoShellClick();
}
},
label:
useNotebook.getState().isShellEnabled || userContext.features.enableCloudShell
? t(Keys.contextMenu.openMongoShell)
: t(Keys.contextMenu.newShell),
label: userContext.features.enableCloudShell ? t(Keys.contextMenu.openMongoShell) : t(Keys.contextMenu.newShell),
});
}
if (
(useNotebook.getState().isShellEnabled || userContext.features.enableCloudShell) &&
userContext.apiType === "Cassandra"
) {
if (userContext.features.enableCloudShell && userContext.apiType === "Cassandra") {
items.push({
iconSrc: HostedTerminalIcon,
onClick: () => {
@@ -1,121 +0,0 @@
import { DefaultButton, IButtonProps, ITextFieldProps, TextField } from "@fluentui/react";
import * as React from "react";
import * as Constants from "../../../Common/Constants";
import * as UrlUtility from "../../../Common/UrlUtility";
import { IGitHubRepo } from "../../../GitHub/GitHubClient";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import * as GitHubUtils from "../../../Utils/GitHubUtils";
import Explorer from "../../Explorer";
import { RepoListItem } from "./GitHubReposComponent";
import { ChildrenMargin } from "./GitHubStyleConstants";
export interface AddRepoComponentProps {
container: Explorer;
getRepo: (owner: string, repo: string) => Promise<IGitHubRepo>;
pinRepo: (item: RepoListItem) => void;
}
interface AddRepoComponentState {
textFieldValue: string;
textFieldErrorMessage: string;
}
export class AddRepoComponent extends React.Component<AddRepoComponentProps, AddRepoComponentState> {
private static readonly DescriptionText =
"Don't see what you're looking for? Add your repo/branch, or any public repo (read-access only) by entering the URL: ";
private static readonly ButtonText = "Add";
private static readonly TextFieldPlaceholder = "https://github.com/owner/repo/tree/branch";
private static readonly TextFieldErrorMessage = "Invalid url";
constructor(props: AddRepoComponentProps) {
super(props);
this.state = {
textFieldValue: "",
textFieldErrorMessage: undefined,
};
}
public render(): JSX.Element {
const textFieldProps: ITextFieldProps = {
placeholder: AddRepoComponent.TextFieldPlaceholder,
autoFocus: true,
value: this.state.textFieldValue,
errorMessage: this.state.textFieldErrorMessage,
onChange: this.onTextFieldChange,
};
const buttonProps: IButtonProps = {
text: AddRepoComponent.ButtonText,
ariaLabel: AddRepoComponent.ButtonText,
onClick: this.onAddRepoButtonClick,
};
return (
<>
<p style={{ marginBottom: ChildrenMargin }}>{AddRepoComponent.DescriptionText}</p>
<TextField {...textFieldProps} />
<DefaultButton style={{ marginTop: ChildrenMargin }} {...buttonProps} />
</>
);
}
private onTextFieldChange = (
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
newValue?: string,
): void => {
this.setState({
textFieldValue: newValue || "",
textFieldErrorMessage: undefined,
});
};
private onAddRepoButtonClick = async (): Promise<void> => {
const startKey: number = TelemetryProcessor.traceStart(Action.NotebooksGitHubManualRepoAdd, {
dataExplorerArea: Constants.Areas.Notebook,
});
let enteredUrl = this.state.textFieldValue;
if (enteredUrl.indexOf("/tree/") === -1) {
enteredUrl = UrlUtility.createUri(enteredUrl, `tree/`);
}
const repoInfo = GitHubUtils.fromRepoUri(enteredUrl);
if (repoInfo) {
this.setState({
textFieldValue: "",
textFieldErrorMessage: undefined,
});
const repo = await this.props.getRepo(repoInfo.owner, repoInfo.repo);
if (repo) {
const item: RepoListItem = {
key: GitHubUtils.toRepoFullName(repo.owner, repo.name),
repo,
branches: repoInfo.branch ? [{ name: repoInfo.branch }] : [],
};
TelemetryProcessor.traceSuccess(
Action.NotebooksGitHubManualRepoAdd,
{
dataExplorerArea: Constants.Areas.Notebook,
},
startKey,
);
return this.props.pinRepo(item);
}
}
this.setState({
textFieldErrorMessage: AddRepoComponent.TextFieldErrorMessage,
});
TelemetryProcessor.traceFailure(
Action.NotebooksGitHubManualRepoAdd,
{
dataExplorerArea: Constants.Areas.Notebook,
error: AddRepoComponent.TextFieldErrorMessage,
},
startKey,
);
};
}
@@ -1,85 +0,0 @@
import { ChoiceGroup, IButtonProps, IChoiceGroupProps, PrimaryButton, IChoiceGroupOption } from "@fluentui/react";
import * as React from "react";
import { ChildrenMargin } from "./GitHubStyleConstants";
export interface AuthorizeAccessComponentProps {
scope: string;
authorizeAccess: (scope: string) => void;
}
export interface AuthorizeAccessComponentState {
scope: string;
}
export class AuthorizeAccessComponent extends React.Component<
AuthorizeAccessComponentProps,
AuthorizeAccessComponentState
> {
// Scopes supported by GitHub OAuth. We're only interested in ones which allow us access to repos.
// https://developer.github.com/apps/building-oauth-apps/understanding-scopes-for-oauth-apps/
public static readonly Scopes = {
Public: {
key: "public_repo",
text: "Public repos only",
},
PublicAndPrivate: {
key: "repo",
text: "Public and private repos",
},
};
private static readonly DescriptionPara1 =
"Connect your notebooks workspace to GitHub. You'll be able to view, edit, and run notebooks stored in your GitHub repositories in Data Explorer.";
private static readonly DescriptionPara2 =
"Complete setup by authorizing Azure Cosmos DB to access the repositories in your GitHub account: ";
private static readonly AuthorizeButtonText = "Authorize access";
private onChoiceGroupChange = (event: React.SyntheticEvent<HTMLElement>, option: IChoiceGroupOption): void =>
this.setState({
scope: option.key,
});
private onButtonClick = (): void => this.props.authorizeAccess(this.state.scope);
constructor(props: AuthorizeAccessComponentProps) {
super(props);
this.state = {
scope: this.props.scope,
};
}
public render(): JSX.Element {
const choiceGroupProps: IChoiceGroupProps = {
options: [
{
key: AuthorizeAccessComponent.Scopes.Public.key,
text: AuthorizeAccessComponent.Scopes.Public.text,
ariaLabel: AuthorizeAccessComponent.Scopes.Public.text,
},
{
key: AuthorizeAccessComponent.Scopes.PublicAndPrivate.key,
text: AuthorizeAccessComponent.Scopes.PublicAndPrivate.text,
ariaLabel: AuthorizeAccessComponent.Scopes.PublicAndPrivate.text,
},
],
selectedKey: this.state.scope,
onChange: this.onChoiceGroupChange,
};
const buttonProps: IButtonProps = {
text: AuthorizeAccessComponent.AuthorizeButtonText,
ariaLabel: AuthorizeAccessComponent.AuthorizeButtonText,
onClick: this.onButtonClick,
};
return (
<>
<p>{AuthorizeAccessComponent.DescriptionPara1}</p>
<p style={{ marginTop: ChildrenMargin }}>{AuthorizeAccessComponent.DescriptionPara2}</p>
<ChoiceGroup style={{ marginTop: ChildrenMargin }} {...choiceGroupProps} />
<PrimaryButton style={{ marginTop: ChildrenMargin }} {...buttonProps} />
</>
);
}
}
@@ -1,74 +0,0 @@
import { DefaultButton, IButtonProps, Link, PrimaryButton } from "@fluentui/react";
import * as React from "react";
import { IGitHubBranch, IGitHubRepo } from "../../../GitHub/GitHubClient";
import { AddRepoComponent, AddRepoComponentProps } from "./AddRepoComponent";
import { AuthorizeAccessComponent, AuthorizeAccessComponentProps } from "./AuthorizeAccessComponent";
import { ButtonsFooterStyle, ChildrenMargin, ContentFooterStyle } from "./GitHubStyleConstants";
import { ReposListComponent, ReposListComponentProps } from "./ReposListComponent";
export interface GitHubReposComponentProps {
showAuthorizeAccess: boolean;
authorizeAccessProps: AuthorizeAccessComponentProps;
reposListProps: ReposListComponentProps;
addRepoProps: AddRepoComponentProps;
resetConnection: () => void;
onOkClick: () => void;
onCancelClick: () => void;
}
export interface RepoListItem {
key: string;
repo: IGitHubRepo;
branches: IGitHubBranch[];
}
export class GitHubReposComponent extends React.Component<GitHubReposComponentProps> {
private static readonly ManageGitHubRepoDescription =
"Select your GitHub repos and branch(es) to pin to your notebooks workspace.";
private static readonly ManageGitHubRepoResetConnection = "View or change your GitHub authorization settings.";
private static readonly OKButtonText = "OK";
private static readonly CancelButtonText = "Cancel";
public render(): JSX.Element {
const content: JSX.Element = this.props.showAuthorizeAccess ? (
<AuthorizeAccessComponent {...this.props.authorizeAccessProps} />
) : (
<>
<p>{GitHubReposComponent.ManageGitHubRepoDescription}</p>
<Link style={{ marginTop: ChildrenMargin }} onClick={this.props.resetConnection}>
{GitHubReposComponent.ManageGitHubRepoResetConnection}
</Link>
<ReposListComponent {...this.props.reposListProps} />
</>
);
const okProps: IButtonProps = {
text: GitHubReposComponent.OKButtonText,
ariaLabel: GitHubReposComponent.OKButtonText,
onClick: this.props.onOkClick,
};
const cancelProps: IButtonProps = {
text: GitHubReposComponent.CancelButtonText,
ariaLabel: GitHubReposComponent.CancelButtonText,
onClick: this.props.onCancelClick,
};
return (
<>
<div>{content}</div>
{!this.props.showAuthorizeAccess && (
<>
<div className={"paneFooter"} style={ContentFooterStyle}>
<AddRepoComponent {...this.props.addRepoProps} />
</div>
<div className={"paneFooter"} style={ButtonsFooterStyle}>
<PrimaryButton {...okProps} />
<DefaultButton style={{ marginLeft: ChildrenMargin }} {...cancelProps} />
</div>
</>
)}
</>
);
}
}
@@ -1,65 +0,0 @@
import {
ICheckboxStyleProps,
ICheckboxStyles,
IDropdownStyleProps,
IDropdownStyles,
IStyleFunctionOrObject,
} from "@fluentui/react";
export const ButtonsFooterStyle: React.CSSProperties = {
paddingTop: 14,
height: "auto",
borderTop: "2px solid lightGray",
};
export const ContentFooterStyle: React.CSSProperties = {
paddingTop: "10px",
height: "auto",
borderTop: "2px solid lightGray",
};
export const ChildrenMargin = 10;
export const FontSize = 12;
export const ReposListCheckboxStyles: IStyleFunctionOrObject<ICheckboxStyleProps, ICheckboxStyles> = {
label: {
margin: 0,
padding: "2 0 2 0",
},
text: {
fontSize: FontSize,
},
};
export const BranchesDropdownCheckboxStyles: IStyleFunctionOrObject<ICheckboxStyleProps, ICheckboxStyles> = {
label: {
margin: 0,
padding: 0,
fontSize: FontSize,
},
root: {
padding: 0,
},
text: {
fontSize: FontSize,
},
};
export const BranchesDropdownStyles: IStyleFunctionOrObject<IDropdownStyleProps, IDropdownStyles> = {
title: {
fontSize: FontSize,
},
};
export const BranchesDropdownOptionContainerStyle: React.CSSProperties = {
padding: 8,
};
export const ContentMainStyle: React.CSSProperties = {
display: "flex",
flexDirection: "column",
};
export const ReposListRepoColumnMinWidth = 192;
export const ReposListBranchesColumnWidth = 116;
export const BranchesDropdownWidth = 200;
@@ -1,309 +0,0 @@
import {
Checkbox,
DetailsList,
DetailsRow,
Dropdown,
ICheckboxProps,
IDetailsFooterProps,
IDetailsListProps,
IDetailsRowBaseProps,
IDropdown,
IDropdownOption,
IDropdownProps,
ILinkProps,
ISelectableDroppableTextProps,
Link,
ResponsiveMode,
SelectionMode,
Text,
} from "@fluentui/react";
import * as React from "react";
import { IGitHubBranch, IGitHubPageInfo } from "../../../GitHub/GitHubClient";
import * as GitHubUtils from "../../../Utils/GitHubUtils";
import { RepoListItem } from "./GitHubReposComponent";
import {
BranchesDropdownCheckboxStyles,
BranchesDropdownOptionContainerStyle,
BranchesDropdownStyles,
BranchesDropdownWidth,
ReposListBranchesColumnWidth,
ReposListCheckboxStyles,
ReposListRepoColumnMinWidth,
} from "./GitHubStyleConstants";
export interface ReposListComponentProps {
branchesProps: Record<string, BranchesProps>; // key'd by repo key
pinnedReposProps: PinnedReposProps;
unpinnedReposProps: UnpinnedReposProps;
pinRepo: (repo: RepoListItem) => void;
unpinRepo: (repo: RepoListItem) => void;
}
export interface BranchesProps {
branches: IGitHubBranch[];
lastPageInfo?: IGitHubPageInfo;
hasMore: boolean;
isLoading: boolean;
defaultBranchName: string;
loadMore: () => void;
}
export interface PinnedReposProps {
repos: RepoListItem[];
}
export interface UnpinnedReposProps {
repos: RepoListItem[];
hasMore: boolean;
isLoading: boolean;
loadMore: () => void;
}
export class ReposListComponent extends React.Component<ReposListComponentProps> {
private static readonly PinnedReposColumnName = "Pinned repos";
private static readonly UnpinnedReposColumnName = "Unpinned repos";
private static readonly BranchesColumnName = "Branches";
private static readonly LoadingText = "Loading...";
private static readonly LoadMoreText = "Load more";
private static readonly DefaultBranchNames = "master/main";
private static readonly FooterIndex = -1;
public render(): JSX.Element {
const pinnedReposListProps: IDetailsListProps = {
styles: {
contentWrapper: {
height: this.props.pinnedReposProps.repos.length ? undefined : 0,
},
},
items: this.props.pinnedReposProps.repos,
getKey: ReposListComponent.getKey,
selectionMode: SelectionMode.none,
compact: true,
columns: [
{
key: ReposListComponent.PinnedReposColumnName,
name: ReposListComponent.PinnedReposColumnName,
ariaLabel: ReposListComponent.PinnedReposColumnName,
minWidth: ReposListRepoColumnMinWidth,
onRender: this.onRenderPinnedReposColumnItem,
},
{
key: ReposListComponent.BranchesColumnName,
name: ReposListComponent.BranchesColumnName,
ariaLabel: ReposListComponent.BranchesColumnName,
minWidth: ReposListBranchesColumnWidth,
maxWidth: ReposListBranchesColumnWidth,
onRender: this.onRenderPinnedReposBranchesColumnItem,
},
],
onRenderDetailsFooter: this.props.pinnedReposProps.repos.length ? undefined : this.onRenderReposFooter,
};
const unpinnedReposListProps: IDetailsListProps = {
items: this.props.unpinnedReposProps.repos,
getKey: ReposListComponent.getKey,
selectionMode: SelectionMode.none,
compact: true,
columns: [
{
key: ReposListComponent.UnpinnedReposColumnName,
name: ReposListComponent.UnpinnedReposColumnName,
ariaLabel: ReposListComponent.UnpinnedReposColumnName,
minWidth: ReposListRepoColumnMinWidth,
onRender: this.onRenderUnpinnedReposColumnItem,
},
{
key: ReposListComponent.BranchesColumnName,
name: ReposListComponent.BranchesColumnName,
ariaLabel: ReposListComponent.BranchesColumnName,
minWidth: ReposListBranchesColumnWidth,
maxWidth: ReposListBranchesColumnWidth,
onRender: this.onRenderUnpinnedReposBranchesColumnItem,
},
],
onRenderDetailsFooter:
this.props.unpinnedReposProps.isLoading || this.props.unpinnedReposProps.hasMore
? this.onRenderReposFooter
: undefined,
};
return (
<>
<DetailsList {...pinnedReposListProps} />
<DetailsList {...unpinnedReposListProps} />
</>
);
}
private onRenderPinnedReposColumnItem = (item: RepoListItem, index: number): JSX.Element => {
if (index === ReposListComponent.FooterIndex) {
return <Text>None</Text>;
}
const checkboxProps: ICheckboxProps = {
...ReposListComponent.getCheckboxPropsForLabel(GitHubUtils.toRepoFullName(item.repo.owner, item.repo.name)),
styles: ReposListCheckboxStyles,
defaultChecked: true,
onChange: () => this.props.unpinRepo(item),
};
return <Checkbox {...checkboxProps} />;
};
private onRenderPinnedReposBranchesColumnItem = (item: RepoListItem, index: number): JSX.Element => {
if (index === ReposListComponent.FooterIndex) {
return <></>;
}
const branchesProps = this.props.branchesProps[GitHubUtils.toRepoFullName(item.repo.owner, item.repo.name)];
if (item.branches.length === 0 && branchesProps.defaultBranchName) {
item.branches = [{ name: branchesProps.defaultBranchName }];
}
const options: IDropdownOption[] = branchesProps.branches.map((branch) => ({
key: branch.name,
text: branch.name,
data: item,
disabled: item.branches.length === 1 && branch.name === item.branches[0].name,
selected: item.branches.findIndex((element) => element.name === branch.name) !== -1,
}));
if (branchesProps.hasMore || branchesProps.isLoading) {
const text = branchesProps.isLoading ? ReposListComponent.LoadingText : ReposListComponent.LoadMoreText;
options.push({
key: text,
text,
data: item,
index: ReposListComponent.FooterIndex,
});
}
const dropdownProps: IDropdownProps = {
styles: BranchesDropdownStyles,
dropdownWidth: BranchesDropdownWidth,
responsiveMode: ResponsiveMode.large,
options,
onRenderList: this.onRenderBranchesDropdownList,
};
if (item.branches.length === 1) {
dropdownProps.placeholder = item.branches[0].name;
} else if (item.branches.length > 1) {
dropdownProps.placeholder = `${item.branches.length} branches`;
}
return <Dropdown {...dropdownProps} />;
};
private onRenderUnpinnedReposBranchesColumnItem = (item: RepoListItem, index: number): JSX.Element => {
if (index === ReposListComponent.FooterIndex) {
return <></>;
}
const dropdownProps: IDropdownProps = {
styles: BranchesDropdownStyles,
options: [],
placeholder: ReposListComponent.DefaultBranchNames,
disabled: true,
};
return <Dropdown {...dropdownProps} />;
};
private onRenderBranchesDropdownList = (
props: ISelectableDroppableTextProps<IDropdown, HTMLDivElement>,
): JSX.Element => {
const renderedList: JSX.Element[] = [];
props.options.forEach((option: IDropdownOption) => {
const item = (
<div key={option.key} style={BranchesDropdownOptionContainerStyle}>
{this.onRenderPinnedReposBranchesDropdownOption(option)}
</div>
);
renderedList.push(item);
});
return <>{renderedList}</>;
};
private onRenderPinnedReposBranchesDropdownOption(option: IDropdownOption): JSX.Element {
const item: RepoListItem = option.data;
const branchesProps = this.props.branchesProps[GitHubUtils.toRepoFullName(item.repo.owner, item.repo.name)];
if (option.index === ReposListComponent.FooterIndex) {
const linkProps: ILinkProps = {
disabled: branchesProps.isLoading,
onClick: branchesProps.loadMore,
};
return <Link {...linkProps}>{option.text}</Link>;
}
const checkboxProps: ICheckboxProps = {
...ReposListComponent.getCheckboxPropsForLabel(option.text),
styles: BranchesDropdownCheckboxStyles,
defaultChecked: option.selected,
disabled: option.disabled,
onChange: (event, checked) => {
const repoListItem = { ...item };
const branch: IGitHubBranch = { name: option.text };
repoListItem.branches = repoListItem.branches.filter((element) => element.name !== branch.name);
if (checked) {
repoListItem.branches.push(branch);
}
this.props.pinRepo(repoListItem);
},
};
return <Checkbox {...checkboxProps} />;
}
private onRenderUnpinnedReposColumnItem = (item: RepoListItem, index: number): JSX.Element => {
if (index === ReposListComponent.FooterIndex) {
const linkProps: ILinkProps = {
disabled: this.props.unpinnedReposProps.isLoading,
onClick: this.props.unpinnedReposProps.loadMore,
};
const linkText = this.props.unpinnedReposProps.isLoading
? ReposListComponent.LoadingText
: ReposListComponent.LoadMoreText;
return <Link {...linkProps}>{linkText}</Link>;
}
const checkboxProps: ICheckboxProps = {
...ReposListComponent.getCheckboxPropsForLabel(GitHubUtils.toRepoFullName(item.repo.owner, item.repo.name)),
styles: ReposListCheckboxStyles,
onChange: () => {
const repoListItem = { ...item };
repoListItem.branches = [];
this.props.pinRepo(repoListItem);
},
};
return <Checkbox {...checkboxProps} />;
};
private onRenderReposFooter = (detailsFooterProps: IDetailsFooterProps): JSX.Element => {
const props: IDetailsRowBaseProps = {
...detailsFooterProps,
item: {},
itemIndex: ReposListComponent.FooterIndex,
};
return <DetailsRow {...props} />;
};
private static getCheckboxPropsForLabel(label: string): ICheckboxProps {
return {
label,
title: label,
ariaLabel: label,
};
}
private static getKey(item: RepoListItem): string {
return item.key;
}
}
@@ -1,13 +0,0 @@
@import "../../../../less/Common/Constants";
.notebookTerminalContainer {
padding: @DefaultSpace;
height: 100%;
width: 100%;
iframe {
border: none;
height: 100%;
width: 100%;
}
}
@@ -1,146 +0,0 @@
import { shallow } from "enzyme";
import React from "react";
import * as DataModels from "../../../Contracts/DataModels";
import { NotebookTerminalComponent, NotebookTerminalComponentProps } from "./NotebookTerminalComponent";
const testAccount: DataModels.DatabaseAccount = {
id: "id",
kind: "kind",
location: "location",
name: "name",
properties: {
documentEndpoint: "https://testDocumentEndpoint.azure.com/",
},
type: "type",
};
const testMongo32Account: DataModels.DatabaseAccount = {
...testAccount,
};
const testMongo36Account: DataModels.DatabaseAccount = {
...testAccount,
properties: {
mongoEndpoint: "https://testMongoEndpoint.azure.com/",
},
};
const testCassandraAccount: DataModels.DatabaseAccount = {
...testAccount,
properties: {
cassandraEndpoint: "https://testCassandraEndpoint.azure.com/",
},
};
const testPostgresAccount: DataModels.DatabaseAccount = {
...testAccount,
properties: {
postgresqlEndpoint: "https://testPostgresEndpoint.azure.com/",
},
};
const testVCoreMongoAccount: DataModels.DatabaseAccount = {
...testAccount,
properties: {
vcoreMongoEndpoint: "https://testVCoreMongoEndpoint.azure.com/",
},
};
const testNotebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo = {
authToken: "authToken",
notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com",
forwardingId: "Id",
};
const testMongoNotebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo = {
authToken: "authToken",
notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/mongo",
forwardingId: "Id",
};
const testCassandraNotebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo = {
authToken: "authToken",
notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/cassandra",
forwardingId: "Id",
};
const testPostgresNotebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo = {
authToken: "authToken",
notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/postgresql",
forwardingId: "Id",
};
const testVCoreMongoNotebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo = {
authToken: "authToken",
notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/mongovcore",
forwardingId: "Id",
};
describe("NotebookTerminalComponent", () => {
it("renders terminal", () => {
const props: NotebookTerminalComponentProps = {
databaseAccount: testAccount,
notebookServerInfo: testNotebookServerInfo,
tabId: undefined,
};
const wrapper = shallow(<NotebookTerminalComponent {...props} />);
expect(wrapper).toMatchSnapshot();
});
it("renders mongo 3.2 shell", () => {
const props: NotebookTerminalComponentProps = {
databaseAccount: testMongo32Account,
notebookServerInfo: testMongoNotebookServerInfo,
tabId: undefined,
};
const wrapper = shallow(<NotebookTerminalComponent {...props} />);
expect(wrapper).toMatchSnapshot();
});
it("renders mongo 3.6 shell", () => {
const props: NotebookTerminalComponentProps = {
databaseAccount: testMongo36Account,
notebookServerInfo: testMongoNotebookServerInfo,
tabId: undefined,
};
const wrapper = shallow(<NotebookTerminalComponent {...props} />);
expect(wrapper).toMatchSnapshot();
});
it("renders cassandra shell", () => {
const props: NotebookTerminalComponentProps = {
databaseAccount: testCassandraAccount,
notebookServerInfo: testCassandraNotebookServerInfo,
tabId: undefined,
};
const wrapper = shallow(<NotebookTerminalComponent {...props} />);
expect(wrapper).toMatchSnapshot();
});
it("renders Postgres shell", () => {
const props: NotebookTerminalComponentProps = {
databaseAccount: testPostgresAccount,
notebookServerInfo: testPostgresNotebookServerInfo,
tabId: undefined,
};
const wrapper = shallow(<NotebookTerminalComponent {...props} />);
expect(wrapper).toMatchSnapshot();
});
it("renders vCore Mongo shell", () => {
const props: NotebookTerminalComponentProps = {
databaseAccount: testVCoreMongoAccount,
notebookServerInfo: testVCoreMongoNotebookServerInfo,
tabId: undefined,
username: "username",
};
const wrapper = shallow(<NotebookTerminalComponent {...props} />);
expect(wrapper).toMatchSnapshot();
});
});
@@ -1,99 +0,0 @@
/**
* Wrapper around Notebook server terminal
*/
import { useTerminal } from "hooks/useTerminal";
import postRobot from "post-robot";
import * as React from "react";
import * as DataModels from "../../../Contracts/DataModels";
import { TerminalProps } from "../../../Terminal/TerminalProps";
import { userContext } from "../../../UserContext";
import * as StringUtils from "../../../Utils/StringUtils";
export interface NotebookTerminalComponentProps {
notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo;
databaseAccount: DataModels.DatabaseAccount;
tabId: string;
username?: string;
}
export class NotebookTerminalComponent extends React.Component<NotebookTerminalComponentProps> {
private terminalWindow: Window;
constructor(props: NotebookTerminalComponentProps) {
super(props);
}
componentDidMount(): void {
this.sendPropsToTerminalFrame();
}
public render(): JSX.Element {
return (
<div className="notebookTerminalContainer">
<iframe
title="Terminal to Notebook Server"
onLoad={(event) => this.handleFrameLoad(event)}
src="terminal.html"
/>
</div>
);
}
handleFrameLoad(event: React.SyntheticEvent<HTMLIFrameElement, Event>): void {
this.terminalWindow = (event.target as HTMLIFrameElement).contentWindow;
useTerminal.getState().setTerminal(this.terminalWindow);
this.sendPropsToTerminalFrame();
}
sendPropsToTerminalFrame(): void {
if (!this.terminalWindow) {
return;
}
let props: TerminalProps = {
terminalEndpoint: this.tryGetTerminalEndpoint(),
notebookServerEndpoint: this.props.notebookServerInfo?.notebookServerEndpoint,
authToken: this.props.notebookServerInfo?.authToken,
subscriptionId: userContext.subscriptionId,
apiType: userContext.apiType,
authType: userContext.authType,
databaseAccount: userContext.databaseAccount,
tabId: this.props.tabId,
};
if (this.props.username) {
props = {
...props,
username: this.props.username,
};
}
postRobot.send(this.terminalWindow, "props", props, {
domain: window.location.origin,
});
}
public tryGetTerminalEndpoint(): string | undefined {
let terminalEndpoint: string | undefined;
const notebookServerEndpoint = this.props.notebookServerInfo?.notebookServerEndpoint;
if (StringUtils.endsWith(notebookServerEndpoint, "mongo")) {
// mongoEndpoint is only available for Mongo 3.6 and higher, fallback to documentEndpoint otherwise
terminalEndpoint =
this.props.databaseAccount?.properties.mongoEndpoint || this.props.databaseAccount?.properties.documentEndpoint;
} else if (StringUtils.endsWith(notebookServerEndpoint, "cassandra")) {
terminalEndpoint = this.props.databaseAccount?.properties.cassandraEndpoint;
} else if (StringUtils.endsWith(notebookServerEndpoint, "postgresql")) {
return this.props.databaseAccount?.properties.postgresqlEndpoint;
} else if (StringUtils.endsWith(notebookServerEndpoint, "mongovcore")) {
return this.props.databaseAccount?.properties.vcoreMongoEndpoint;
}
if (terminalEndpoint) {
return new URL(terminalEndpoint).host;
}
return undefined;
}
}
@@ -1,73 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`NotebookTerminalComponent renders Postgres shell 1`] = `
<div
className="notebookTerminalContainer"
>
<iframe
onLoad={[Function]}
src="terminal.html"
title="Terminal to Notebook Server"
/>
</div>
`;
exports[`NotebookTerminalComponent renders cassandra shell 1`] = `
<div
className="notebookTerminalContainer"
>
<iframe
onLoad={[Function]}
src="terminal.html"
title="Terminal to Notebook Server"
/>
</div>
`;
exports[`NotebookTerminalComponent renders mongo 3.2 shell 1`] = `
<div
className="notebookTerminalContainer"
>
<iframe
onLoad={[Function]}
src="terminal.html"
title="Terminal to Notebook Server"
/>
</div>
`;
exports[`NotebookTerminalComponent renders mongo 3.6 shell 1`] = `
<div
className="notebookTerminalContainer"
>
<iframe
onLoad={[Function]}
src="terminal.html"
title="Terminal to Notebook Server"
/>
</div>
`;
exports[`NotebookTerminalComponent renders terminal 1`] = `
<div
className="notebookTerminalContainer"
>
<iframe
onLoad={[Function]}
src="terminal.html"
title="Terminal to Notebook Server"
/>
</div>
`;
exports[`NotebookTerminalComponent renders vCore Mongo shell 1`] = `
<div
className="notebookTerminalContainer"
>
<iframe
onLoad={[Function]}
src="terminal.html"
title="Terminal to Notebook Server"
/>
</div>
`;
@@ -1,11 +0,0 @@
.notebookViewerMetadataContainer {
margin: 0px 10px;
.title, .decoration, .persona {
display: inline-block;
}
.extras {
margin-top: 5px;
}
}
@@ -1,63 +0,0 @@
import { shallow } from "enzyme";
import React from "react";
import { NotebookMetadataComponent, NotebookMetadataComponentProps } from "./NotebookMetadataComponent";
describe("NotebookMetadataComponent", () => {
it("renders un-liked notebook", () => {
const props: NotebookMetadataComponentProps = {
data: {
id: "id",
name: "name",
description: "description",
author: "author",
thumbnailUrl: "thumbnailUrl",
created: "created",
gitSha: "gitSha",
tags: ["tag"],
isSample: false,
downloads: 0,
favorites: 0,
views: 0,
newCellId: undefined,
policyViolations: undefined,
pendingScanJobIds: undefined,
},
isFavorite: false,
downloadButtonText: "Download",
onTagClick: undefined,
};
const wrapper = shallow(<NotebookMetadataComponent {...props} />);
expect(wrapper).toMatchSnapshot();
});
it("renders liked notebook", () => {
const props: NotebookMetadataComponentProps = {
data: {
id: "id",
name: "name",
description: "description",
author: "author",
thumbnailUrl: "thumbnailUrl",
created: "created",
gitSha: "gitSha",
tags: ["tag"],
isSample: false,
downloads: 0,
favorites: 0,
views: 0,
newCellId: undefined,
policyViolations: undefined,
pendingScanJobIds: undefined,
},
isFavorite: true,
downloadButtonText: "Download",
onTagClick: undefined,
};
const wrapper = shallow(<NotebookMetadataComponent {...props} />);
expect(wrapper).toMatchSnapshot();
});
// TODO Add test for metadata display
});
@@ -1,77 +0,0 @@
/**
* Wrapper around Notebook metadata
*/
import { FontWeights, Icon, Link, Persona, PersonaSize, Stack, Text } from "@fluentui/react";
import * as React from "react";
import { IGalleryItem } from "../../../Juno/JunoClient";
import * as FileSystemUtil from "../../Notebook/FileSystemUtil";
import "./NotebookViewerComponent.less";
import CosmosDBLogo from "../../../../images/CosmosDB-logo.svg";
export interface NotebookMetadataComponentProps {
data: IGalleryItem;
isFavorite: boolean;
downloadButtonText?: string;
onTagClick: (tag: string) => void;
}
export class NotebookMetadataComponent extends React.Component<NotebookMetadataComponentProps> {
public render(): JSX.Element {
const options: Intl.DateTimeFormatOptions = {
year: "numeric",
month: "short",
day: "numeric",
};
const dateString = new Date(this.props.data.created).toLocaleString("default", options);
return (
<Stack tokens={{ childrenGap: 10 }}>
<Stack horizontal verticalAlign="center" tokens={{ childrenGap: 30 }}>
<Stack.Item>
<Text variant="xxLarge" nowrap>
{FileSystemUtil.stripExtension(this.props.data.name, "ipynb")}
</Text>
</Stack.Item>
<Stack.Item>
<Text>
<Icon iconName="Heart" /> {this.props.data.favorites} likes
</Text>
</Stack.Item>
</Stack>
<Stack horizontal verticalAlign="center" tokens={{ childrenGap: 10 }}>
<Persona
imageUrl={this.props.data.isSample && CosmosDBLogo}
text={this.props.data.author}
size={PersonaSize.size32}
/>
<Text>{dateString}</Text>
<Text>
<Icon iconName="RedEye" /> {this.props.data.views}
</Text>
<Text>
<Icon iconName="Download" />
{this.props.data.downloads}
</Text>
</Stack>
<Text nowrap>
{this.props.data.tags?.map((tag, index, array) => (
<span key={tag}>
<Link onClick={(): void => this.props.onTagClick(tag)}>{tag}</Link>
{index === array.length - 1 ? <></> : ", "}
</span>
))}
</Text>
<Text variant="large" styles={{ root: { fontWeight: FontWeights.semibold } }}>
Description
</Text>
<Text>{this.props.data.description}</Text>
</Stack>
);
}
}
@@ -1,8 +0,0 @@
@import "../../../../less/Common/Constants";
.notebookViewerContainer {
padding: 30px;
height: 100%;
width: 100%;
overflow-y: auto;
}
@@ -1,185 +0,0 @@
/**
* Wrapper around Notebook Viewer Read only content
*/
import { Icon, Link, ProgressIndicator } from "@fluentui/react";
import { Notebook } from "@nteract/commutable";
import { createContentRef } from "@nteract/core";
import * as React from "react";
import { contents } from "rx-jupyter";
import { getErrorMessage, getErrorStack, handleError } from "../../../Common/ErrorHandlingUtils";
import { IGalleryItem, JunoClient } from "../../../Juno/JunoClient";
import { SessionStorageUtility } from "../../../Shared/StorageUtility";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
import { traceFailure, traceStart, traceSuccess } from "../../../Shared/Telemetry/TelemetryProcessor";
import Explorer from "../../Explorer";
import { NotebookClientV2 } from "../../Notebook/NotebookClientV2";
import { NotebookComponentBootstrapper } from "../../Notebook/NotebookComponent/NotebookComponentBootstrapper";
import NotebookReadOnlyRenderer from "../../Notebook/NotebookRenderer/NotebookReadOnlyRenderer";
import { useNotebook } from "../../Notebook/useNotebook";
import { NotebookMetadataComponent } from "./NotebookMetadataComponent";
import "./NotebookViewerComponent.less";
export interface NotebookViewerComponentProps {
container?: Explorer;
junoClient?: JunoClient;
notebookUrl: string;
galleryItem?: IGalleryItem;
isFavorite?: boolean;
backNavigationText: string;
hideInputs?: boolean;
hidePrompts?: boolean;
onBackClick: () => void;
onTagClick: (tag: string) => void;
}
interface NotebookViewerComponentState {
content: Notebook;
galleryItem?: IGalleryItem;
isFavorite?: boolean;
showProgressBar: boolean;
}
export class NotebookViewerComponent extends React.Component<
NotebookViewerComponentProps,
NotebookViewerComponentState
> {
private clientManager: NotebookClientV2;
private notebookComponentBootstrapper: NotebookComponentBootstrapper;
constructor(props: NotebookViewerComponentProps) {
super(props);
this.clientManager = new NotebookClientV2({
connectionInfo: { authToken: undefined, notebookServerEndpoint: undefined, forwardingId: undefined },
databaseAccountName: undefined,
defaultExperience: "NotebookViewer",
isReadOnly: true,
cellEditorType: "codemirror",
autoSaveInterval: 365 * 24 * 3600 * 1000, // There is no way to turn off auto-save, set to 1 year
contentProvider: contents.JupyterContentProvider, // NotebookViewer only knows how to talk to Jupyter contents API
});
this.notebookComponentBootstrapper = new NotebookComponentBootstrapper({
notebookClient: this.clientManager,
contentRef: createContentRef(),
});
this.state = {
content: undefined,
galleryItem: props.galleryItem,
isFavorite: props.isFavorite,
showProgressBar: true,
};
this.loadNotebookContent();
}
private async loadNotebookContent(): Promise<void> {
const startKey = traceStart(Action.NotebooksGalleryViewNotebook, {
notebookUrl: this.props.notebookUrl,
notebookId: this.props.galleryItem?.id,
isSample: this.props.galleryItem?.isSample,
});
try {
const response = await fetch(this.props.notebookUrl);
if (!response.ok) {
this.setState({ showProgressBar: false });
throw new Error(`Received HTTP ${response.status} while fetching ${this.props.notebookUrl}`);
}
traceSuccess(
Action.NotebooksGalleryViewNotebook,
{
notebookUrl: this.props.notebookUrl,
notebookId: this.props.galleryItem?.id,
isSample: this.props.galleryItem?.isSample,
},
startKey,
);
const notebook: Notebook = await response.json();
this.notebookComponentBootstrapper.setContent("json", notebook);
this.setState({ content: notebook, showProgressBar: false });
if (this.props.galleryItem && !SessionStorageUtility.getEntry(this.props.galleryItem.id)) {
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 });
SessionStorageUtility.setEntry(this.props.galleryItem?.id, "true");
}
} catch (error) {
traceFailure(
Action.NotebooksGalleryViewNotebook,
{
notebookUrl: this.props.notebookUrl,
notebookId: this.props.galleryItem?.id,
isSample: this.props.galleryItem?.isSample,
error: getErrorMessage(error),
errorStack: getErrorStack(error),
},
startKey,
);
this.setState({ showProgressBar: false });
handleError(error, "NotebookViewerComponent/loadNotebookContent", "Failed to load notebook content");
}
}
public render(): JSX.Element {
return (
<div className="notebookViewerContainer">
{this.props.backNavigationText !== undefined ? (
<Link onClick={this.props.onBackClick}>
<Icon iconName="Back" /> {this.props.backNavigationText}
</Link>
) : (
<></>
)}
{this.state.galleryItem ? (
<div style={{ margin: 10 }}>
<NotebookMetadataComponent
data={this.state.galleryItem}
isFavorite={this.state.isFavorite}
downloadButtonText={this.props.container && `Download to ${useNotebook.getState().notebookFolderName}`}
onTagClick={this.props.onTagClick}
/>
</div>
) : (
<></>
)}
{this.state.showProgressBar && <ProgressIndicator />}
{this.notebookComponentBootstrapper.renderComponent(NotebookReadOnlyRenderer, {
hideInputs: this.props.hideInputs,
hidePrompts: this.props.hidePrompts,
})}
</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,
};
}
}
@@ -1,197 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`NotebookMetadataComponent renders liked notebook 1`] = `
<Stack
tokens={
{
"childrenGap": 10,
}
}
>
<Stack
horizontal={true}
tokens={
{
"childrenGap": 30,
}
}
verticalAlign="center"
>
<StackItem>
<Text
nowrap={true}
variant="xxLarge"
>
name
</Text>
</StackItem>
<StackItem>
<Text>
<Icon
iconName="Heart"
/>
0
likes
</Text>
</StackItem>
</Stack>
<Stack
horizontal={true}
tokens={
{
"childrenGap": 10,
}
}
verticalAlign="center"
>
<StyledPersonaBase
imageUrl={false}
size={11}
text="author"
/>
<Text>
Invalid Date
</Text>
<Text>
<Icon
iconName="RedEye"
/>
0
</Text>
<Text>
<Icon
iconName="Download"
/>
0
</Text>
</Stack>
<Text
nowrap={true}
>
<span
key="tag"
>
<StyledLinkBase
onClick={[Function]}
>
tag
</StyledLinkBase>
</span>
</Text>
<Text
styles={
{
"root": {
"fontWeight": 600,
},
}
}
variant="large"
>
Description
</Text>
<Text>
description
</Text>
</Stack>
`;
exports[`NotebookMetadataComponent renders un-liked notebook 1`] = `
<Stack
tokens={
{
"childrenGap": 10,
}
}
>
<Stack
horizontal={true}
tokens={
{
"childrenGap": 30,
}
}
verticalAlign="center"
>
<StackItem>
<Text
nowrap={true}
variant="xxLarge"
>
name
</Text>
</StackItem>
<StackItem>
<Text>
<Icon
iconName="Heart"
/>
0
likes
</Text>
</StackItem>
</Stack>
<Stack
horizontal={true}
tokens={
{
"childrenGap": 10,
}
}
verticalAlign="center"
>
<StyledPersonaBase
imageUrl={false}
size={11}
text="author"
/>
<Text>
Invalid Date
</Text>
<Text>
<Icon
iconName="RedEye"
/>
0
</Text>
<Text>
<Icon
iconName="Download"
/>
0
</Text>
</Stack>
<Text
nowrap={true}
>
<span
key="tag"
>
<StyledLinkBase
onClick={[Function]}
>
tag
</StyledLinkBase>
</span>
</Text>
<Text
styles={
{
"root": {
"fontWeight": 600,
},
}
}
variant="large"
>
Description
</Text>
<Text>
description
</Text>
</Stack>
`;
@@ -179,7 +179,8 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
private shouldShowComputedPropertiesEditor: boolean;
private shouldShowIndexingPolicyEditor: boolean;
private shouldShowPartitionKeyEditor: boolean;
private isGlobalSecondaryIndex: boolean;
private isGlobalSecondaryIndexSource: boolean;
private isGlobalSecondaryIndexTarget: boolean;
private isVectorSearchEnabled: boolean;
private isFullTextSearchEnabled: boolean;
private totalThroughputUsed: number;
@@ -197,8 +198,8 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
this.shouldShowComputedPropertiesEditor = userContext.apiType === "SQL";
this.shouldShowIndexingPolicyEditor = userContext.apiType !== "Cassandra" && userContext.apiType !== "Mongo";
this.shouldShowPartitionKeyEditor = userContext.apiType === "SQL" && isRunningOnPublicCloud();
this.isGlobalSecondaryIndex =
!!this.collection?.materializedViewDefinition() || !!this.collection?.materializedViews();
this.isGlobalSecondaryIndexSource = !!this.collection?.materializedViews();
this.isGlobalSecondaryIndexTarget = !!this.collection?.materializedViewDefinition();
this.isVectorSearchEnabled = isVectorSearchEnabled() && !hasDatabaseSharedThroughput(this.collection);
this.isFullTextSearchEnabled = userContext.apiType === "SQL";
@@ -1287,7 +1288,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
collection: this.collection,
database: this.database,
isFixedContainer: this.isFixedContainer,
isGlobalSecondaryIndex: this.isGlobalSecondaryIndex,
isGlobalSecondaryIndexTarget: this.isGlobalSecondaryIndexTarget,
onThroughputChange: this.onThroughputChange,
throughput: this.state.throughput,
throughputBaseline: this.state.throughputBaseline,
@@ -1356,7 +1357,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
isFullTextSearchEnabled: this.isFullTextSearchEnabled,
shouldDiscardContainerPolicies: this.state.shouldDiscardContainerPolicies,
resetShouldDiscardContainerPolicyChange: this.resetShouldDiscardContainerPolicies,
isGlobalSecondaryIndex: this.isGlobalSecondaryIndex,
};
const indexingPolicyComponentProps: IndexingPolicyComponentProps = {
@@ -1509,7 +1509,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
});
}
if (this.isGlobalSecondaryIndex) {
if (this.isGlobalSecondaryIndexTarget || this.isGlobalSecondaryIndexSource) {
tabs.push({
tab: SettingsV2TabTypes.GlobalSecondaryIndexTab,
content: <GlobalSecondaryIndexComponent {...globalSecondaryIndexComponentProps} />,
@@ -27,7 +27,6 @@ export interface ContainerPolicyComponentProps {
isFullTextSearchEnabled: boolean;
shouldDiscardContainerPolicies: boolean;
resetShouldDiscardContainerPolicyChange: () => void;
isGlobalSecondaryIndex?: boolean;
}
export const ContainerPolicyComponent: React.FC<ContainerPolicyComponentProps> = ({
@@ -9,7 +9,7 @@ describe("ScaleComponent", () => {
collection: collection,
database: undefined,
isFixedContainer: false,
isGlobalSecondaryIndex: false,
isGlobalSecondaryIndexTarget: false,
onThroughputChange: () => {
return;
},
@@ -23,7 +23,7 @@ export interface ScaleComponentProps {
collection: ViewModels.Collection;
database: ViewModels.Database;
isFixedContainer: boolean;
isGlobalSecondaryIndex: boolean;
isGlobalSecondaryIndexTarget: boolean;
onThroughputChange: (newThroughput: number) => void;
throughput: number;
throughputBaseline: number;
@@ -147,7 +147,7 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
throughputError={this.props.throughputError}
instantMaximumThroughput={this.offer?.instantMaximumThroughput}
softAllowedMaximumThroughput={this.offer?.softAllowedMaximumThroughput}
isGlobalSecondaryIndex={this.props.isGlobalSecondaryIndex}
isGlobalSecondaryIndexTarget={this.props.isGlobalSecondaryIndexTarget}
/>
);
@@ -44,7 +44,7 @@ describe("ThroughputInputAutoPilotV3Component", () => {
},
instantMaximumThroughput: 5000,
softAllowedMaximumThroughput: 1000000,
isGlobalSecondaryIndex: false,
isGlobalSecondaryIndexTarget: false,
};
it("throughput input visible", () => {
@@ -81,7 +81,7 @@ export interface ThroughputInputAutoPilotV3Props {
throughputError?: string;
instantMaximumThroughput: number;
softAllowedMaximumThroughput: number;
isGlobalSecondaryIndex: boolean;
isGlobalSecondaryIndexTarget: boolean;
}
interface ThroughputInputAutoPilotV3State {
@@ -427,7 +427,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
toolTipElement={getToolTipContainer(this.props.infoBubbleText)}
/>
</Label>
{!this.props.isGlobalSecondaryIndex && (
{!this.props.isGlobalSecondaryIndexTarget && (
<>
{this.overrideWithProvisionedThroughputSettings() && (
<MessageBar
@@ -109,28 +109,17 @@ exports[`SettingsComponent renders 1`] = `
"computedProperties": [Function],
"conflictResolutionPolicy": [Function],
"container": Explorer {
"_isInitializingNotebooks": false,
"databasesRefreshed": Promise {},
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function],
"phoenixClient": PhoenixClient {
"armResourceId": undefined,
"retryOptions": {
"maxTimeout": 5000,
"minTimeout": 5000,
"retries": 3,
},
},
"provideFeedbackEmail": [Function],
"queriesClient": QueriesClient {
"container": [Circular],
},
"refreshNotebookList": [Function],
"resourceTree": ResourceTreeAdapter {
"container": [Circular],
"copyNotebook": [Function],
"parameters": [Function],
},
},
@@ -181,7 +170,7 @@ exports[`SettingsComponent renders 1`] = `
}
isAutoPilotSelected={false}
isFixedContainer={false}
isGlobalSecondaryIndex={true}
isGlobalSecondaryIndexTarget={true}
onAutoPilotSelected={[Function]}
onMaxAutoPilotThroughputChange={[Function]}
onScaleDiscardableChange={[Function]}
@@ -231,28 +220,17 @@ exports[`SettingsComponent renders 1`] = `
"computedProperties": [Function],
"conflictResolutionPolicy": [Function],
"container": Explorer {
"_isInitializingNotebooks": false,
"databasesRefreshed": Promise {},
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function],
"phoenixClient": PhoenixClient {
"armResourceId": undefined,
"retryOptions": {
"maxTimeout": 5000,
"minTimeout": 5000,
"retries": 3,
},
},
"provideFeedbackEmail": [Function],
"queriesClient": QueriesClient {
"container": [Circular],
},
"refreshNotebookList": [Function],
"resourceTree": ResourceTreeAdapter {
"container": [Circular],
"copyNotebook": [Function],
"parameters": [Function],
},
},
@@ -352,7 +330,6 @@ exports[`SettingsComponent renders 1`] = `
fullTextPolicy={{}}
fullTextPolicyBaseline={{}}
isFullTextSearchEnabled={true}
isGlobalSecondaryIndex={true}
isVectorSearchEnabled={false}
onFullTextPolicyChange={[Function]}
onFullTextPolicyDirtyChange={[Function]}
@@ -458,28 +435,17 @@ exports[`SettingsComponent renders 1`] = `
"computedProperties": [Function],
"conflictResolutionPolicy": [Function],
"container": Explorer {
"_isInitializingNotebooks": false,
"databasesRefreshed": Promise {},
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function],
"phoenixClient": PhoenixClient {
"armResourceId": undefined,
"retryOptions": {
"maxTimeout": 5000,
"minTimeout": 5000,
"retries": 3,
},
},
"provideFeedbackEmail": [Function],
"queriesClient": QueriesClient {
"container": [Circular],
},
"refreshNotebookList": [Function],
"resourceTree": ResourceTreeAdapter {
"container": [Circular],
"copyNotebook": [Function],
"parameters": [Function],
},
},
@@ -530,28 +496,17 @@ exports[`SettingsComponent renders 1`] = `
}
explorer={
Explorer {
"_isInitializingNotebooks": false,
"databasesRefreshed": Promise {},
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function],
"phoenixClient": PhoenixClient {
"armResourceId": undefined,
"retryOptions": {
"maxTimeout": 5000,
"minTimeout": 5000,
"retries": 3,
},
},
"provideFeedbackEmail": [Function],
"queriesClient": QueriesClient {
"container": [Circular],
},
"refreshNotebookList": [Function],
"resourceTree": ResourceTreeAdapter {
"container": [Circular],
"copyNotebook": [Function],
"parameters": [Function],
},
}
@@ -699,28 +654,17 @@ exports[`SettingsComponent renders 1`] = `
"computedProperties": [Function],
"conflictResolutionPolicy": [Function],
"container": Explorer {
"_isInitializingNotebooks": false,
"databasesRefreshed": Promise {},
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function],
"phoenixClient": PhoenixClient {
"armResourceId": undefined,
"retryOptions": {
"maxTimeout": 5000,
"minTimeout": 5000,
"retries": 3,
},
},
"provideFeedbackEmail": [Function],
"queriesClient": QueriesClient {
"container": [Circular],
},
"refreshNotebookList": [Function],
"resourceTree": ResourceTreeAdapter {
"container": [Circular],
"copyNotebook": [Function],
"parameters": [Function],
},
},
@@ -771,28 +715,17 @@ exports[`SettingsComponent renders 1`] = `
}
explorer={
Explorer {
"_isInitializingNotebooks": false,
"databasesRefreshed": Promise {},
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function],
"phoenixClient": PhoenixClient {
"armResourceId": undefined,
"retryOptions": {
"maxTimeout": 5000,
"minTimeout": 5000,
"retries": 3,
},
},
"provideFeedbackEmail": [Function],
"queriesClient": QueriesClient {
"container": [Circular],
},
"refreshNotebookList": [Function],
"resourceTree": ResourceTreeAdapter {
"container": [Circular],
"copyNotebook": [Function],
"parameters": [Function],
},
}
@@ -19,7 +19,7 @@ export interface ThroughputInputProps {
isFreeTier: boolean;
showFreeTierExceedThroughputTooltip: boolean;
isQuickstart?: boolean;
isGlobalSecondaryIndex?: boolean;
isGlobalSecondaryIndexTarget?: boolean;
setThroughputValue: (throughput: number) => void;
setIsAutoscale: (isAutoscale: boolean) => void;
setIsThroughputCapExceeded: (isThroughputCapExceeded: boolean) => void;
@@ -32,7 +32,7 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
isFreeTier,
showFreeTierExceedThroughputTooltip,
isQuickstart,
isGlobalSecondaryIndex,
isGlobalSecondaryIndexTarget,
setThroughputValue,
setIsAutoscale,
setIsThroughputCapExceeded,
@@ -202,7 +202,7 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
</Text>
<InfoTooltip>{PricingUtils.getRuToolTipText()}</InfoTooltip>
</Stack>
{!isGlobalSecondaryIndex && (
{!isGlobalSecondaryIndexTarget && (
<Stack horizontal verticalAlign="center">
<div role="radiogroup">
<input
@@ -2,6 +2,7 @@ import {
DefaultButton,
Dropdown,
IDropdownOption,
ILabelStyleProps,
IStyleFunctionOrObject,
ITextFieldStyleProps,
ITextFieldStyles,
@@ -18,6 +19,7 @@ import {
getIndexTypeOptions,
getQuantizerTypeOptions,
supportsQuantization,
supportsQuantizationByteSize,
} from "Explorer/Controls/VectorSearch/VectorSearchUtils";
import { Keys, t } from "Localization";
import React, { FunctionComponent, useState } from "react";
@@ -33,7 +35,7 @@ export interface IVectorEmbeddingPoliciesComponentProps {
vectorIndexes?: VectorIndex[];
discardChanges?: boolean;
onChangesDiscarded?: () => void;
isGlobalSecondaryIndex?: boolean;
isGlobalSecondaryIndexTarget?: boolean;
}
export interface VectorEmbeddingPolicyData {
@@ -54,11 +56,13 @@ export interface VectorEmbeddingPolicyData {
type VectorEmbeddingPolicyProperty = "dataType" | "distanceFunction" | "indexType";
const labelStyles = {
root: {
fontSize: 12,
color: "var(--colorNeutralForeground1)",
},
const labelStyles = (props: ILabelStyleProps) => {
return {
root: {
fontSize: 12,
color: props.disabled ? "var(--colorNeutralForeground3)" : "var(--colorNeutralForeground1)",
},
};
};
const textFieldStyles: IStyleFunctionOrObject<ITextFieldStyleProps, ITextFieldStyles> = {
@@ -95,7 +99,7 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
onVectorEmbeddingChange,
discardChanges,
onChangesDiscarded,
isGlobalSecondaryIndex,
isGlobalSecondaryIndexTarget,
}): JSX.Element => {
const isExistingPolicy = (policy: VectorEmbeddingPolicyData): boolean => {
if (!vectorEmbeddingsBaseline || vectorEmbeddingsBaseline.length === 0) {
@@ -166,7 +170,7 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
indexType: matchingType || "none",
indexingSearchListSize: matchingIndex?.indexingSearchListSize || undefined,
quantizationByteSize: matchingIndex?.quantizationByteSize || undefined,
quantizerType: supportsQuantizer ? matchingIndex?.quantizerType || "product" : undefined,
quantizerType: supportsQuantizer ? matchingIndex?.quantizerType || "spherical" : undefined,
vectorIndexShardKey: matchingIndex?.vectorIndexShardKey || undefined,
pathError: onVectorEmbeddingPathError(embedding.path),
dimensionsError: onVectorEmbeddingDimensionError(embedding.dimensions, matchingIndex?.type || "none"),
@@ -256,16 +260,23 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
vectorEmbedding.indexingSearchListSize = undefined;
}
if (supportsQuantization(vectorEmbedding.indexType)) {
vectorEmbedding.quantizerType = vectorEmbedding.quantizerType || "product";
vectorEmbedding.quantizerType = vectorEmbedding.quantizerType || "spherical";
} else {
vectorEmbedding.quantizerType = undefined;
}
if (!supportsQuantizationByteSize(vectorEmbedding.quantizerType)) {
vectorEmbedding.quantizationByteSize = undefined;
}
setVectorEmbeddingPolicyData(vectorEmbeddings);
};
const onQuantizerTypeChange = (index: number, option: IDropdownOption): void => {
const vectorEmbeddings = [...vectorEmbeddingPolicyData];
vectorEmbeddings[index].quantizerType = option.key as VectorIndex["quantizerType"];
const quantizerType = option.key as VectorIndex["quantizerType"];
vectorEmbeddings[index].quantizerType = quantizerType;
if (!supportsQuantizationByteSize(quantizerType)) {
vectorEmbeddings[index].quantizationByteSize = undefined;
}
setVectorEmbeddingPolicyData(vectorEmbeddings);
};
@@ -327,7 +338,7 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
};
const getQuantizationByteSizeTooltipContent = (): string => {
const containerName = isGlobalSecondaryIndex
const containerName = isGlobalSecondaryIndexTarget
? t(Keys.controls.vectorEmbeddingPolicies.quantizationByteSizeTooltipGlobalSecondaryIndexName)
: t(Keys.controls.vectorEmbeddingPolicies.quantizationByteSizeTooltipContainerName);
return t(Keys.controls.vectorEmbeddingPolicies.quantizationByteSizeTooltip, { containerName });
@@ -434,30 +445,6 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
onVectorEmbeddingIndexTypeChange(index, option)
}
></Dropdown>
<Stack style={{ marginLeft: "10px" }}>
<Label
disabled={
isExistingPolicy(vectorEmbeddingPolicy) ||
!supportsQuantization(vectorEmbeddingPolicy.indexType)
}
styles={labelStyles}
>
{t(Keys.controls.vectorEmbeddingPolicies.quantizationByteSize)}
<InfoTooltip>{getQuantizationByteSizeTooltipContent()}</InfoTooltip>
</Label>
<TextField
disabled={
isExistingPolicy(vectorEmbeddingPolicy) ||
!supportsQuantization(vectorEmbeddingPolicy.indexType)
}
id={`vector-policy-quantizationByteSize-${index + 1}`}
styles={textFieldStyles}
value={String(vectorEmbeddingPolicy.quantizationByteSize || "")}
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
onQuantizationByteSizeChange(index, event)
}
/>
</Stack>
<Stack style={{ marginLeft: "10px" }}>
<Label
disabled={
@@ -483,6 +470,32 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
}
/>
</Stack>
<Stack style={{ marginLeft: "10px" }}>
<Label
disabled={
isExistingPolicy(vectorEmbeddingPolicy) ||
!supportsQuantization(vectorEmbeddingPolicy.indexType) ||
!supportsQuantizationByteSize(vectorEmbeddingPolicy.quantizerType)
}
styles={labelStyles}
>
{t(Keys.controls.vectorEmbeddingPolicies.quantizationByteSize)}
<InfoTooltip>{getQuantizationByteSizeTooltipContent()}</InfoTooltip>
</Label>
<TextField
disabled={
isExistingPolicy(vectorEmbeddingPolicy) ||
!supportsQuantization(vectorEmbeddingPolicy.indexType) ||
!supportsQuantizationByteSize(vectorEmbeddingPolicy.quantizerType)
}
id={`vector-policy-quantizationByteSize-${index + 1}`}
styles={textFieldStyles}
value={String(vectorEmbeddingPolicy.quantizationByteSize || "")}
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
onQuantizationByteSizeChange(index, event)
}
/>
</Stack>
<Stack style={{ marginLeft: "10px" }}>
<Label
disabled={
@@ -1,6 +1,5 @@
import { IDropdownOption } from "@fluentui/react";
import { VectorIndex } from "Contracts/DataModels";
import { Keys, t } from "Localization";
const dataTypes = ["float32", "uint8", "int8", "float16"];
const distanceFunctions = ["euclidean", "cosine", "dotproduct"];
@@ -10,13 +9,16 @@ export const getDataTypeOptions = (): IDropdownOption[] => createDropdownOptions
export const getDistanceFunctionOptions = (): IDropdownOption[] => createDropdownOptionsFromLiterals(distanceFunctions);
export const getIndexTypeOptions = (): IDropdownOption[] => createDropdownOptionsFromLiterals(indexTypes);
export const getQuantizerTypeOptions = (): IDropdownOption[] => [
{ key: "spherical", text: "Spherical" },
{ key: "product", text: "Product" },
{ key: "spherical", text: `Spherical (${t(Keys.common.preview)})` },
];
export const supportsQuantization = (indexType: VectorIndex["type"] | "none" | undefined): boolean =>
indexType === "quantizedFlat" || indexType === "diskANN";
export const supportsQuantizationByteSize = (quantizerType: VectorIndex["quantizerType"] | undefined): boolean =>
quantizerType === "product";
function createDropdownOptionsFromLiterals<T extends string>(literals: T[]): IDropdownOption[] {
return literals.map((value) => ({
key: value,
+15 -15
View File
@@ -10,6 +10,7 @@ import { Capability, DatabaseAccount } from "../Contracts/DataModels";
import { updateUserContext, userContext } from "../UserContext";
import { update } from "../Utils/arm/generatedClients/cosmos/databaseAccounts";
import Explorer from "./Explorer";
import { useCommandBar } from "./Menus/CommandBar/CommandBarComponentAdapter";
const mockUpdate = update as jest.MockedFunction<typeof update>;
@@ -26,21 +27,6 @@ jest.mock("./Controls/Dialog", () => ({
},
}));
// Silence useNotebook subscription calls
jest.mock("./Notebook/useNotebook", () => ({
useNotebook: {
subscribe: jest.fn(),
getState: jest.fn().mockReturnValue(
new Proxy(
{},
{
get: () => jest.fn().mockResolvedValue(undefined),
},
),
),
},
}));
describe("Explorer.openEnableSynapseLinkDialog", () => {
let explorer: Explorer;
@@ -69,6 +55,7 @@ describe("Explorer.openEnableSynapseLinkDialog", () => {
beforeEach(() => {
jest.clearAllMocks();
mockUpdate.mockResolvedValue(undefined);
useCommandBar.getState().setIsSynapseLinkUpdating(false);
explorer = new Explorer();
});
@@ -102,6 +89,19 @@ describe("Explorer.openEnableSynapseLinkDialog", () => {
expect(userContext.databaseAccount.properties.enableAnalyticalStorage).toBe(true);
});
it("should track Synapse Link update progress in the command bar store", async () => {
mockUpdate.mockImplementation(async () => {
expect(useCommandBar.getState().isSynapseLinkUpdating).toBe(true);
return {} as Awaited<ReturnType<typeof update>>;
});
explorer.openEnableSynapseLinkDialog();
const dialogProps = mockOpenDialog.mock.calls[0][0];
await dialogProps.onPrimaryButtonClick();
expect(useCommandBar.getState().isSynapseLinkUpdating).toBe(false);
});
});
describe("with targetAccountOverride", () => {
+12 -583
View File
@@ -1,9 +1,6 @@
import * as msal from "@azure/msal-browser";
import { Link } from "@fluentui/react/lib/Link";
import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility";
import { sendMessage } from "Common/MessageHandler";
import { stringifyError } from "Common/stringifyError";
import { Platform, configContext } from "ConfigContext";
import { MessageTypes } from "Contracts/ExplorerContracts";
import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane";
import {
@@ -13,58 +10,42 @@ import {
scheduleRefreshFabricToken,
} from "Platform/Fabric/FabricUtil";
import { acquireMsalTokenForAccount } from "Utils/AuthorizationUtils";
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointUtils";
import { featureRegistered } from "Utils/FeatureRegistrationUtils";
import { update } from "Utils/arm/generatedClients/cosmos/databaseAccounts";
import * as ko from "knockout";
import React from "react";
import _ from "underscore";
import shallow from "zustand/shallow";
import { AuthType } from "../AuthType";
import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer";
import * as Constants from "../Common/Constants";
import { Areas, ConnectionStatusType, HttpStatusCodes, Notebook } from "../Common/Constants";
import { getErrorMessage, getErrorStack, handleError } from "../Common/ErrorHandlingUtils";
import { getErrorMessage, getErrorStack } from "../Common/ErrorHandlingUtils";
import * as Logger from "../Common/Logger";
import { QueriesClient } from "../Common/QueriesClient";
import { readCollection } from "../Common/dataAccess/readCollection";
import { readDatabases } from "../Common/dataAccess/readDatabases";
import * as DataModels from "../Contracts/DataModels";
import { ContainerConnectionInfo, IPhoenixServiceInfo, IProvisionData, IResponse } from "../Contracts/DataModels";
import * as ViewModels from "../Contracts/ViewModels";
import { UploadDetailsRecord } from "../Contracts/ViewModels";
import { GitHubOAuthService } from "../GitHub/GitHubOAuthService";
import MetricScenario from "../Metrics/MetricEvents";
import { ApplicationMetricPhase } from "../Metrics/ScenarioConfig";
import { scenarioMonitor } from "../Metrics/ScenarioMonitor";
import { PhoenixClient } from "../Phoenix/PhoenixClient";
import * as ExplorerSettings from "../Shared/ExplorerSettings";
import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants";
import { Action } from "../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
import { updateUserContext, userContext } from "../UserContext";
import { getCollectionName, getUploadName } from "../Utils/APITypeUtils";
import { stringToBlob } from "../Utils/BlobUtils";
import { isCapabilityEnabled } from "../Utils/CapabilityUtils";
import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils";
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils";
import { useSidePanel } from "../hooks/useSidePanel";
import { ReactTabKind, useTabs } from "../hooks/useTabs";
import "./ComponentRegisterer";
import { DialogProps, useDialog } from "./Controls/Dialog";
import { useCommandBar } from "./Menus/CommandBar/CommandBarComponentAdapter";
import * as FileSystemUtil from "./Notebook/FileSystemUtil";
import { NotebookContentItem, NotebookContentItemType } from "./Notebook/NotebookContentItem";
import type NotebookManager from "./Notebook/NotebookManager";
import { NotebookUtil } from "./Notebook/NotebookUtil";
import { useNotebook } from "./Notebook/useNotebook";
import { AddCollectionPanel } from "./Panes/AddCollectionPanel/AddCollectionPanel";
import { CassandraAddCollectionPane } from "./Panes/CassandraAddCollectionPane/CassandraAddCollectionPane";
import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane/ExecuteSprocParamsPane";
import { StringInputPane } from "./Panes/StringInputPane/StringInputPane";
import { UploadItemsPane } from "./Panes/UploadItemsPane/UploadItemsPane";
import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient";
import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab";
import TabsBase from "./Tabs/TabsBase";
import TerminalTab from "./Tabs/TerminalTab";
import Database from "./Tree/Database";
@@ -87,19 +68,7 @@ export default class Explorer {
// Tabs
public isTabsContentExpanded: ko.Observable<boolean>;
public gitHubOAuthService: GitHubOAuthService;
// Notebooks
public notebookManager?: NotebookManager;
private _isInitializingNotebooks: boolean;
private notebookToImport: {
name: string;
content: string;
};
private static readonly MaxNbDatabasesToAutoExpand = 5;
public phoenixClient: PhoenixClient;
/**
* Resolves when the initial refreshAllDatabases (including collection loading) completes.
@@ -110,13 +79,6 @@ export default class Explorer {
const startKey: number = TelemetryProcessor.traceStart(Action.InitializeDataExplorer, {
dataExplorerArea: Constants.Areas.ResourceTree,
});
this._isInitializingNotebooks = false;
this.phoenixClient = new PhoenixClient(userContext?.databaseAccount?.id);
useNotebook.subscribe(
() => this.refreshCommandBarButtons(),
(state) => state.isNotebooksEnabledForAccount,
);
this.queriesClient = new QueriesClient(this);
@@ -171,31 +133,8 @@ export default class Explorer {
startKey,
);
useNotebook.subscribe(
async () => this.initiateAndRefreshNotebookList(),
(state) => [state.isNotebookEnabled, state.isRefreshed],
shallow,
);
this.resourceTree = new ResourceTreeAdapter(this);
// Override notebook server parameters from URL parameters
if (
userContext.features.notebookServerUrl &&
validateEndpoint(userContext.features.notebookServerUrl, allowedNotebookServerUrls) &&
userContext.features.notebookServerToken
) {
useNotebook.getState().setNotebookServerInfo({
notebookServerEndpoint: userContext.features.notebookServerUrl,
authToken: userContext.features.notebookServerToken,
forwardingId: undefined,
});
}
if (userContext.features.notebookBasePath) {
useNotebook.getState().setNotebookBasePath(userContext.features.notebookBasePath);
}
if (isFabricMirrored()) {
useTabs.getState().closeReactTab(ReactTabKind.Home);
}
@@ -203,23 +142,6 @@ export default class Explorer {
this.refreshExplorer();
}
public async initiateAndRefreshNotebookList(): Promise<void> {
if (!this.notebookManager) {
const NotebookManager = (await import(/* webpackChunkName: "NotebookManager" */ "./Notebook/NotebookManager"))
.default;
this.notebookManager = new NotebookManager();
this.notebookManager.initialize({
container: this,
resourceTree: this.resourceTree,
refreshCommandBarButtons: () => this.refreshCommandBarButtons(),
refreshNotebookList: () => this.refreshNotebookList(),
});
}
this.refreshCommandBarButtons();
this.refreshNotebookList();
}
public openEnableSynapseLinkDialog(targetAccountOverride?: DataModels.AccountOverride): void {
const subscriptionId = targetAccountOverride?.subscriptionId ?? userContext.subscriptionId;
const resourceGroup = targetAccountOverride?.resourceGroup ?? userContext.resourceGroup;
@@ -242,7 +164,7 @@ export default class Explorer {
const clearInProgressMessage = logConsoleProgress(
"Enabling Azure Synapse Link for this account. This may take a few minutes before you can enable analytical store for this account.",
);
useNotebook.getState().setIsSynapseLinkUpdating(true);
useCommandBar.getState().setIsSynapseLinkUpdating(true);
useDialog.getState().closeDialog();
try {
@@ -263,7 +185,7 @@ export default class Explorer {
logConsoleError(`Enabling Azure Synapse Link for this account failed. ${getErrorMessage(error)}`);
TelemetryProcessor.traceFailure(Action.EnableAzureSynapseLink, {}, startTime);
} finally {
useNotebook.getState().setIsSynapseLinkUpdating(false);
useCommandBar.getState().setIsSynapseLinkUpdating(false);
}
},
@@ -446,7 +368,6 @@ export default class Explorer {
await (userContext.authType === AuthType.ResourceToken
? this.refreshDatabaseForResourceToken()
: this.refreshAllDatabases());
await this.refreshNotebookList();
}
logConsoleInfo("Successfully refreshed databases");
@@ -457,108 +378,6 @@ export default class Explorer {
window.open(Constants.Urls.feedbackEmail, "_blank");
};
public async initNotebooks(databaseAccount: DataModels.DatabaseAccount): Promise<void> {
if (!databaseAccount) {
throw new Error("No database account specified");
}
if (this._isInitializingNotebooks) {
return;
}
this._isInitializingNotebooks = true;
this.refreshNotebookList();
this._isInitializingNotebooks = false;
}
public async allocateContainer(): Promise<void> {
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
const isAllocating = useNotebook.getState().isAllocating;
if (
isAllocating === false &&
(notebookServerInfo === undefined ||
(notebookServerInfo && notebookServerInfo.notebookServerEndpoint === undefined))
) {
const connectionStatus: ContainerConnectionInfo = {
status: ConnectionStatusType.Connecting,
};
useNotebook.getState().setConnectionInfo(connectionStatus);
let connectionInfo;
try {
TelemetryProcessor.traceStart(Action.PhoenixConnection, {
dataExplorerArea: Areas.Notebook,
});
useNotebook.getState().setIsAllocating(true);
const provisionData: IProvisionData = {
cosmosEndpoint: userContext?.databaseAccount?.properties?.documentEndpoint,
poolId: undefined,
};
connectionInfo = await this.phoenixClient.allocateContainer(provisionData);
if (!connectionInfo?.data?.phoenixServiceUrl) {
throw new Error(`PhoenixServiceUrl is invalid!`);
}
await this.setNotebookInfo(connectionInfo, connectionStatus);
TelemetryProcessor.traceSuccess(Action.PhoenixConnection, {
dataExplorerArea: Areas.Notebook,
});
} catch (error) {
TelemetryProcessor.traceFailure(Action.PhoenixConnection, {
dataExplorerArea: Areas.Notebook,
status: error.status,
error: getErrorMessage(error),
errorStack: getErrorStack(error),
});
connectionStatus.status = ConnectionStatusType.Failed;
useNotebook.getState().resetContainerConnection(connectionStatus);
if (error?.status === HttpStatusCodes.Forbidden && error.message) {
useDialog.getState().showOkModalDialog("Connection Failed", `${error.message}`);
} else {
useDialog
.getState()
.showOkModalDialog(
"Connection Failed",
"We are unable to connect to the temporary workspace. Please try again in a few minutes. If the error persists, file a support ticket.",
);
}
throw error;
} finally {
useNotebook.getState().setIsAllocating(false);
this.refreshCommandBarButtons();
this.refreshNotebookList();
this._isInitializingNotebooks = false;
}
}
}
public async setNotebookInfo(
connectionInfo: IResponse<IPhoenixServiceInfo>,
connectionStatus: DataModels.ContainerConnectionInfo,
): Promise<void> {
const containerData = {
forwardingId: connectionInfo.data.forwardingId,
dbAccountName: userContext.databaseAccount.name,
};
await this.phoenixClient.initiateContainerHeartBeat(true, containerData);
connectionStatus.status = ConnectionStatusType.Connected;
useNotebook.getState().setConnectionInfo(connectionStatus);
const noteBookServerInfo = {
notebookServerEndpoint:
(validateEndpoint(userContext.features.notebookServerUrl, allowedNotebookServerUrls) &&
userContext.features.notebookServerUrl) ||
connectionInfo.data.phoenixServiceUrl,
authToken: userContext.features.notebookServerToken || connectionInfo.data.authToken,
forwardingId: connectionInfo.data.forwardingId,
};
useNotebook.getState().setNotebookServerInfo(noteBookServerInfo);
this.notebookManager?.notebookClient
.getMemoryUsage()
.then((memoryUsageInfo) => useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo));
}
private getDeltaDatabases(
updatedDatabaseList: DataModels.Database[],
databases: ViewModels.Database[],
@@ -652,332 +471,8 @@ export default class Explorer {
}
}
public uploadFile(
name: string,
content: string,
parent: NotebookContentItem,
isGithubTree?: boolean,
): Promise<NotebookContentItem> {
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
const error = "Attempt to upload notebook, but notebook is not enabled";
handleError(error, "Explorer/uploadFile");
throw new Error(error);
}
const promise = this.notebookManager?.notebookContentClient.uploadFileAsync(name, content, parent, isGithubTree);
promise
.then(() => this.resourceTree.triggerRender())
.catch((reason) => useDialog.getState().showOkModalDialog("Unable to upload file", getErrorMessage(reason)));
return promise;
}
public async importAndOpen(path: string): Promise<boolean> {
const name = NotebookUtil.getName(path);
const item = NotebookUtil.createNotebookContentItem(name, path, "file");
const parent = this.resourceTree.myNotebooksContentRoot;
if (parent && parent.children && useNotebook.getState().isNotebookEnabled && this.notebookManager?.notebookClient) {
const existingItem = _.find(parent.children, (node) => node.name === name);
if (existingItem) {
return this.openNotebook(existingItem);
}
const content = await this.readFile(item);
const uploadedItem = await this.uploadFile(name, content, parent);
return this.openNotebook(uploadedItem);
}
return Promise.resolve(false);
}
public async importAndOpenContent(name: string, content: string): Promise<boolean> {
const parent = this.resourceTree.myNotebooksContentRoot;
if (parent && parent.children && useNotebook.getState().isNotebookEnabled && this.notebookManager?.notebookClient) {
if (this.notebookToImport && this.notebookToImport.name === name && this.notebookToImport.content === content) {
this.notebookToImport = undefined; // we don't want to try opening this notebook again
}
const existingItem = _.find(parent.children, (node) => node.name === name);
if (existingItem) {
return this.openNotebook(existingItem);
}
const uploadedItem = await this.uploadFile(name, content, parent);
return this.openNotebook(uploadedItem);
}
this.notebookToImport = { name, content }; // we'll try opening this notebook later on
return Promise.resolve(false);
}
public copyNotebook(name: string, content: string): void {
this.notebookManager?.openCopyNotebookPane(name, content);
}
/**
* Note: To keep it simple, this creates a disconnected NotebookContentItem that is not connected to the resource tree.
* Connecting it to a tree possibly requires the intermediate missing folders if the item is nested in a subfolder.
* Manually creating the missing folders between the root and its parent dir would break the UX: expanding a folder
* will not fetch its content if the children array exists (and has only one child which was manually created).
* Fetching the intermediate folders possibly involves a few chained async calls which isn't ideal.
*
* @param name
* @param path
*/
public createNotebookContentItemFile(name: string, path: string): NotebookContentItem {
return NotebookUtil.createNotebookContentItem(name, path, "file");
}
public async openNotebook(notebookContentItem: NotebookContentItem): Promise<boolean> {
if (!notebookContentItem || !notebookContentItem.path) {
throw new Error(`Invalid notebookContentItem: ${notebookContentItem}`);
}
if (notebookContentItem.type === NotebookContentItemType.Notebook && useNotebook.getState().isPhoenixNotebooks) {
await this.allocateContainer();
}
const notebookTabs = useTabs
.getState()
.getTabs(
ViewModels.CollectionTabKind.NotebookV2,
(tab) =>
(tab as NotebookV2Tab).notebookPath &&
FileSystemUtil.isPathEqual((tab as NotebookV2Tab).notebookPath(), notebookContentItem.path),
) as NotebookV2Tab[];
let notebookTab = notebookTabs && notebookTabs[0];
if (notebookTab) {
useTabs.getState().activateTab(notebookTab);
} else {
const options: NotebookTabOptions = {
account: userContext.databaseAccount,
tabKind: ViewModels.CollectionTabKind.NotebookV2,
node: undefined,
title: notebookContentItem.name,
tabPath: notebookContentItem.path,
collection: undefined,
masterKey: userContext.masterKey || "",
isTabsContentExpanded: ko.observable(true),
onLoadStartKey: undefined,
container: this,
notebookContentItem,
};
try {
const NotebookTabV2 = await import(/* webpackChunkName: "NotebookV2Tab" */ "./Tabs/NotebookV2Tab");
notebookTab = new NotebookTabV2.default(options);
useTabs.getState().activateNewTab(notebookTab);
} catch (reason) {
console.error("Import NotebookV2Tab failed!", reason);
return false;
}
}
return true;
}
public renameNotebook(notebookFile: NotebookContentItem, isGithubTree?: boolean): void {
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
const error = "Attempt to rename notebook, but notebook is not enabled";
handleError(error, "Explorer/renameNotebook");
throw new Error(error);
}
// Don't delete if tab is open to avoid accidental deletion
const openedNotebookTabs = useTabs
.getState()
.getTabs(ViewModels.CollectionTabKind.NotebookV2, (tab: NotebookV2Tab) => {
return tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), notebookFile.path);
});
if (openedNotebookTabs.length > 0) {
useDialog
.getState()
.showOkModalDialog("Unable to rename file", "This file is being edited. Please close the tab and try again.");
} else {
useSidePanel.getState().openSidePanel(
"Rename Notebook",
<StringInputPane
closePanel={() => {
useSidePanel.getState().closeSidePanel();
this.resourceTree.triggerRender();
}}
inputLabel="Enter new notebook name"
submitButtonLabel="Rename"
errorMessage="Could not rename notebook"
inProgressMessage="Renaming notebook to"
successMessage="Renamed notebook to"
paneTitle="Rename Notebook"
defaultInput={FileSystemUtil.stripExtension(notebookFile.name, "ipynb")}
onSubmit={(notebookFile: NotebookContentItem, input: string): Promise<NotebookContentItem> =>
this.notebookManager?.notebookContentClient.renameNotebook(notebookFile, input, isGithubTree)
}
notebookFile={notebookFile}
/>,
);
}
}
public onCreateDirectory(parent: NotebookContentItem, isGithubTree?: boolean): void {
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
const error = "Attempt to create notebook directory, but notebook is not enabled";
handleError(error, "Explorer/onCreateDirectory");
throw new Error(error);
}
useSidePanel.getState().openSidePanel(
"Create new directory",
<StringInputPane
closePanel={() => {
useSidePanel.getState().closeSidePanel();
this.resourceTree.triggerRender();
}}
errorMessage="Could not create directory "
inProgressMessage="Creating directory "
successMessage="Created directory "
inputLabel="Enter new directory name"
paneTitle="Create new directory"
submitButtonLabel="Create"
defaultInput=""
onSubmit={(notebookFile: NotebookContentItem, input: string): Promise<NotebookContentItem> =>
this.notebookManager?.notebookContentClient.createDirectory(notebookFile, input, isGithubTree)
}
notebookFile={parent}
/>,
);
}
public readFile(notebookFile: NotebookContentItem): Promise<string> {
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
const error = "Attempt to read file, but notebook is not enabled";
handleError(error, "Explorer/downloadFile");
throw new Error(error);
}
return this.notebookManager?.notebookContentClient.readFileContent(notebookFile.path);
}
public downloadFile(notebookFile: NotebookContentItem): Promise<void> {
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
const error = "Attempt to download file, but notebook is not enabled";
handleError(error, "Explorer/downloadFile");
throw new Error(error);
}
const clearMessage = NotificationConsoleUtils.logConsoleProgress(`Downloading ${notebookFile.path}`);
return this.notebookManager?.notebookContentClient.readFileContent(notebookFile.path).then(
(content: string) => {
const blob = stringToBlob(content, "text/plain");
if (navigator.msSaveBlob) {
// for IE and Edge
navigator.msSaveBlob(blob, notebookFile.name);
} else {
const downloadLink: HTMLAnchorElement = document.createElement("a");
const url = URL.createObjectURL(blob);
downloadLink.href = url;
downloadLink.target = "_self";
downloadLink.download = notebookFile.name;
// for some reason, FF displays the download prompt only when
// the link is added to the dom so we add and remove it
document.body.appendChild(downloadLink);
downloadLink.click();
downloadLink.remove();
}
clearMessage();
},
(error) => {
logConsoleError(`Could not download notebook ${getErrorMessage(error)}`);
clearMessage();
},
);
}
private refreshNotebookList = async (): Promise<void> => {
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
return;
}
await this.resourceTree.initialize();
await useNotebook.getState().initializeNotebooksTree(this.notebookManager);
this.notebookManager?.refreshPinnedRepos();
if (this.notebookToImport) {
this.importAndOpenContent(this.notebookToImport.name, this.notebookToImport.content);
}
};
public deleteNotebookFile(item: NotebookContentItem, isGithubTree?: boolean): Promise<void> {
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
const error = "Attempt to delete notebook file, but notebook is not enabled";
handleError(error, "Explorer/deleteNotebookFile");
throw new Error(error);
}
// Don't delete if tab is open to avoid accidental deletion
const openedNotebookTabs = useTabs
.getState()
.getTabs(ViewModels.CollectionTabKind.NotebookV2, (tab: NotebookV2Tab) => {
return tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), item.path);
});
if (openedNotebookTabs.length > 0) {
useDialog
.getState()
.showOkModalDialog("Unable to delete file", "This file is being edited. Please close the tab and try again.");
return Promise.reject();
}
if (item.type === NotebookContentItemType.Directory && item.children && item.children.length > 0) {
useDialog.getState().openDialog({
isModal: true,
title: "Unable to delete file",
subText: "Directory is not empty.",
primaryButtonText: "Close",
secondaryButtonText: undefined,
onPrimaryButtonClick: () => useDialog.getState().closeDialog(),
onSecondaryButtonClick: undefined,
});
return Promise.reject();
}
return this.notebookManager?.notebookContentClient.deleteContentItem(item, isGithubTree).then(
() => logConsoleInfo(`Successfully deleted: ${item.path}`),
(reason) => logConsoleError(`Failed to delete "${item.path}": ${JSON.stringify(reason)}`),
);
}
// TODO: Delete this function when ResourceTreeAdapter is removed.
public async refreshContentItem(item: NotebookContentItem): Promise<void> {
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
const error = "Attempt to refresh notebook list, but notebook is not enabled";
handleError(error, "Explorer/refreshContentItem");
return Promise.reject(new Error(error));
}
await this.notebookManager?.notebookContentClient.updateItemChildrenInPlace(item);
}
public async openNotebookTerminal(kind: ViewModels.TerminalKind): Promise<void> {
if (userContext.features.enableCloudShell) {
this.connectToNotebookTerminal(kind);
} else if (useNotebook.getState().isPhoenixFeatures) {
await this.allocateContainer();
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
if (notebookServerInfo && notebookServerInfo.notebookServerEndpoint !== undefined) {
this.connectToNotebookTerminal(kind);
} else {
useDialog
.getState()
.showOkModalDialog(
"Failed to connect",
"Failed to connect to temporary workspace. This could happen because of network issues. Please refresh the page and try again.",
);
}
} else {
this.connectToNotebookTerminal(kind);
}
public openNotebookTerminal(kind: ViewModels.TerminalKind): void {
this.connectToNotebookTerminal(kind);
}
private connectToNotebookTerminal(kind: ViewModels.TerminalKind): void {
@@ -1000,6 +495,10 @@ export default class Explorer {
title = "Mongo Shell";
break;
case ViewModels.TerminalKind.CosmosDB:
title = "Cosmos DB Shell";
break;
default:
throw new Error("Terminal kind: ${kind} not supported");
}
@@ -1063,32 +562,6 @@ export default class Explorer {
}
}
public async handleOpenFileAction(path: string): Promise<void> {
if (useNotebook.getState().isPhoenixNotebooks === undefined) {
await useNotebook.getState().getPhoenixStatus();
}
if (useNotebook.getState().isPhoenixNotebooks) {
await this.allocateContainer();
}
// 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);
}
}
}
public openUploadItemsPane(onUpload?: (data: UploadDetailsRecord[]) => void): void {
useSidePanel.getState().openSidePanel("Upload " + getUploadName(), <UploadItemsPane onUpload={onUpload} />);
}
@@ -1098,24 +571,6 @@ export default class Explorer {
.openSidePanel("Input parameters", <ExecuteSprocParamsPane storedProcedure={storedProcedure} />);
}
public getDownloadModalContent(fileName: string): JSX.Element {
if (useNotebook.getState().isPhoenixNotebooks) {
return (
<>
<p>{Notebook.galleryNotebookDownloadContent1}</p>
<br />
<p>
{Notebook.galleryNotebookDownloadContent2}
<Link href={Notebook.cosmosNotebookGitDocumentationUrl} target="_blank">
{Notebook.learnMore}
</Link>
</p>
</>
);
}
return <p> Download {fileName} from gallery as a copy to your notebooks to run and/or edit the notebook. </p>;
}
public async refreshExplorer(): Promise<void> {
// Start DatabaseLoad scenario before fetching databases
if (userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo") {
@@ -1124,8 +579,7 @@ export default class Explorer {
// Run independent initialization tasks in parallel:
// - Database loading (ARM/SDK calls for databases + collections)
// - Notebook enabled check (Phoenix + Portal backend — no dependency on databases)
// - Feature registration check (ARM call — no dependency on databases or notebooks)
// - Feature registration check (ARM call — no dependency on databases)
const databasesTask =
userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo"
? (async () => {
@@ -1142,37 +596,12 @@ export default class Explorer {
})()
: Promise.resolve();
const notebooksTask = !isFabricNative()
? useNotebook.getState().refreshNotebooksEnabledStateForAccount()
: Promise.resolve();
const featureRegistrationTask =
userContext.authType === AuthType.AAD && userContext.apiType === "SQL" && !isFabricNative()
? featureRegistered(userContext.subscriptionId, "ThroughputBucketing")
: Promise.resolve(false);
const [, , throughputBucketsEnabled] = await Promise.all([databasesTask, notebooksTask, featureRegistrationTask]);
// Notebook initialization depends on refreshNotebooksEnabledStateForAccount completing above
// TODO: remove reference to isNotebookEnabled and isNotebooksEnabledForAccount
const isNotebookEnabled =
configContext.platform !== Platform.Fabric &&
(userContext.features.notebooksDownBanner ||
useNotebook.getState().isPhoenixNotebooks ||
useNotebook.getState().isPhoenixFeatures);
useNotebook.getState().setIsNotebookEnabled(isNotebookEnabled);
useNotebook
.getState()
.setIsShellEnabled(useNotebook.getState().isPhoenixFeatures && isPublicInternetAccessAllowed());
TelemetryProcessor.trace(Action.NotebookEnabled, ActionModifiers.Mark, {
isNotebookEnabled,
dataExplorerArea: Constants.Areas.Notebook,
});
if (useNotebook.getState().isPhoenixNotebooks) {
await this.initNotebooks(userContext.databaseAccount);
}
const [, throughputBucketsEnabled] = await Promise.all([databasesTask, featureRegistrationTask]);
if (throughputBucketsEnabled) {
updateUserContext({ throughputBucketsEnabled });
@@ -5,14 +5,12 @@
*/
import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react";
import { makeStyles, useFluent } from "@fluentui/react-components";
import { useNotebook } from "Explorer/Notebook/useNotebook";
import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane";
import { KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
import { isFabric } from "Platform/Fabric/FabricUtil";
import { userContext } from "UserContext";
import * as React from "react";
import create, { UseStore } from "zustand";
import { ConnectionStatusType } from "../../../Common/Constants";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../Explorer";
import { useSelectedNode } from "../../useSelectedNode";
@@ -28,6 +26,8 @@ export interface CommandBarStore {
setContextButtons: (contextButtons: CommandButtonComponentProps[]) => void;
isHidden: boolean;
setIsHidden: (isHidden: boolean) => void;
isSynapseLinkUpdating: boolean;
setIsSynapseLinkUpdating: (isSynapseLinkUpdating: boolean) => void;
}
export const useCommandBar: UseStore<CommandBarStore> = create((set) => ({
@@ -35,6 +35,8 @@ export const useCommandBar: UseStore<CommandBarStore> = create((set) => ({
setContextButtons: (contextButtons: CommandButtonComponentProps[]) => set((state) => ({ ...state, contextButtons })),
isHidden: false,
setIsHidden: (isHidden: boolean) => set((state) => ({ ...state, isHidden })),
isSynapseLinkUpdating: false,
setIsSynapseLinkUpdating: (isSynapseLinkUpdating: boolean) => set({ isSynapseLinkUpdating }),
}));
const useStyles = makeStyles({
@@ -69,26 +71,25 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
const selectedNodeState = useSelectedNode();
const buttons = useCommandBar((state) => state.contextButtons);
const isHidden = useCommandBar((state) => state.isHidden);
const isSynapseLinkUpdating = useCommandBar((state) => state.isSynapseLinkUpdating);
// targetDocument is used by referenced components
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { targetDocument } = useFluent();
const setKeyboardHandlers = useKeyboardActionGroup(KeyboardActionGroup.COMMAND_BAR);
const styles = useStyles();
const { connectionInfo, isPhoenixNotebooks, isPhoenixFeatures } = useNotebook((state) => ({
connectionInfo: state.connectionInfo,
isPhoenixNotebooks: state.isPhoenixNotebooks,
isPhoenixFeatures: state.isPhoenixFeatures,
}));
// Subscribe to the store changes that affect button creation
const dataPlaneRbacEnabled = useDataPlaneRbac((state) => state.dataPlaneRbacEnabled);
const aadTokenUpdated = useDataPlaneRbac((state) => state.aadTokenUpdated);
// Memoize the expensive button creation
const staticButtons = React.useMemo(() => {
return CommandBarComponentButtonFactory.createStaticCommandBarButtons(container, selectedNodeState);
}, [container, selectedNodeState, dataPlaneRbacEnabled, aadTokenUpdated]);
return CommandBarComponentButtonFactory.createStaticCommandBarButtons(
container,
selectedNodeState,
isSynapseLinkUpdating,
);
}, [container, selectedNodeState, dataPlaneRbacEnabled, aadTokenUpdated, isSynapseLinkUpdating]);
if (userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo") {
const buttons =
@@ -134,11 +135,6 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
const uiFabricControlButtons = CommandBarUtil.convertButton(controlButtons, "var(--colorNeutralBackground1)");
uiFabricControlButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true));
// Add connection status if needed (using the hook values we got at the top level)
if ((isPhoenixNotebooks || isPhoenixFeatures) && connectionInfo?.status !== ConnectionStatusType.Connect) {
uiFabricControlButtons.unshift(CommandBarUtil.createConnectionStatus(container, "connectionStatus"));
}
const rootStyle = {
root: {
backgroundColor: "var(--colorNeutralBackground1)",
@@ -4,7 +4,6 @@ import { DatabaseAccount } from "../../../Contracts/DataModels";
import { CollectionBase } from "../../../Contracts/ViewModels";
import { updateUserContext } from "../../../UserContext";
import Explorer from "../../Explorer";
import { useNotebook } from "../../Notebook/useNotebook";
import { useDatabases } from "../../useDatabases";
import { useSelectedNode } from "../../useSelectedNode";
import * as CommandBarComponentButtonFactory from "./CommandBarComponentButtonFactory";
@@ -37,6 +36,18 @@ describe("CommandBarComponentButtonFactory tests", () => {
expect(enableAzureSynapseLinkBtn).toBeDefined();
});
it("Button should be disabled while Synapse Link is updating", () => {
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(
mockExplorer,
selectedNodeState,
true,
);
const enableAzureSynapseLinkBtn = buttons.find(
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel,
);
expect(enableAzureSynapseLinkBtn.disabled).toBe(true);
});
// TODO: Now that Tables API supports dataplane RBAC, calling createStaticCommandBarButtons will enable the
// Entra ID Login button, which causes this test to fail due to "Invalid hook call.". This seems to be
// unsupported in jest and needs to be tested with react-hooks-testing-library.
@@ -74,8 +85,8 @@ describe("CommandBarComponentButtonFactory tests", () => {
});
});
describe("Open Cassandra shell button", () => {
const openCassandraShellBtnLabel = "Open Cassandra shell";
describe("Open Cassandra Shell button", () => {
const openCassandraShellBtnLabel = "Open Cassandra Shell";
const selectedNodeState = useSelectedNode.getState();
beforeAll(() => {
@@ -99,11 +110,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
});
});
afterEach(() => {
useNotebook.getState().setIsNotebookEnabled(false);
useNotebook.getState().setIsNotebooksEnabledForAccount(false);
});
it("Cassandra Api not available - button should be hidden", () => {
updateUserContext({
databaseAccount: {
@@ -127,7 +133,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
expect(openCassandraShellBtn).toBeUndefined();
});
it("Notebooks is not enabled and is unavailable - button should be shown and disabled", () => {
it("Cloud Shell is unavailable - button should be hidden", () => {
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
expect(openCassandraShellBtn).toBeUndefined();
@@ -135,14 +141,14 @@ describe("CommandBarComponentButtonFactory tests", () => {
});
describe("Open Postgres and vCore Mongo buttons", () => {
const openPostgresShellButtonLabel = "Open PSQL shell";
const openVCoreMongoShellButtonLabel = "Open MongoDB (DocumentDB) shell";
const openPostgresShellButtonLabel = "Open PSQL Shell";
const openVCoreMongoShellButtonLabel = "Open MongoDB (DocumentDB) Shell";
beforeAll(() => {
mockExplorer = {} as Explorer;
});
it("creates Postgres shell button", () => {
it("creates Postgres Shell button", () => {
const buttons = CommandBarComponentButtonFactory.createPostgreButtons(mockExplorer);
const openPostgresShellButton = buttons.find(
(button) => button.commandButtonLabel === openPostgresShellButtonLabel,
@@ -150,7 +156,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
expect(openPostgresShellButton).toBeDefined();
});
it("creates vCore Mongo shell button", () => {
it("creates vCore Mongo Shell button", () => {
const buttons = CommandBarComponentButtonFactory.createVCoreMongoButtons(mockExplorer);
const openVCoreMongoShellButton = buttons.find(
(button) => button.commandButtonLabel === openVCoreMongoShellButtonLabel,
@@ -17,14 +17,19 @@ import SynapseIcon from "../../../../images/synapse-link.svg";
import VSCodeIcon from "../../../../images/vscode.svg";
import { AuthType } from "../../../AuthType";
import * as Constants from "../../../Common/Constants";
import { Platform, configContext } from "../../../ConfigContext";
import { configContext, Platform } from "../../../ConfigContext";
import * as ViewModels from "../../../Contracts/ViewModels";
import { userContext } from "../../../UserContext";
import {
isVCoreMongoNativeAuthDisabled,
userContext,
VCoreMongoNativeAuthDisabledMessage,
VCoreMongoNativeAuthLearnMoreUrl,
} from "../../../UserContext";
import { isRunningOnNationalCloud } from "../../../Utils/CloudUtils";
import { useSidePanel } from "../../../hooks/useSidePanel";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import { useDialog } from "../../Controls/Dialog";
import Explorer from "../../Explorer";
import { useNotebook } from "../../Notebook/useNotebook";
import { BrowseQueriesPane } from "../../Panes/BrowseQueriesPane/BrowseQueriesPane";
import { LoadQueryPane } from "../../Panes/LoadQueryPane/LoadQueryPane";
import { SettingsPane, useDataPlaneRbac } from "../../Panes/SettingsPane/SettingsPane";
@@ -37,6 +42,7 @@ let counter = 0;
export function createStaticCommandBarButtons(
container: Explorer,
selectedNodeState: SelectedNodeState,
isSynapseLinkUpdating = false,
): CommandButtonComponentProps[] {
if (userContext.authType === AuthType.ResourceToken) {
return createStaticCommandBarButtonsForResourceToken(container, selectedNodeState);
@@ -56,7 +62,7 @@ export function createStaticCommandBarButtons(
userContext.apiType !== "Tables" &&
userContext.apiType !== "Cassandra"
) {
const addSynapseLink = createOpenSynapseLinkDialogButton(container);
const addSynapseLink = createOpenSynapseLinkDialogButton(container, isSynapseLinkUpdating);
if (addSynapseLink) {
addDivider();
buttons.push(addSynapseLink);
@@ -75,6 +81,15 @@ export function createStaticCommandBarButtons(
}
}
if (
userContext.apiType === "SQL" &&
userContext.features.enableCloudShell &&
userContext.features.enableCosmosDBShell
) {
addDivider();
buttons.push(createOpenTerminalButtonByKind(container, ViewModels.TerminalKind.CosmosDB));
}
if (!selectedNodeState.isDatabaseNodeOrNoneSelected()) {
const isQuerySupported = userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
@@ -121,14 +136,13 @@ export function createContextCommandBarButtons(
const buttons: CommandButtonComponentProps[] = [];
if (!selectedNodeState.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo") {
const label =
useNotebook.getState().isShellEnabled || userContext.features.enableCloudShell ? "Open Mongo Shell" : "New Shell";
const label = userContext.features.enableCloudShell ? "Open Mongo Shell" : "New Shell";
const newMongoShellBtn: CommandButtonComponentProps = {
iconSrc: HostedTerminalIcon,
iconAlt: label,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
if (useNotebook.getState().isShellEnabled || userContext.features.enableCloudShell) {
if (userContext.features.enableCloudShell) {
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
} else {
selectedCollection && selectedCollection.onNewMongoShellClick();
@@ -142,7 +156,7 @@ export function createContextCommandBarButtons(
}
if (
(useNotebook.getState().isShellEnabled || userContext.features.enableCloudShell) &&
userContext.features.enableCloudShell &&
!selectedNodeState.isDatabaseNodeOrNoneSelected() &&
userContext.apiType === "Cassandra"
) {
@@ -237,7 +251,10 @@ function areScriptsSupported(): boolean {
);
}
function createOpenSynapseLinkDialogButton(container: Explorer): CommandButtonComponentProps {
function createOpenSynapseLinkDialogButton(
container: Explorer,
isSynapseLinkUpdating: boolean,
): CommandButtonComponentProps {
if (configContext.platform === Platform.Emulator) {
return undefined;
}
@@ -258,7 +275,7 @@ function createOpenSynapseLinkDialogButton(container: Explorer): CommandButtonCo
onCommandClick: () => container.openEnableSynapseLinkDialog(),
commandButtonLabel: label,
hasPopup: false,
disabled: useNotebook.getState().isSynapseLinkUpdating,
disabled: isSynapseLinkUpdating,
ariaLabel: label,
};
}
@@ -492,20 +509,28 @@ function createOpenTerminalButtonByKind(
return "PSQL";
case ViewModels.TerminalKind.VCoreMongo:
return "MongoDB (DocumentDB)";
case ViewModels.TerminalKind.CosmosDB:
return "Cosmos DB";
default:
return "";
}
};
const label = `Open ${terminalFriendlyName()} shell`;
const tooltip =
"This feature is not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks.";
const disableButton =
!useNotebook.getState().isNotebooksEnabledForAccount && !useNotebook.getState().isNotebookEnabled;
const label = `Open ${terminalFriendlyName()} Shell`;
const tooltip = "Cloud Shell is not available for this account.";
const isNativeAuthDisabled = terminalKind === ViewModels.TerminalKind.VCoreMongo && isVCoreMongoNativeAuthDisabled();
const disableButton = !userContext.features.enableCloudShell || isNativeAuthDisabled;
return {
iconSrc: HostedTerminalIcon,
iconAlt: label,
onCommandClick: () => {
if (useNotebook.getState().isNotebookEnabled || userContext.features.enableCloudShell) {
if (isNativeAuthDisabled) {
useDialog.getState().showOkModalDialog("Native Authentication Disabled", VCoreMongoNativeAuthDisabledMessage, {
linkText: "Learn more",
linkUrl: VCoreMongoNativeAuthLearnMoreUrl,
});
return;
}
if (userContext.features.enableCloudShell) {
container.openNotebookTerminal(terminalKind);
}
},
@@ -513,7 +538,7 @@ function createOpenTerminalButtonByKind(
hasPopup: false,
disabled: disableButton,
ariaLabel: label,
tooltipText: !disableButton ? "" : tooltip,
tooltipText: isNativeAuthDisabled ? VCoreMongoNativeAuthDisabledMessage : !disableButton ? "" : tooltip,
};
}
@@ -17,9 +17,6 @@ import { configContext, Platform } from "../../../ConfigContext";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../Explorer";
import { ConnectionStatus } from "./ConnectionStatusComponent";
import { MemoryTracker } from "./MemoryTrackerComponent";
/**
* Convert our NavbarButtonConfig to UI Fabric buttons
@@ -258,20 +255,6 @@ export const createDivider = (key: string): ICommandBarItemProps => {
};
};
export const createMemoryTracker = (key: string): ICommandBarItemProps => {
return {
key,
onRender: () => <MemoryTracker />,
};
};
export const createConnectionStatus = (container: Explorer, key: string): ICommandBarItemProps => {
return {
key,
onRender: () => <ConnectionStatus container={container} />,
};
};
export function createKeyboardHandlers(allButtons: CommandButtonComponentProps[]): KeyboardHandlerMap {
const handlers: KeyboardHandlerMap = {};
@@ -1,184 +0,0 @@
@import "../../../../less/Common/Constants";
.connectionStatusContainer {
cursor: default;
align-items: center;
border: 1px;
min-height: 44px;
> span {
padding-right: 12px;
font-size: 12px;
font-family: @DataExplorerFont;
color: @DefaultFontColor;
}
&:focus{
outline: 0px;
}
}
.commandReactBtn {
&:hover {
background-color: rgb(238, 247, 255);
color: rgb(32, 31, 30);
cursor: pointer;
}
&:focus{
outline: 1px dashed #605e5c;
}
}
.connectedReactBtn {
&:hover {
background-color: rgb(238, 247, 255);
color: rgb(32, 31, 30);
cursor: pointer;
}
&:focus{
outline: 0px;
}
}
.connectIcon{
margin: 0px 4px;
height: 18px;
width: 18px;
color: rgb(0, 120, 212);
}
.status {
position: relative;
display: block;
margin-right: 8px;
width: 1em;
height: 1em;
font-size: 9px!important;
padding: 0px!important;
border-radius: 0.5em;
}
.status::before,
.status::after {
position: absolute;
content: "";
}
.status::before {
top: 0;
left: 0;
width: 1em;
height: 1em;
background-color: rgba(#fff, 0.1);
border-radius: 100%;
opacity: 1;
transform: translate3d(0, 0, 0) scale(0);
}
.connected{
background-color: green;
box-shadow:
0 0 0 0em rgba(green, 0),
0em 0.05em 0.1em rgba(#000000, 0.2);
transform: translate3d(0, 0, 0) scale(1);
}
.connecting{
background-color:#ffbf00;
box-shadow:
0 0 0 0em rgba(#ffbf00, 0),
0em 0.05em 0.1em rgba(#000000, 0.2);
transform: translate3d(0, 0, 0) scale(1);
}
.failed{
background-color:#bd1919;
box-shadow:
0 0 0 0em rgba(#bd1919, 0),
0em 0.05em 0.1em rgba(#000000, 0.2);
transform: translate3d(0, 0, 0) scale(1);
}
.status.connecting.is-animating {
animation: status-outer-connecting 3000ms infinite;
}
.status.failed.is-animating {
animation: status-outer-failed 3000ms infinite;
}
.status.connected.is-animating {
animation: status-outer-connected 3000ms infinite;
}
@keyframes status-outer-connected {
0% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0em #008000, 0em 0.05em 0.1em rgba(0, 0, 0, 0.2);
}
20% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0em rgba(0, 128, 0, 0.6), 0em 0.05em 0.1em rgba(0, 0, 0, 0.5);
}
40% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0em rgba(0, 128, 0, 0.5), 0em 0.05em 0.1em rgba(0, 0, 0, 0.4);
}
60% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0em rgba(0, 128, 0, 0.3), 0em 0.05em 0.1em rgba(0, 0, 0, 0.3);
}
80% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0.5em rgba(0, 128, 0, 0.1), 0em 0.05em 0.1em rgba(0, 0, 0, 0.1);
}
85% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0em rgba(0, 128, 0, 0), 0em 0.05em 0.1em rgba(0, 0, 0, 0);
}
}
@keyframes status-outer-failed {
0% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0em #bd1919, 0em 0.05em 0.1em rgba(0, 0, 0, 0.2);
}
20% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0em #c52d2d, 0em 0.05em 0.1em rgba(0, 0, 0, 0.5);
}
40% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0em #b47b7b, 0em 0.05em 0.1em rgba(0, 0, 0, 0.4);
}
60% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0em rgba(0, 128, 0, 0.3), 0em 0.05em 0.1em rgba(0, 0, 0, 0.3);
}
80% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0.5em rgba(0, 128, 0, 0.1), 0em 0.05em 0.1em rgba(0, 0, 0, 0.1);
}
85% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0em rgba(0, 128, 0, 0), 0em 0.05em 0.1em rgba(0, 0, 0, 0);
}
}
@keyframes status-outer-connecting {
0% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0em #ffbf00, 0em 0.05em 0.1em rgba(0, 0, 0, 0.2);
}
20% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0em #f0dfad, 0em 0.05em 0.1em rgba(0, 0, 0, 0.5);
}
40% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0em rgba(198, 243, 198, 0.5), 0em 0.05em 0.1em rgba(0, 0, 0, 0.4);
}
60% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0em rgba(213, 241, 213, 0.3), 0em 0.05em 0.1em rgba(0, 0, 0, 0.3);
}
80% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0.5em rgba(0, 128, 0, 0.1), 0em 0.05em 0.1em rgba(0, 0, 0, 0.1);
}
85% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0em rgba(0, 128, 0, 0), 0em 0.05em 0.1em rgba(0, 0, 0, 0);
}
}
@@ -1,186 +0,0 @@
import {
FocusTrapCallout,
FocusZone,
FocusZoneTabbableElements,
FontWeights,
Icon,
mergeStyleSets,
ProgressIndicator,
Stack,
Text,
TooltipHost,
} from "@fluentui/react";
import { useId } from "@fluentui/react-hooks";
import { ActionButton, DefaultButton } from "@fluentui/react/lib/Button";
import * as React from "react";
import "../../../../less/hostedexplorer.less";
import { ConnectionStatusType, ContainerStatusType, Notebook } from "../../../Common/Constants";
import Explorer from "../../Explorer";
import { useNotebook } from "../../Notebook/useNotebook";
import "../CommandBar/ConnectionStatusComponent.less";
interface Props {
container: Explorer;
}
export const ConnectionStatus: React.FC<Props> = ({ container }: Props): JSX.Element => {
const connectionInfo = useNotebook((state) => state.connectionInfo);
const [second, setSecond] = React.useState("00");
const [minute, setMinute] = React.useState("00");
const [isActive, setIsActive] = React.useState(false);
const [counter, setCounter] = React.useState(0);
const [statusColor, setStatusColor] = React.useState("");
const [toolTipContent, setToolTipContent] = React.useState("Connect to temporary workspace.");
const [isBarDismissed, setIsBarDismissed] = React.useState<boolean>(false);
const buttonId = useId("callout-button");
const containerInfo = useNotebook((state) => state.containerStatus);
const styles = mergeStyleSets({
callout: {
width: 320,
padding: "20px 24px",
},
title: {
marginBottom: 12,
fontWeight: FontWeights.semilight,
},
buttons: {
display: "flex",
justifyContent: "flex-end",
marginTop: 20,
},
});
React.useEffect(() => {
let intervalId: NodeJS.Timeout;
if (isActive) {
intervalId = setInterval(() => {
const secondCounter = counter % 60;
const minuteCounter = Math.floor(counter / 60);
const computedSecond: string = String(secondCounter).length === 1 ? `0${secondCounter}` : `${secondCounter}`;
const computedMinute: string = String(minuteCounter).length === 1 ? `0${minuteCounter}` : `${minuteCounter}`;
setSecond(computedSecond);
setMinute(computedMinute);
setCounter((counter) => counter + 1);
}, 1000);
}
return () => clearInterval(intervalId);
}, [isActive, counter]);
React.useEffect(() => {
if (connectionInfo?.status === ConnectionStatusType.Reconnect) {
setToolTipContent("Click here to Reconnect to temporary workspace.");
} else if (connectionInfo?.status === ConnectionStatusType.Failed) {
setStatusColor("status failed is-animating");
setToolTipContent("Click here to Reconnect to temporary workspace.");
}
}, [connectionInfo.status]);
const stopTimer = () => {
setIsActive(false);
setCounter(0);
setSecond("00");
setMinute("00");
};
const memoryUsageInfo = useNotebook((state) => state.memoryUsageInfo);
const totalGB = memoryUsageInfo ? memoryUsageInfo.totalKB / Notebook.memoryGuageToGB : 0;
const usedGB = totalGB > 0 ? totalGB - memoryUsageInfo.freeKB / Notebook.memoryGuageToGB : 0;
if (
connectionInfo &&
(connectionInfo.status === ConnectionStatusType.Connect || connectionInfo.status === ConnectionStatusType.Reconnect)
) {
return (
<ActionButton className="commandReactBtn" onClick={() => container.allocateContainer()}>
<TooltipHost content={toolTipContent}>
<Stack className="connectionStatusContainer" horizontal>
<Icon iconName="ConnectVirtualMachine" className="connectIcon" />
<span>{connectionInfo.status}</span>
</Stack>
</TooltipHost>
</ActionButton>
);
}
if (connectionInfo && connectionInfo.status === ConnectionStatusType.Connecting && isActive === false) {
stopTimer();
setIsActive(true);
setStatusColor("status connecting is-animating");
setToolTipContent("Connecting to temporary workspace.");
} else if (connectionInfo && connectionInfo.status === ConnectionStatusType.Connected && isActive === true) {
stopTimer();
setStatusColor("status connected is-animating");
setToolTipContent("Connected to temporary workspace.");
} else if (connectionInfo && connectionInfo.status === ConnectionStatusType.Failed && isActive === true) {
stopTimer();
setStatusColor("status failed is-animating");
setToolTipContent("Click here to Reconnect to temporary workspace.");
}
return (
<>
<TooltipHost
content={
containerInfo?.status === ContainerStatusType.Active
? `Connected to temporary workspace. This temporary workspace will get disconnected in ${Math.round(
containerInfo.durationLeftInMinutes,
)} minutes.`
: toolTipContent
}
>
<ActionButton
id={buttonId}
className={connectionInfo.status === ConnectionStatusType.Failed ? "commandReactBtn" : "connectedReactBtn"}
onClick={(e: React.MouseEvent<HTMLSpanElement>) =>
connectionInfo.status === ConnectionStatusType.Failed ? container.allocateContainer() : e.preventDefault()
}
>
<Stack className="connectionStatusContainer" horizontal>
<i className={statusColor}></i>
<span className={connectionInfo.status === ConnectionStatusType.Failed ? "connectionStatusFailed" : ""}>
{connectionInfo.status}
</span>
{connectionInfo.status === ConnectionStatusType.Connecting && isActive && (
<ProgressIndicator description={minute + ":" + second} />
)}
{connectionInfo.status === ConnectionStatusType.Connected && !isActive && (
<ProgressIndicator
className={totalGB !== 0 && usedGB / totalGB > 0.8 ? "lowMemory" : ""}
description={usedGB.toFixed(1) + " of " + totalGB.toFixed(1) + " GB"}
percentComplete={totalGB !== 0 ? usedGB / totalGB : 0}
/>
)}
</Stack>
{!isBarDismissed &&
containerInfo.status &&
containerInfo.status === ContainerStatusType.Active &&
Math.round(containerInfo.durationLeftInMinutes) <= Notebook.remainingTimeForAlert ? (
<FocusTrapCallout
role="alertdialog"
className={styles.callout}
gapSpace={0}
target={`#${buttonId}`}
onDismiss={() => setIsBarDismissed(true)}
setInitialFocus
>
<Text block variant="xLarge" className={styles.title}>
Remaining Time
</Text>
<Text block variant="small">
This temporary workspace will get disconnected in {Math.round(containerInfo.durationLeftInMinutes)}{" "}
minutes. To save your work permanently, save your notebooks to a GitHub repository or download the
notebooks to your local machine before the session ends.
</Text>
<FocusZone handleTabKey={FocusZoneTabbableElements.all} isCircularNavigation>
<Stack className={styles.buttons} gap={8} horizontal>
<DefaultButton onClick={() => setIsBarDismissed(true)}>Dimiss</DefaultButton>
</Stack>
</FocusZone>
</FocusTrapCallout>
) : undefined}
</ActionButton>
</TooltipHost>
</>
);
};
@@ -1,24 +0,0 @@
@import "../../../../less/Common/Constants";
.memoryTrackerContainer {
cursor: default;
align-items: center;
margin: 0 9px;
border: 1px;
> span {
padding-right: 12px;
font-size: 13px;
font-family: @DataExplorerFont;
color: @DefaultFontColor;
}
> .lowMemory {
.ms-ProgressIndicator-progressBar {
background-color: @SelectionHigh;
}
.ms-ProgressIndicator-itemDescription {
color: @SelectionHigh;
}
}
}
@@ -1,29 +0,0 @@
import { ProgressIndicator, Spinner, SpinnerSize, Stack } from "@fluentui/react";
import * as React from "react";
import { useNotebook } from "../../Notebook/useNotebook";
export const MemoryTracker: React.FC = (): JSX.Element => {
const memoryUsageInfo = useNotebook((state) => state.memoryUsageInfo);
if (!memoryUsageInfo) {
return (
<Stack className="memoryTrackerContainer" horizontal>
<span>Memory</span>
<Spinner size={SpinnerSize.medium} />
</Stack>
);
}
const totalGB = memoryUsageInfo.totalKB / 1048576;
const usedGB = totalGB - memoryUsageInfo.freeKB / 1048576;
return (
<Stack className="memoryTrackerContainer" horizontal>
<span>Memory</span>
<ProgressIndicator
className={usedGB / totalGB > 0.8 ? "lowMemory" : ""}
description={usedGB.toFixed(1) + " of " + totalGB.toFixed(1) + " GB"}
percentComplete={usedGB / totalGB}
/>
</Stack>
);
};
@@ -4,13 +4,6 @@ import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility";
export enum Type {
OpenCollection = "OpenCollection",
OpenNotebook = "OpenNotebook",
}
export interface OpenNotebookItem {
type: Type.OpenNotebook;
name: string;
path: string;
}
export interface OpenCollectionItem {
@@ -19,7 +12,7 @@ export interface OpenCollectionItem {
collectionId: string;
}
type Item = OpenNotebookItem | OpenCollectionItem;
type Item = OpenCollectionItem;
const itemsMaxNumber: number = 5;
@@ -42,14 +35,14 @@ const migrateOldData = () => {
componentName: AppStateComponentNames.MostRecentActivity,
globalAccountName: accountName,
},
itemsMap[accountId].map((item) => {
if ((item.type as unknown as number) === 0) {
item.type = Type.OpenCollection;
} else if ((item.type as unknown as number) === 1) {
item.type = Type.OpenNotebook;
}
return item;
}),
itemsMap[accountId]
.filter((item) => (item.type as unknown as number) !== 1 && (item.type as string) !== "OpenNotebook")
.map((item) => {
if ((item.type as unknown as number) === 0) {
item.type = Type.OpenCollection;
}
return item;
}),
);
}
});
@@ -97,7 +90,7 @@ export const getItems = (accountName: string): Item[] => {
componentName: AppStateComponentNames.MostRecentActivity,
globalAccountName: accountName,
}) as Item[]) || []
);
).filter((item) => item.type in Type);
};
export const collectionWasOpened = (
-33
View File
@@ -1,33 +0,0 @@
/**
* file list returns path starting with ./blah
* rename returns simply blah.
* Both are the same. This method only handles these two cases and no other complicated paths that may contain ..
* ./ inside the path.
* TODO: this should go away when not using jupyter for file operations and use normalized paths.
* @param path1
* @param path2
*/
export function isPathEqual(path1: string, path2: string): boolean {
const normalize = (path: string): string => {
const dotSlash = "./";
if (path.indexOf(dotSlash) === 0) {
path = path.substring(dotSlash.length);
}
return path;
};
return normalize(path1) === normalize(path2);
}
/**
* Remove extension
* @param path
* @param extension Without the ".". e.g. "ipynb" (and not ".ipynb")
*/
export function stripExtension(path: string, extension: string): string {
const splitted = path.split(".");
if (splitted[splitted.length - 1] === extension) {
splitted.pop();
}
return splitted.join(".");
}
-20
View File
@@ -1,20 +0,0 @@
import { NotebookContentRecordProps, selectors } from "@nteract/core";
/**
* A bunch of utilities to interact with nteract
*/
export function getCurrentCellType(content: NotebookContentRecordProps): "markdown" | "code" | "raw" | undefined {
if (!content) {
return undefined;
}
const cellFocusedId = selectors.notebook.cellFocused(content.model);
if (cellFocusedId) {
const cell = selectors.notebook.cellById(content.model, { id: cellFocusedId });
if (cell) {
return cell.cell_type;
}
}
return undefined;
}
-287
View File
@@ -1,287 +0,0 @@
// Manages all the redux logic for the notebook nteract code
// TODO: Merge with NotebookClient?
// Vendor modules
import {
actions,
AppState,
ContentRecord,
createHostRef,
createKernelspecsRef,
HostRecord,
HostRef,
IContentProvider,
KernelspecsRef,
makeAppRecord,
makeCommsRecord,
makeContentsRecord,
makeEditorsRecord,
makeEntitiesRecord,
makeHostsRecord,
makeJupyterHostRecord,
makeStateRecord,
makeTransformsRecord,
} from "@nteract/core";
import { configOption, defineConfigOption } from "@nteract/mythic-configuration";
import { Media } from "@nteract/outputs";
import TransformVDOM from "@nteract/transform-vdom";
import * as Immutable from "immutable";
import { Notification } from "react-notification-system";
import { AnyAction, Dispatch, Middleware, MiddlewareAPI, Store } from "redux";
import * as Constants from "../../Common/Constants";
import { NotebookWorkspaceConnectionInfo } from "../../Contracts/DataModels";
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext";
import configureStore from "./NotebookComponent/store";
import { CdbAppState, makeCdbRecord } from "./NotebookComponent/types";
export type KernelSpecsDisplay = { name: string; displayName: string };
export interface NotebookClientV2Parameters {
connectionInfo: NotebookWorkspaceConnectionInfo;
databaseAccountName: string;
defaultExperience: string;
isReadOnly?: boolean; // if true: do not fetch kernelspecs automatically (this is for notebook viewer)
cellEditorType?: string; // override "codemirror" default,
autoSaveInterval?: number; // in ms
contentProvider: IContentProvider;
}
export type ActionListener = (newValue: any) => void;
export class NotebookClientV2 {
private store: Store<AppState, AnyAction>;
private contentHostRef: HostRef;
private kernelSpecsForDisplay: KernelSpecsDisplay[] = [];
private kernelSpecsRef: KernelspecsRef;
private databaseAccountName: string;
private defaultExperience: string;
constructor(params: NotebookClientV2Parameters) {
this.databaseAccountName = params.databaseAccountName;
this.defaultExperience = params.defaultExperience;
this.configureStore(params);
this.kernelSpecsRef = createKernelspecsRef();
// Fetch kernel specs when opening new tab
if (!params.isReadOnly) {
this.getStore().dispatch(
actions.fetchKernelspecs({
hostRef: this.contentHostRef,
kernelspecsRef: this.kernelSpecsRef,
}),
);
}
}
public getAvailableKernelSpecs(): KernelSpecsDisplay[] {
return this.kernelSpecsForDisplay;
}
public getStore(): Store<AppState, AnyAction> {
return this.store;
}
/**
* Lazy init redux store as singleton.
* Don't move store in Explorer yet as it is typed to AppState which is nteract-specific
*/
private configureStore(params: NotebookClientV2Parameters): void {
const jupyterHostRecord = makeJupyterHostRecord({
id: null,
type: "jupyter",
defaultKernelName: "python",
token: params.connectionInfo.authToken,
origin: params.connectionInfo.notebookServerEndpoint,
basePath: "/", // Jupyter server base URL
bookstoreEnabled: false, //!!config.bookstore.version,
showHeaderEditor: true,
crossDomain: true,
});
this.contentHostRef = createHostRef();
const NullTransform = (): any => null;
const kernelspecsRef = createKernelspecsRef();
const initialState: CdbAppState = {
app: makeAppRecord({
version: "dataExplorer 1.0",
host: jupyterHostRecord,
// TODO: tamitta: notificationSystem.addNotification was removed, do we need a substitute?
}),
core: makeStateRecord({
currentKernelspecsRef: kernelspecsRef,
entities: makeEntitiesRecord({
editors: makeEditorsRecord({}),
hosts: makeHostsRecord({
byRef: Immutable.Map<string, HostRecord>().set(this.contentHostRef, jupyterHostRecord),
}),
comms: makeCommsRecord(),
contents: makeContentsRecord({
byRef: Immutable.Map<string, ContentRecord>(),
}),
transforms: userContext.features.sandboxNotebookOutputs
? undefined
: makeTransformsRecord({
displayOrder: Immutable.List([
"application/vnd.jupyter.widget-view+json",
"application/vnd.vega.v5+json",
"application/vnd.vega.v4+json",
"application/vnd.vega.v3+json",
"application/vnd.vega.v2+json",
"application/vnd.vegalite.v3+json",
"application/vnd.vegalite.v2+json",
"application/vnd.vegalite.v1+json",
"application/geo+json",
"application/vnd.plotly.v1+json",
"text/vnd.plotly.v1+html",
"application/x-nteract-model-debug+json",
"application/vnd.dataresource+json",
"application/vdom.v1+json",
"application/json",
"application/javascript",
"text/html",
"text/markdown",
"text/latex",
"image/svg+xml",
"image/gif",
"image/png",
"image/jpeg",
"text/plain",
]),
byId: Immutable.Map({
"text/vnd.plotly.v1+html": NullTransform,
"application/vnd.plotly.v1+json": NullTransform,
"application/geo+json": NullTransform,
"application/x-nteract-model-debug+json": NullTransform,
"application/vnd.dataresource+json": NullTransform,
"application/vnd.jupyter.widget-view+json": NullTransform,
"application/vnd.vegalite.v1+json": NullTransform,
"application/vnd.vegalite.v2+json": NullTransform,
"application/vnd.vegalite.v3+json": NullTransform,
"application/vnd.vega.v2+json": NullTransform,
"application/vnd.vega.v3+json": NullTransform,
"application/vnd.vega.v4+json": NullTransform,
"application/vnd.vega.v5+json": NullTransform,
"application/vdom.v1+json": TransformVDOM,
"application/json": Media.Json,
"application/javascript": Media.JavaScript,
"text/html": Media.HTML,
"text/markdown": Media.Markdown,
"text/latex": Media.LaTeX,
"image/svg+xml": Media.SVG,
"image/gif": Media.Image,
"image/png": Media.Image,
"image/jpeg": Media.Image,
"text/plain": Media.Plain,
}),
}),
}),
}),
cdb: makeCdbRecord({
databaseAccountName: params.databaseAccountName,
defaultExperience: params.defaultExperience,
}),
};
/**
* Intercept kernelspecs updates actions rather than subscribing to the store state changes (which
* is triggered for *any* state change).
* TODO: Use react-redux connect() to subscribe to state changes?
*/
const cacheKernelSpecsMiddleware: Middleware =
<D extends Dispatch<AnyAction>, S extends AppState>({ dispatch, getState }: MiddlewareAPI<D, S>) =>
(next: Dispatch<AnyAction>) =>
<A extends AnyAction>(action: A): A => {
switch (action.type) {
case actions.FETCH_KERNELSPECS_FULFILLED: {
const payload = (action as unknown as actions.FetchKernelspecsFulfilled).payload;
const defaultKernelName = payload.defaultKernelName;
this.kernelSpecsForDisplay = Object.values(payload.kernelspecs)
.filter((spec) => !spec.metadata?.hasOwnProperty("hidden"))
.map((spec) => ({
name: spec.name,
displayName: spec.displayName,
}))
.sort((a: KernelSpecsDisplay, b: KernelSpecsDisplay) => {
// Put default at the top, otherwise lexicographically compare
if (a.displayName === defaultKernelName) {
return -1;
} else if (b.name === defaultKernelName) {
return 1;
} else {
return a.displayName.localeCompare(b.displayName);
}
});
break;
}
}
return next(action);
};
const traceErrorFct = (title: string, message: string) => {
TelemetryProcessor.traceFailure(Action.NotebookErrorNotification, {
dataExplorerArea: Constants.Areas.Notebook,
title,
message,
level: "Error",
});
console.error(`${title}: ${message}`);
};
this.store = configureStore(
initialState,
params.contentProvider,
traceErrorFct,
[cacheKernelSpecsMiddleware],
!params.isReadOnly,
);
// Additional configuration
this.store.dispatch(configOption("editorType").action(params.cellEditorType ?? "codemirror"));
this.store.dispatch(
configOption("autoSaveInterval").action(params.autoSaveInterval ?? Constants.Notebook.autoSaveIntervalMs),
);
this.store.dispatch(configOption("codeMirror.lineNumbers").action(true));
const readOnlyConfigOption = configOption("codeMirror.readOnly");
const readOnlyValue = params.isReadOnly ? "nocursor" : undefined;
if (!readOnlyConfigOption) {
defineConfigOption({
label: "Read-only",
key: "codeMirror.readOnly",
values: [
{ label: "Read-Only", value: "nocursor" },
{ label: "Not read-only", value: undefined },
],
defaultValue: readOnlyValue,
});
} else {
this.store.dispatch(readOnlyConfigOption.action(readOnlyValue));
}
}
/**
* Handle notification coming from nteract
* The messages coming from nteract are not good enough to expose to user.
* We use the notificationsToUserEpic to control the messages from action.
* We log possible errors coming from nteract in telemetry and display in console
*/
private handleNotification = (msg: Notification): void => {
if (msg.level === "error") {
TelemetryProcessor.traceFailure(Action.NotebookErrorNotification, {
dataExplorerArea: Constants.Areas.Notebook,
title: msg.title,
message: msg.message,
level: msg.level,
});
console.error(`${msg.title}: ${msg.message}`);
} else {
console.log(`${msg.title}: ${msg.message}`);
}
};
}
@@ -1,111 +0,0 @@
import { FileType, IContent, IContentProvider, ServerConfig } from "@nteract/core";
import { Observable, of } from "rxjs";
import { AjaxResponse } from "rxjs/ajax";
import { HttpStatusCodes } from "../../../../Common/Constants";
import { getErrorMessage } from "../../../../Common/ErrorHandlingUtils";
import * as Logger from "../../../../Common/Logger";
export interface InMemoryContentProviderParams {
[path: string]: { readonly: boolean; content: IContent<FileType> };
}
// Nteract relies on `errno` property to figure out the kind of failure
// That's why we need a custom wrapper around Error to include `errno` property
class InMemoryContentProviderError extends Error {
constructor(
error: string,
public errno: number = InMemoryContentProvider.SelfErrorCode,
) {
super(error);
}
}
export class InMemoryContentProvider implements IContentProvider {
public static readonly SelfErrorCode = 666;
constructor(private params: InMemoryContentProviderParams) {}
public remove(): Observable<AjaxResponse> {
return this.errorResponse("Not implemented", "remove");
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public get(_config: ServerConfig, uri: string): Observable<AjaxResponse> {
const item = this.params[uri];
if (item) {
return of(this.createSuccessAjaxResponse(HttpStatusCodes.OK, item.content));
}
return this.errorResponse(`${uri} not found`, "get");
}
public update(): Observable<AjaxResponse> {
return this.errorResponse("Not implemented", "update");
}
public create(): Observable<AjaxResponse> {
return this.errorResponse("Not implemented", "create");
}
public save<FT extends FileType>(
_config: ServerConfig, // eslint-disable-line @typescript-eslint/no-unused-vars
uri: string,
model: Partial<IContent<FT>>,
): Observable<AjaxResponse> {
const item = this.params[uri];
if (item) {
if (!item.readonly) {
Object.assign(item.content, model);
}
return of(this.createSuccessAjaxResponse(HttpStatusCodes.OK, item.content));
}
return this.errorResponse(`${uri} not found`, "save");
}
public listCheckpoints(): Observable<AjaxResponse> {
return this.errorResponse("Not implemented", "listCheckpoints");
}
public createCheckpoint(): Observable<AjaxResponse> {
return this.errorResponse("Not implemented", "createCheckpoint");
}
public deleteCheckpoint(): Observable<AjaxResponse> {
return this.errorResponse("Not implemented", "deleteCheckpoint");
}
public restoreFromCheckpoint(): Observable<AjaxResponse> {
return this.errorResponse("Not implemented", "restoreFromCheckpoint");
}
private errorResponse(message: string, functionName: string): Observable<AjaxResponse> {
const error = new InMemoryContentProviderError(message);
Logger.logError(error.message, `InMemoryContentProvider/${functionName}`, error.errno);
return of(this.createErrorAjaxResponse(error));
}
private createSuccessAjaxResponse(status: number, content: IContent<FileType>): AjaxResponse {
return {
originalEvent: new Event("no-op"),
xhr: new XMLHttpRequest(),
request: {},
status,
response: content ? content : undefined,
responseText: content ? JSON.stringify(content) : undefined,
responseType: "json",
};
}
private createErrorAjaxResponse(error: InMemoryContentProviderError): AjaxResponse {
return {
originalEvent: new Event("no-op"),
xhr: new XMLHttpRequest(),
request: {},
status: error.errno,
response: error,
responseText: getErrorMessage(error),
responseType: "json",
};
}
}
@@ -1,15 +0,0 @@
// memory://<path>
// Custom scheme for in memory content
export const ContentUriPattern = /memory:\/\/([^/]*)/;
export function fromContentUri(contentUri: string): undefined | string {
const matches = contentUri.match(ContentUriPattern);
if (matches && matches.length > 1) {
return matches[1];
}
return undefined;
}
export function toContentUri(path: string): string {
return `memory://${path}`;
}
@@ -1,23 +0,0 @@
import * as InMemoryContentProviderUtils from "./ContentProviders/InMemoryContentProviderUtils";
describe("fromContentUri", () => {
it("fromContentUri should return valid result", () => {
const contentUri = "memory://resource/path";
const result = "resource";
expect(InMemoryContentProviderUtils.fromContentUri(contentUri)).toEqual(result);
});
it("fromContentUri should return undefined on invalid input", () => {
const contentUri = "invalid";
expect(InMemoryContentProviderUtils.fromContentUri(contentUri)).toEqual(undefined);
});
it("toContentUri should return valid result", () => {
const path = "resource/path";
const result = "memory://resource/path";
expect(InMemoryContentProviderUtils.toContentUri(path)).toEqual(result);
});
});
@@ -1,13 +0,0 @@
.notebookComponentContainer {
text-transform: none;
line-height: 1.28581;
letter-spacing: 0;
font-size: 14px;
font-weight: 400;
color: #182026;
height: 100%;
.hotKeys {
height: 100%;
}
}
@@ -1,25 +0,0 @@
import { ContentRef } from "@nteract/core";
import * as React from "react";
import NotificationSystem, { System as ReactNotificationSystem } from "react-notification-system";
import { default as Contents } from "./contents";
export class NotebookComponent extends React.Component<{ contentRef: ContentRef }> {
notificationSystem!: ReactNotificationSystem;
shouldComponentUpdate(nextProps: { contentRef: ContentRef }): boolean {
return nextProps.contentRef !== this.props.contentRef;
}
public render(): JSX.Element {
return (
<div className="notebookComponentContainer">
<Contents contentRef={this.props.contentRef} />
<NotificationSystem
ref={(notificationSystem: ReactNotificationSystem) => {
this.notificationSystem = notificationSystem;
}}
/>
</div>
);
}
}
@@ -1,50 +0,0 @@
// Vendor modules
import { actions, createContentRef, createKernelRef, selectors } from "@nteract/core";
import * as React from "react";
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
import { NotebookClientV2 } from "../NotebookClientV2";
import { NotebookContentItem } from "../NotebookContentItem";
import { NotebookComponentBootstrapper } from "./NotebookComponentBootstrapper";
import VirtualCommandBarComponent from "./VirtualCommandBarComponent";
export interface NotebookComponentAdapterOptions {
contentItem: NotebookContentItem;
notebooksBasePath: string;
notebookClient: NotebookClientV2;
onUpdateKernelInfo: () => void;
}
export class NotebookComponentAdapter extends NotebookComponentBootstrapper implements ReactAdapter {
private onUpdateKernelInfo: () => void;
public parameters: any;
constructor(options: NotebookComponentAdapterOptions) {
super({
contentRef: selectors.contentRefByFilepath(options.notebookClient.getStore().getState(), {
filepath: options.contentItem.path,
}),
notebookClient: options.notebookClient,
});
this.onUpdateKernelInfo = options.onUpdateKernelInfo;
if (!this.contentRef) {
this.contentRef = createContentRef();
const kernelRef = createKernelRef();
// Request fetching notebook content
this.getStore().dispatch(
actions.fetchContent({
filepath: options.contentItem.path,
params: {},
kernelRef,
contentRef: this.contentRef,
}),
);
}
}
protected renderExtraComponent = (): JSX.Element => {
return <VirtualCommandBarComponent contentRef={this.contentRef} onRender={this.onUpdateKernelInfo} />;
};
}
@@ -1,379 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Link } from "@fluentui/react";
import { CellId, CellType, ImmutableNotebook } from "@nteract/commutable";
// Vendor modules
import { actions, AppState, ContentRef, KernelRef, NotebookContentRecord, selectors } from "@nteract/core";
import "@nteract/styles/editor-overrides.css";
import "@nteract/styles/global-variables.css";
import "codemirror/addon/hint/show-hint.css";
import "codemirror/lib/codemirror.css";
import { Notebook } from "Common/Constants";
import { useDialog } from "Explorer/Controls/Dialog";
import * as React from "react";
import { Provider } from "react-redux";
import "react-table/react-table.css";
import { AnyAction, Store } from "redux";
import { NotebookClientV2 } from "../NotebookClientV2";
import { NotebookContentProviderType, NotebookUtil } from "../NotebookUtil";
import * as NteractUtil from "../NTeractUtil";
import * as CdbActions from "./actions";
import { NotebookComponent } from "./NotebookComponent";
import "./NotebookComponent.less";
export interface NotebookComponentBootstrapperOptions {
notebookClient: NotebookClientV2;
contentRef: ContentRef;
}
interface IWrapModel {
name: string;
path: string;
last_modified: Date;
created: string;
content: unknown;
format: string;
mimetype: unknown;
size: number;
writeable: boolean;
type: string;
}
export class NotebookComponentBootstrapper {
public contentRef: ContentRef;
protected renderExtraComponent: () => JSX.Element;
private notebookClient: NotebookClientV2;
constructor(options: NotebookComponentBootstrapperOptions) {
this.notebookClient = options.notebookClient;
this.contentRef = options.contentRef;
}
protected static wrapModelIntoContent(name: string, path: string, content: unknown): IWrapModel {
return {
name,
path,
last_modified: new Date(),
created: "",
content,
format: "json",
mimetype: undefined,
size: 0,
writeable: false,
type: "notebook",
};
}
private renderDefaultNotebookComponent(props: any): JSX.Element {
return (
<>
{this.renderExtraComponent && this.renderExtraComponent()}
{React.createElement<{ contentRef: ContentRef }>(NotebookComponent, { contentRef: this.contentRef, ...props })}
</>
);
}
public getContent(): { name: string; content: string | ImmutableNotebook } {
const record = this.getStore().getState().core.entities.contents.byRef.get(this.contentRef);
let content: string | ImmutableNotebook;
switch (record.model.type) {
case "notebook":
content = 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 getNotebookPath(): string {
return this.getStore().getState().core.entities.contents.byRef.get(this.contentRef)?.filepath;
}
public setContent(name: string, content: unknown): void {
this.getStore().dispatch(
actions.fetchContentFulfilled({
filepath: undefined,
model: NotebookComponentBootstrapper.wrapModelIntoContent(name, undefined, content),
kernelRef: undefined,
contentRef: this.contentRef,
}),
);
}
/**
* We can overload the notebook renderer here
* @param renderer
* @props additional props
*/
public renderComponent(
renderer?: any, // TODO FIX THIS React.ComponentClass<{ contentRef: ContentRef; isReadOnly?: boolean }>,
props?: any,
): JSX.Element {
return (
<Provider store={this.getStore()}>
{renderer
? React.createElement<{ contentRef: ContentRef }>(renderer, { contentRef: this.contentRef, ...props })
: this.renderDefaultNotebookComponent(props)}
</Provider>
);
}
/* Notebook operations. See nteract/packages/connected-components/src/notebook-menu/index.tsx */
public notebookSave(): void {
if (
NotebookUtil.getContentProviderType(this.getNotebookPath()) ===
NotebookContentProviderType.JupyterContentProviderType
) {
useDialog.getState().showOkCancelModalDialog(
Notebook.saveNotebookModalTitle,
undefined,
"Save",
async () => {
this.getStore().dispatch(
actions.save({
contentRef: this.contentRef,
}),
);
},
"Cancel",
undefined,
this.getSaveNotebookSubText(),
);
} else {
this.getStore().dispatch(
actions.save({
contentRef: this.contentRef,
}),
);
}
}
public notebookChangeKernel(kernelSpecName: string): void {
this.getStore().dispatch(
actions.changeKernelByName({
contentRef: this.contentRef,
kernelSpecName,
oldKernelRef: this.getCurrentKernelRef(),
}),
);
}
public notebookRunAndAdvance(): void {
this.getStore().dispatch(
CdbActions.executeFocusedCellAndFocusNext({
contentRef: this.contentRef,
}),
);
}
public notebookRunAll(): void {
this.getStore().dispatch(
actions.executeAllCells({
contentRef: this.contentRef,
}),
);
}
public notebookInterruptKernel(): void {
this.getStore().dispatch(
actions.interruptKernel({
kernelRef: this.getCurrentKernelRef(),
}),
);
}
public notebookKillKernel(): void {
this.getStore().dispatch(
actions.killKernel({
restarting: false,
kernelRef: this.getCurrentKernelRef(),
}),
);
}
public notebookRestartKernel(): void {
this.getStore().dispatch(
actions.restartKernel({
kernelRef: this.getCurrentKernelRef(),
contentRef: this.contentRef,
outputHandling: "None",
}),
);
}
public notebookClearAllOutputs(): void {
this.getStore().dispatch(
actions.clearAllOutputs({
contentRef: this.contentRef,
}),
);
}
public notebookInsertBelow(): void {
this.getStore().dispatch(
actions.createCellBelow({
cellType: "code",
contentRef: this.contentRef,
}),
);
}
public notebookChangeCellType(type: CellType): void {
const focusedCellId = this.getFocusedCellId();
if (!focusedCellId) {
console.error("No focused cell");
return;
}
this.getStore().dispatch(
actions.changeCellType({
id: focusedCellId,
contentRef: this.contentRef,
to: type,
}),
);
}
public notebokCopy(): void {
const focusedCellId = this.getFocusedCellId();
if (!focusedCellId) {
console.error("No focused cell");
return;
}
this.getStore().dispatch(
actions.copyCell({
id: focusedCellId,
contentRef: this.contentRef,
}),
);
}
public notebookCut(): void {
const focusedCellId = this.getFocusedCellId();
if (!focusedCellId) {
console.error("No focused cell");
return;
}
this.getStore().dispatch(
actions.cutCell({
id: focusedCellId,
contentRef: this.contentRef,
}),
);
}
public notebookPaste(): void {
this.getStore().dispatch(
actions.pasteCell({
contentRef: this.contentRef,
}),
);
}
public notebookShutdown(): void {
const store = this.getStore();
const kernelRef = this.getCurrentKernelRef();
if (kernelRef) {
store.dispatch(
actions.killKernel({
restarting: false,
kernelRef,
}),
);
}
store.dispatch(
CdbActions.closeNotebook({
contentRef: this.contentRef,
}),
);
}
public isContentDirty(): boolean {
const content = selectors.content(this.getStore().getState(), { contentRef: this.contentRef });
if (!content) {
return false;
}
// TODO Fix this typing here
return selectors.notebook.isDirty(content.model as never);
}
public isNotebookUntrusted(): boolean {
return NotebookUtil.isNotebookUntrusted(this.getStore().getState(), this.contentRef);
}
/**
* For display purposes, only return non-killed kernels
*/
public getCurrentKernelName(): string {
const currentKernel = selectors.kernel(this.getStore().getState(), { kernelRef: this.getCurrentKernelRef() });
return (currentKernel && currentKernel.status !== "killed" && currentKernel.kernelSpecName) || undefined;
}
// Returns the kernel name to select in the kernels dropdown
public getSelectedKernelName(): string {
const currentKernelName = this.getCurrentKernelName();
if (!currentKernelName) {
// if there's no live kernel, try to get the kernel name from the notebook metadata
const content = selectors.content(this.getStore().getState(), { contentRef: this.contentRef });
const notebook = content && (content as NotebookContentRecord).model.notebook;
if (!notebook) {
return undefined;
}
const { kernelSpecName } = NotebookUtil.extractNewKernel("", notebook);
return kernelSpecName || undefined;
}
return currentKernelName;
}
public getActiveCellTypeStr(): string {
const content = selectors.content(this.getStore().getState(), { contentRef: this.contentRef });
return NteractUtil.getCurrentCellType(content as NotebookContentRecord);
}
private getCurrentKernelRef(): KernelRef {
return selectors.kernelRefByContentRef(this.getStore().getState(), { contentRef: this.contentRef });
}
private getFocusedCellId(): CellId {
const content = selectors.content(this.getStore().getState(), { contentRef: this.contentRef });
if (!content) {
return undefined;
}
return selectors.notebook.cellFocused((content as NotebookContentRecord).model);
}
protected getStore(): Store<AppState, AnyAction> {
return this.notebookClient.getStore();
}
private getSaveNotebookSubText(): JSX.Element {
return (
<>
<p>{Notebook.saveNotebookModalContent}</p>
<br />
<p>
{Notebook.newNotebookModalContent2}
<Link href={Notebook.cosmosNotebookHomePageUrl} target="_blank">
{Notebook.learnMore}
</Link>
</p>
</>
);
}
}
@@ -1,79 +0,0 @@
import { FileType, IContent, IContentProvider, IGetParams, ServerConfig } from "@nteract/core";
import { Observable } from "rxjs";
import { AjaxResponse } from "rxjs/ajax";
import { GitHubContentProvider } from "../../../GitHub/GitHubContentProvider";
import * as GitHubUtils from "../../../Utils/GitHubUtils";
import { InMemoryContentProvider } from "./ContentProviders/InMemoryContentProvider";
import * as InMemoryContentProviderUtils from "./ContentProviders/InMemoryContentProviderUtils";
export class NotebookContentProvider implements IContentProvider {
constructor(
private inMemoryContentProvider: InMemoryContentProvider,
private gitHubContentProvider: GitHubContentProvider,
private jupyterContentProvider: IContentProvider,
) {}
public remove(serverConfig: ServerConfig, path: string): Observable<AjaxResponse> {
return this.getContentProvider(path).remove(serverConfig, path);
}
public get(serverConfig: ServerConfig, path: string, params: Partial<IGetParams>): Observable<AjaxResponse> {
return this.getContentProvider(path).get(serverConfig, path, params);
}
public update<FT extends FileType>(
serverConfig: ServerConfig,
path: string,
model: Partial<IContent<FT>>,
): Observable<AjaxResponse> {
return this.getContentProvider(path).update(serverConfig, path, model);
}
public create<FT extends FileType>(
serverConfig: ServerConfig,
path: string,
model: Partial<IContent<FT>> & { type: FT },
): Observable<AjaxResponse> {
return this.getContentProvider(path).create(serverConfig, path, model);
}
public save<FT extends FileType>(
serverConfig: ServerConfig,
path: string,
model: Partial<IContent<FT>>,
): Observable<AjaxResponse> {
return this.getContentProvider(path).save(serverConfig, path, model);
}
public listCheckpoints(serverConfig: ServerConfig, path: string): Observable<AjaxResponse> {
return this.getContentProvider(path).listCheckpoints(serverConfig, path);
}
public createCheckpoint(serverConfig: ServerConfig, path: string): Observable<AjaxResponse> {
return this.getContentProvider(path).createCheckpoint(serverConfig, path);
}
public deleteCheckpoint(serverConfig: ServerConfig, path: string, checkpointID: string): Observable<AjaxResponse> {
return this.getContentProvider(path).deleteCheckpoint(serverConfig, path, checkpointID);
}
public restoreFromCheckpoint(
serverConfig: ServerConfig,
path: string,
checkpointID: string,
): Observable<AjaxResponse> {
return this.getContentProvider(path).restoreFromCheckpoint(serverConfig, path, checkpointID);
}
private getContentProvider(path: string): IContentProvider {
if (InMemoryContentProviderUtils.fromContentUri(path)) {
return this.inMemoryContentProvider;
}
if (GitHubUtils.fromContentUri(path)) {
return this.gitHubContentProvider;
}
return this.jupyterContentProvider;
}
}
@@ -1,84 +0,0 @@
import { AppState, ContentRef, selectors } from "@nteract/core";
import * as React from "react";
import { connect } from "react-redux";
import { NotebookUtil } from "../NotebookUtil";
import * as NteractUtil from "../NTeractUtil";
interface VirtualCommandBarComponentProps {
kernelSpecName: string;
kernelStatus: string;
currentCellType: string;
isNotebookUntrusted: boolean;
onRender: () => void;
}
class VirtualCommandBarComponent extends React.Component<VirtualCommandBarComponentProps> {
constructor(props: VirtualCommandBarComponentProps) {
super(props);
this.state = {};
}
shouldComponentUpdate(nextProps: VirtualCommandBarComponentProps): boolean {
return (
this.props.kernelStatus !== nextProps.kernelStatus ||
this.props.kernelSpecName !== nextProps.kernelSpecName ||
this.props.currentCellType !== nextProps.currentCellType ||
this.props.isNotebookUntrusted !== nextProps.isNotebookUntrusted
);
}
public render(): JSX.Element {
this.props.onRender && this.props.onRender();
return <></>;
}
}
interface InitialProps {
contentRef: ContentRef;
onRender: () => void;
}
// Redux
const makeMapStateToProps = (
initialState: AppState,
initialProps: InitialProps,
): ((state: AppState) => VirtualCommandBarComponentProps) => {
const { contentRef } = initialProps;
const mapStateToProps = (state: AppState) => {
const content = selectors.content(state, { contentRef });
let kernelStatus, kernelSpecName, currentCellType;
if (!content || content.type !== "notebook") {
return {
kernelStatus,
kernelSpecName,
currentCellType,
isNotebookUntrusted: NotebookUtil.isNotebookUntrusted(state, contentRef),
} as VirtualCommandBarComponentProps;
}
const kernelRef = content.model.kernelRef;
let kernel;
if (kernelRef) {
kernel = selectors.kernel(state, { kernelRef });
}
if (kernel) {
kernelStatus = kernel.status;
kernelSpecName = kernel.kernelSpecName;
}
currentCellType = NteractUtil.getCurrentCellType(content);
return {
kernelStatus,
kernelSpecName,
currentCellType,
isNotebookUntrusted: NotebookUtil.isNotebookUntrusted(state, contentRef),
onRender: initialProps.onRender,
};
};
return mapStateToProps;
};
export default connect(makeMapStateToProps)(VirtualCommandBarComponent);
@@ -1,19 +0,0 @@
import { Observable, of } from "rxjs";
import { AjaxRequest, AjaxResponse } from "rxjs/ajax";
let fakeAjaxResponse: AjaxResponse = {
originalEvent: <Event>(<unknown>undefined),
xhr: new XMLHttpRequest(),
request: <AjaxRequest>(<unknown>null),
status: 200,
response: {},
responseText: "",
responseType: "json",
};
export const sessions = {
create: (): Observable<AjaxResponse> => of(fakeAjaxResponse),
__setResponse: (response: AjaxResponse) => {
fakeAjaxResponse = response;
},
createSpy: undefined as any,
};
@@ -1,153 +0,0 @@
import { CellId } from "@nteract/commutable";
import { ContentRef } from "@nteract/core";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
import { SnapshotFragment, SnapshotRequest } from "./types";
export const CLOSE_NOTEBOOK = "CLOSE_NOTEBOOK";
export interface CloseNotebookAction {
type: "CLOSE_NOTEBOOK";
payload: {
contentRef: ContentRef;
};
}
export const closeNotebook = (payload: { contentRef: ContentRef }): CloseNotebookAction => {
return {
type: CLOSE_NOTEBOOK,
payload,
};
};
export const EXECUTE_FOCUSED_CELL_AND_FOCUS_NEXT = "EXECUTE_FOCUSED_CELL_AND_FOCUS_NEXT";
export interface ExecuteFocusedCellAndFocusNextAction {
type: "EXECUTE_FOCUSED_CELL_AND_FOCUS_NEXT";
payload: {
contentRef: ContentRef;
};
}
export const executeFocusedCellAndFocusNext = (payload: {
contentRef: ContentRef;
}): ExecuteFocusedCellAndFocusNextAction => {
return {
type: EXECUTE_FOCUSED_CELL_AND_FOCUS_NEXT,
payload,
};
};
export const UPDATE_KERNEL_RESTART_DELAY = "UPDATE_KERNEL_RESTART_DELAY";
export interface UpdateKernelRestartDelayAction {
type: "UPDATE_KERNEL_RESTART_DELAY";
payload: {
delayMs: number;
};
}
export const UpdateKernelRestartDelay = (payload: { delayMs: number }): UpdateKernelRestartDelayAction => {
return {
type: UPDATE_KERNEL_RESTART_DELAY,
payload,
};
};
export const SET_HOVERED_CELL = "SET_HOVERED_CELL";
export interface SetHoveredCellAction {
type: "SET_HOVERED_CELL";
payload: {
cellId: CellId;
};
}
export const setHoveredCell = (payload: { cellId: CellId }): SetHoveredCellAction => {
return {
type: SET_HOVERED_CELL,
payload,
};
};
export const TRACE_NOTEBOOK_TELEMETRY = "TRACE_NOTEBOOK_TELEMETRY";
export interface TraceNotebookTelemetryAction {
type: "TRACE_NOTEBOOK_TELEMETRY";
payload: {
action: Action;
actionModifier?: string;
data?: any;
};
}
export const traceNotebookTelemetry = (payload: {
action: Action;
actionModifier?: string;
data?: any;
}): TraceNotebookTelemetryAction => {
return {
type: TRACE_NOTEBOOK_TELEMETRY,
payload,
};
};
export const STORE_CELL_OUTPUT_SNAPSHOT = "STORE_CELL_OUTPUT_SNAPSHOT";
export interface StoreCellOutputSnapshotAction {
type: "STORE_CELL_OUTPUT_SNAPSHOT";
payload: {
cellId: string;
snapshot: SnapshotFragment;
};
}
export const storeCellOutputSnapshot = (payload: {
cellId: string;
snapshot: SnapshotFragment;
}): StoreCellOutputSnapshotAction => {
return {
type: STORE_CELL_OUTPUT_SNAPSHOT,
payload,
};
};
export const STORE_NOTEBOOK_SNAPSHOT = "STORE_NOTEBOOK_SNAPSHOT";
export interface StoreNotebookSnapshotAction {
type: "STORE_NOTEBOOK_SNAPSHOT";
payload: {
imageSrc: string;
requestId: string;
};
}
export const storeNotebookSnapshot = (payload: {
imageSrc: string;
requestId: string;
}): StoreNotebookSnapshotAction => {
return {
type: STORE_NOTEBOOK_SNAPSHOT,
payload,
};
};
export const TAKE_NOTEBOOK_SNAPSHOT = "TAKE_NOTEBOOK_SNAPSHOT";
export interface TakeNotebookSnapshotAction {
type: "TAKE_NOTEBOOK_SNAPSHOT";
payload: SnapshotRequest;
}
export const takeNotebookSnapshot = (payload: SnapshotRequest): TakeNotebookSnapshotAction => {
return {
type: TAKE_NOTEBOOK_SNAPSHOT,
payload,
};
};
export const NOTEBOOK_SNAPSHOT_ERROR = "NOTEBOOK_SNAPSHOT_ERROR";
export interface NotebookSnapshotErrorAction {
type: "NOTEBOOK_SNAPSHOT_ERROR";
payload: {
error: string;
};
}
export const notebookSnapshotError = (payload: { error: string }): NotebookSnapshotErrorAction => {
return {
type: NOTEBOOK_SNAPSHOT_ERROR,
payload,
};
};
@@ -1,96 +0,0 @@
import { AppState, ContentRef, selectors } from "@nteract/core";
import * as React from "react";
import { connect } from "react-redux";
import styled from "styled-components";
import NotebookRenderer from "../../../NotebookRenderer/NotebookRenderer";
import * as TextFile from "./text-file";
const PaddedContainer = styled.div`
padding-left: var(--nt-spacing-l, 10px);
padding-top: var(--nt-spacing-m, 10px);
padding-right: var(--nt-spacing-m, 10px);
`;
const JupyterExtensionContainer = styled.div`
display: flex;
flex-flow: column;
align-items: stretch;
height: 100%;
`;
const JupyterExtensionChoiceContainer = styled.div`
flex: 1 1 auto;
overflow: auto;
`;
interface FileProps {
type: "notebook" | "file" | "dummy";
contentRef: ContentRef;
mimetype?: string | null;
}
export class File extends React.PureComponent<FileProps> {
getChoice = () => {
let choice;
// notebooks don't report a mimetype so we'll use the content.type
if (this.props.type === "notebook") {
choice = <NotebookRenderer contentRef={this.props.contentRef} />;
} else if (this.props.type === "dummy") {
choice = undefined;
} else if (this.props.mimetype === undefined || !TextFile.handles(this.props.mimetype)) {
// This should not happen as we intercept mimetype upstream, but just in case
choice = (
<PaddedContainer>
<pre>
This file type cannot be rendered. Please download the file, in order to view it outside of Data Explorer.
</pre>
</PaddedContainer>
);
} else {
choice = <TextFile.default contentRef={this.props.contentRef} />;
}
return choice;
};
render(): JSX.Element {
const choice = this.getChoice();
// Right now we only handle one kind of editor
// If/when we support more modes, we would case them off here
return (
<React.Fragment>
<JupyterExtensionContainer>
<JupyterExtensionChoiceContainer>{choice}</JupyterExtensionChoiceContainer>
</JupyterExtensionContainer>
</React.Fragment>
);
}
}
interface InitialProps {
contentRef: ContentRef;
}
// Since the contentRef stays unique for the duration of this file,
// we use the makeMapStateToProps pattern to optimize re-render
const makeMapStateToProps = (initialState: AppState, initialProps: InitialProps) => {
const { contentRef } = initialProps;
const mapStateToProps = (state: AppState) => {
const content = selectors.content(state, initialProps);
return {
contentRef,
mimetype: content.mimetype,
type: content.type,
};
};
return mapStateToProps;
};
export const ConnectedFile = connect(makeMapStateToProps)(File);
export default ConnectedFile;
@@ -1,144 +0,0 @@
import { actions, AppState, ContentRef, selectors } from "@nteract/core";
import { IMonacoProps as MonacoEditorProps } from "@nteract/monaco-editor";
import * as React from "react";
import { connect } from "react-redux";
import { Dispatch } from "redux";
import styled from "styled-components";
import * as StringUtils from "../../../../../Utils/StringUtils";
const EditorContainer = styled.div`
position: absolute;
left: 0;
height: 100%;
width: 100%;
.monaco {
height: 100%;
}
`;
interface MappedStateProps {
mimetype: string;
text: string;
contentRef: ContentRef;
theme?: "light" | "dark";
}
interface MappedDispatchProps {
handleChange: (value: string) => void;
}
type TextFileProps = MappedStateProps & MappedDispatchProps;
interface TextFileState {
Editor: React.ComponentType<MonacoEditorProps>;
}
class EditorPlaceholder extends React.PureComponent<MonacoEditorProps> {
render(): JSX.Element {
// TODO: Show a little blocky placeholder
return undefined;
}
}
export class TextFile extends React.PureComponent<TextFileProps, TextFileState> {
constructor(props: TextFileProps) {
super(props);
this.state = {
Editor: EditorPlaceholder,
};
}
handleChange = (source: string) => {
this.props.handleChange(source);
};
componentDidMount(): void {
import(/* webpackChunkName: "monaco-editor" */ "@nteract/monaco-editor").then((module) => {
this.setState({ Editor: module.default });
});
}
render(): JSX.Element {
const Editor = this.state.Editor;
return (
<EditorContainer className="nteract-editor" style={{ position: "static" }}>
<Editor
id={"no-cell-id-for-single-editor"}
contentRef={this.props.contentRef}
theme={this.props.theme === "dark" ? "vs-dark" : "vs"}
language={"plaintext"}
editorFocused
value={this.props.text}
onChange={this.handleChange.bind(this)}
/>
</EditorContainer>
);
}
}
interface InitialProps {
contentRef: ContentRef;
}
function makeMapStateToTextFileProps(
initialState: AppState,
initialProps: InitialProps,
): (state: AppState) => MappedStateProps {
const { contentRef } = initialProps;
const mapStateToTextFileProps = (state: AppState) => {
const content = selectors.content(state, { contentRef });
if (!content || content.type !== "file") {
throw new Error("The text file component must have content");
}
const text = content.model ? content.model.text : "";
return {
contentRef,
mimetype: content.mimetype !== undefined ? content.mimetype : "text/plain",
text,
};
};
return mapStateToTextFileProps;
}
const makeMapDispatchToTextFileProps = (
initialDispatch: Dispatch,
initialProps: InitialProps,
): ((dispatch: Dispatch) => MappedDispatchProps) => {
const { contentRef } = initialProps;
const mapDispatchToTextFileProps = (dispatch: Dispatch) => {
return {
handleChange: (source: string) => {
dispatch(
actions.updateFileText({
contentRef,
text: source,
}),
);
},
};
};
return mapDispatchToTextFileProps;
};
const ConnectedTextFile = connect<MappedStateProps, MappedDispatchProps, InitialProps, AppState>(
makeMapStateToTextFileProps,
makeMapDispatchToTextFileProps,
)(TextFile);
export function handles(mimetype: string) {
return (
!mimetype ||
StringUtils.startsWith(mimetype, "text/") ||
StringUtils.startsWith(mimetype, "application/javascript") ||
StringUtils.startsWith(mimetype, "application/json") ||
StringUtils.startsWith(mimetype, "application/x-ipynb+json")
);
}
export default ConnectedTextFile;
@@ -1,173 +0,0 @@
// Vendor modules
import { CellType, ImmutableNotebook } from "@nteract/commutable";
import { HeaderDataProps } from "@nteract/connected-components/lib/header-editor";
import {
AppState,
ContentRef,
HostRecord,
selectors,
actions,
DirectoryContentRecordProps,
DummyContentRecordProps,
FileContentRecordProps,
NotebookContentRecordProps,
} from "@nteract/core";
import { RecordOf } from "immutable";
import * as React from "react";
import { HotKeys, KeyMap } from "react-hotkeys";
import { connect } from "react-redux";
import { Dispatch } from "redux";
// Local modules
import { default as File } from "./file";
interface IContentsBaseProps {
contentRef: ContentRef;
error?: object | null;
}
interface IStateToProps {
headerData?: HeaderDataProps;
}
interface IDispatchFromProps {
handlers?: any;
onHeaderEditorChange?: (props: HeaderDataProps) => void;
}
type ContentsProps = IContentsBaseProps & IStateToProps & IDispatchFromProps;
class Contents extends React.PureComponent<ContentsProps> {
private keyMap: KeyMap = {
CHANGE_CELL_TYPE: ["ctrl+shift+y", "ctrl+shift+m", "meta+shift+y", "meta+shift+m"],
COPY_CELL: ["ctrl+shift+c", "meta+shift+c"],
CREATE_CELL_ABOVE: ["ctrl+shift+a", "meta+shift+a"],
CREATE_CELL_BELOW: ["ctrl+shift+b", "meta+shift+b"],
CUT_CELL: ["ctrl+shift+x", "meta+shift+x"],
DELETE_CELL: ["ctrl+shift+d", "meta+shift+d"],
EXECUTE_ALL_CELLS: ["alt+r a"],
INTERRUPT_KERNEL: ["alt+r i"],
KILL_KERNEL: ["alt+r k"],
OPEN: ["ctrl+o", "meta+o"],
PASTE_CELL: ["ctrl+shift+v"],
RESTART_KERNEL: ["alt+r r", "alt+r c", "alt+r a"],
SAVE: ["ctrl+s", "ctrl+shift+s", "meta+s", "meta+shift+s"],
};
render(): JSX.Element {
const { contentRef, handlers } = this.props;
if (!contentRef) {
return <></>;
}
return (
<React.Fragment>
<HotKeys keyMap={this.keyMap} handlers={handlers} className="hotKeys">
<File contentRef={contentRef} />
</HotKeys>
</React.Fragment>
);
}
}
const makeMapStateToProps: any = (initialState: AppState, initialProps: { contentRef: ContentRef }) => {
const host: HostRecord = initialState.app.host;
if (host.type !== "jupyter") {
throw new Error("this component only works with jupyter apps");
}
const mapStateToProps = (state: AppState): Partial<ContentsProps> => {
const contentRef: ContentRef = initialProps.contentRef;
if (!contentRef) {
throw new Error("cant display without a contentRef");
}
const content:
| RecordOf<NotebookContentRecordProps>
| RecordOf<DummyContentRecordProps>
| RecordOf<FileContentRecordProps>
| RecordOf<DirectoryContentRecordProps>
| undefined = selectors.content(state, { contentRef });
if (!content) {
return {
contentRef: undefined,
error: undefined,
headerData: undefined,
};
}
let headerData: HeaderDataProps = {
authors: [],
description: "",
tags: [],
title: "",
};
// If a notebook, we need to read in the headerData if available
if (content.type === "notebook") {
const notebook: ImmutableNotebook = content.model.get("notebook");
const metadata: any = notebook.metadata.toJS();
const { authors = [], description = "", tags = [], title = "" } = metadata;
// Updates
headerData = Object.assign({}, headerData, {
authors,
description,
tags,
title,
});
}
return {
contentRef,
error: content.error,
headerData,
};
};
return mapStateToProps;
};
const mapDispatchToProps = (dispatch: Dispatch, ownProps: ContentsProps): object => {
const { contentRef } = ownProps;
return {
onHeaderEditorChange: (props: HeaderDataProps) => {
return dispatch(
actions.overwriteMetadataFields({
...props,
contentRef: ownProps.contentRef,
}),
);
},
// `HotKeys` handlers object
// see: https://github.com/greena13/react-hotkeys#defining-handlers
handlers: {
CHANGE_CELL_TYPE: (event: KeyboardEvent) => {
const type: CellType = event.key === "Y" ? "code" : "markdown";
return dispatch(actions.changeCellType({ to: type, contentRef }));
},
COPY_CELL: () => dispatch(actions.copyCell({ contentRef })),
CREATE_CELL_ABOVE: () => dispatch(actions.createCellAbove({ cellType: "code", contentRef })),
CREATE_CELL_BELOW: () => dispatch(actions.createCellBelow({ cellType: "code", contentRef })),
CUT_CELL: () => dispatch(actions.cutCell({ contentRef })),
DELETE_CELL: () => dispatch(actions.deleteCell({ contentRef })),
EXECUTE_ALL_CELLS: () => dispatch(actions.executeAllCells({ contentRef })),
INTERRUPT_KERNEL: () => dispatch(actions.interruptKernel({})),
KILL_KERNEL: () => dispatch(actions.killKernel({ restarting: false })),
PASTE_CELL: () => dispatch(actions.pasteCell({ contentRef })),
RESTART_KERNEL: (event: KeyboardEvent) => {
const outputHandling: "None" | "Clear All" | "Run All" =
event.key === "r" ? "None" : event.key === "a" ? "Run All" : "Clear All";
return dispatch(actions.restartKernel({ outputHandling, contentRef }));
},
SAVE: () => dispatch(actions.save({ contentRef })),
},
};
};
export default connect(makeMapStateToProps, mapDispatchToProps)(Contents);
@@ -1,477 +0,0 @@
import { makeNotebookRecord } from "@nteract/commutable";
import { actions, state } from "@nteract/core";
import * as Immutable from "immutable";
import { StateObservable } from "redux-observable";
import { Subject, of } from "rxjs";
import { toArray } from "rxjs/operators";
import * as sinon from "sinon";
import { NotebookUtil } from "../NotebookUtil";
import { launchWebSocketKernelEpic } from "./epics";
import { CdbAppState, makeCdbRecord } from "./types";
import { sessions } from "rx-jupyter";
describe("Extract kernel from notebook", () => {
it("Reads metadata kernelspec first", () => {
const fakeNotebook = makeNotebookRecord({
metadata: Immutable.Map({
kernelspec: {
display_name: "Python 3",
language: "python",
name: "python3",
},
language_info: {
name: "python",
version: "3.7.3",
mimetype: "text/x-python",
codemirror_mode: {
name: "ipython",
version: 3,
},
pygments_lexer: "ipython3",
nbconvert_exporter: "python",
file_extension: ".py",
},
}),
});
const result = NotebookUtil.extractNewKernel("blah", fakeNotebook);
expect(result.kernelSpecName).toEqual("python3");
});
it("Reads language info in metadata if kernelspec not present", () => {
const fakeNotebook = makeNotebookRecord({
metadata: Immutable.Map({
language_info: {
name: "python",
version: "3.7.3",
mimetype: "text/x-python",
codemirror_mode: {
name: "ipython",
version: 3,
},
pygments_lexer: "ipython3",
nbconvert_exporter: "python",
file_extension: ".py",
},
}),
});
const result = NotebookUtil.extractNewKernel("blah", fakeNotebook);
expect(result.kernelSpecName).toEqual("python");
});
it("Returns nothing if no kernelspec nor language info is found in metadata", () => {
const fakeNotebook = makeNotebookRecord({
metadata: Immutable.Map({
blah: "this should be ignored",
}),
});
const result = NotebookUtil.extractNewKernel("blah", fakeNotebook);
expect(result.kernelSpecName).toEqual(undefined);
});
});
const initialState = {
app: state.makeAppRecord({
host: state.makeJupyterHostRecord({
type: "jupyter",
token: "eh",
basePath: "/",
}),
}),
comms: state.makeCommsRecord(),
config: Immutable.Map({}),
core: state.makeStateRecord({
kernelRef: "fake",
entities: state.makeEntitiesRecord({
contents: state.makeContentsRecord({
byRef: Immutable.Map({
fakeContentRef: state.makeNotebookContentRecord(),
}),
}),
kernels: state.makeKernelsRecord({
byRef: Immutable.Map({
fake: state.makeRemoteKernelRecord({
type: "websocket",
channels: new Subject<any>(),
kernelSpecName: "fancy",
id: "0",
}),
}),
}),
}),
}),
cdb: makeCdbRecord({
databaseAccountName: "dbAccountName",
defaultExperience: "defaultExperience",
}),
};
describe("launchWebSocketKernelEpic", () => {
const createSpy = sinon.spy(sessions, "create");
const contentRef = "fakeContentRef";
const kernelRef = "fake";
it("launches remote kernels", async () => {
const state$ = new StateObservable(new Subject<CdbAppState>() as any, initialState);
const cwd = "/";
const kernelId = "123";
const kernelSpecName = "kernelspecname";
const sessionId = "sessionId";
const action$ = of(
actions.launchKernelByName({
contentRef,
kernelRef,
kernelSpecName,
cwd,
selectNextKernel: true,
}),
);
(sessions as any).__setResponse({
originalEvent: undefined,
xhr: new XMLHttpRequest(),
request: null,
status: 200,
response: {
id: sessionId,
path: "notebooks/Untitled7.ipynb",
name: "",
type: "notebook",
kernel: {
id: kernelId,
name: "kernel_launched",
last_activity: "2019-11-07T14:29:54.432454Z",
execution_state: "starting",
connections: 0,
},
notebook: {
path: "notebooks/Untitled7.ipynb",
name: "",
},
},
responseText: null,
responseType: "json",
});
const responseActions = await launchWebSocketKernelEpic(action$, state$).pipe(toArray()).toPromise();
expect(responseActions).toMatchObject([
{
type: actions.LAUNCH_KERNEL_SUCCESSFUL,
payload: {
contentRef,
kernelRef,
selectNextKernel: true,
kernel: {
info: null,
sessionId: sessionId,
type: "websocket",
kernelSpecName,
cwd,
id: kernelId,
},
},
},
]);
});
it("launches any kernel with no kernelspecs in the state", async () => {
const state$ = new StateObservable(new Subject<CdbAppState>() as any, initialState);
const cwd = "/";
const kernelId = "123";
const kernelSpecName = "kernelspecname";
const sessionId = "sessionId";
const action$ = of(
actions.launchKernelByName({
contentRef,
kernelRef,
kernelSpecName,
cwd,
selectNextKernel: true,
}),
);
(sessions as any).__setResponse({
originalEvent: undefined,
xhr: new XMLHttpRequest(),
request: null,
status: 200,
response: {
id: sessionId,
path: "notebooks/Untitled7.ipynb",
name: "",
type: "notebook",
kernel: {
id: kernelId,
name: "kernel_launched",
last_activity: "2019-11-07T14:29:54.432454Z",
execution_state: "starting",
connections: 0,
},
notebook: {
path: "notebooks/Untitled7.ipynb",
name: "",
},
},
responseText: null,
responseType: "json",
});
await launchWebSocketKernelEpic(action$, state$).pipe(toArray()).toPromise();
expect(createSpy.lastCall.args[1]).toMatchObject({
kernel: {
name: kernelSpecName,
},
});
});
it("launches no kernel if no kernel is specified and state has no kernelspecs", async () => {
const state$ = new StateObservable(new Subject<CdbAppState>() as any, initialState);
const cwd = "/";
const kernelId = "123";
const kernelSpecName = "kernelspecname";
const sessionId = "sessionId";
const action$ = of(
actions.launchKernelByName({
contentRef,
kernelRef,
kernelSpecName: undefined,
cwd,
selectNextKernel: true,
}),
);
(sessions as any).__setResponse({
originalEvent: undefined,
xhr: new XMLHttpRequest(),
request: null,
status: 200,
response: {
id: sessionId,
path: "notebooks/Untitled7.ipynb",
name: "",
type: "notebook",
kernel: {
id: kernelId,
name: "kernel_launched",
last_activity: "2019-11-07T14:29:54.432454Z",
execution_state: "starting",
connections: 0,
},
notebook: {
path: "notebooks/Untitled7.ipynb",
name: "",
},
},
responseText: null,
responseType: "json",
});
const responseActions = await launchWebSocketKernelEpic(action$, state$).pipe(toArray()).toPromise();
expect(responseActions).toMatchObject([
{
type: actions.LAUNCH_KERNEL_FAILED,
},
]);
});
it("emits an error if backend returns an error", async () => {
const state$ = new StateObservable(new Subject<CdbAppState>() as any, initialState);
const cwd = "/";
const action$ = of(
actions.launchKernelByName({
contentRef,
kernelRef,
kernelSpecName: undefined,
cwd,
selectNextKernel: true,
}),
);
(sessions as any).__setResponse({
originalEvent: undefined,
xhr: new XMLHttpRequest(),
request: null,
status: 500,
response: null,
responseText: null,
responseType: "json",
});
const responseActions = await launchWebSocketKernelEpic(action$, state$).pipe(toArray()).toPromise();
expect(responseActions).toMatchObject([
{
type: actions.LAUNCH_KERNEL_FAILED,
},
]);
});
describe("Choose correct kernelspecs to launch", () => {
beforeAll(() => {
// Initialize kernelspecs with 2 supported kernels
const createKernelSpecsRecord = (): Immutable.RecordOf<state.KernelspecsRecordProps> =>
state.makeKernelspecsRecord({
byRef: Immutable.Map({
kernelspecsref: state.makeKernelspecsByRefRecord({
defaultKernelName: "kernel2",
byName: Immutable.Map({
kernel1: state.makeKernelspec({
name: "kernel1",
argv: Immutable.List([]),
env: Immutable.Map(),
interruptMode: "interruptMode1",
language: "language1",
displayName: "Kernel One",
metadata: Immutable.Map(),
resources: Immutable.Map(),
}),
kernel2: state.makeKernelspec({
name: "kernel2",
argv: Immutable.List([]),
env: Immutable.Map(),
interruptMode: "interruptMode2",
language: "language2",
displayName: "Kernel Two",
metadata: Immutable.Map(),
resources: Immutable.Map(),
}),
}),
}),
}),
refs: Immutable.List(["kernelspecsref"]),
});
initialState.core = initialState.core
.setIn(["entities", "kernelspecs"], createKernelSpecsRecord())
.set("currentKernelspecsRef", "kernelspecsref");
// some fake response we don't care about
(sessions as any).__setResponse({
originalEvent: undefined,
xhr: new XMLHttpRequest(),
request: null,
status: 200,
response: {
id: "sessionId",
path: "notebooks/Untitled7.ipynb",
name: "",
type: "notebook",
kernel: {
id: "kernelId",
name: "kernel_launched",
last_activity: "2019-11-07T14:29:54.432454Z",
execution_state: "starting",
connections: 0,
},
notebook: {
path: "notebooks/Untitled7.ipynb",
name: "",
},
},
responseText: null,
responseType: "json",
});
});
it("launches supported kernel in kernelspecs", async () => {
const state$ = new StateObservable(new Subject<CdbAppState>() as any, initialState);
const action$ = of(
actions.launchKernelByName({
contentRef,
kernelRef,
kernelSpecName: "kernel2",
cwd: "cwd",
selectNextKernel: true,
}),
);
await launchWebSocketKernelEpic(action$, state$).pipe(toArray()).toPromise();
expect(createSpy.lastCall.args[1]).toMatchObject({
kernel: {
name: "kernel2",
},
});
});
it("launches undefined kernel uses default kernel from kernelspecs", async () => {
const state$ = new StateObservable(new Subject<CdbAppState>() as any, initialState);
const action$ = of(
actions.launchKernelByName({
contentRef,
kernelRef,
kernelSpecName: undefined,
cwd: "cwd",
selectNextKernel: true,
}),
);
await launchWebSocketKernelEpic(action$, state$).pipe(toArray()).toPromise();
expect(createSpy.lastCall.args[1]).toMatchObject({
kernel: {
name: "kernel2",
},
});
});
it("launches unsupported kernel uses default kernel from kernelspecs", async () => {
const state$ = new StateObservable(new Subject<CdbAppState>() as any, initialState);
const action$ = of(
actions.launchKernelByName({
contentRef,
kernelRef,
kernelSpecName: "This is an unknown kernelspec",
cwd: "cwd",
selectNextKernel: true,
}),
);
await launchWebSocketKernelEpic(action$, state$).pipe(toArray()).toPromise();
expect(createSpy.lastCall.args[1]).toMatchObject({
kernel: {
name: "kernel2",
},
});
});
it("launches unsupported kernel uses kernelspecs with similar name", async () => {
const state$ = new StateObservable(new Subject<CdbAppState>() as any, initialState);
const action$ = of(
actions.launchKernelByName({
contentRef,
kernelRef,
kernelSpecName: "ernel1",
cwd: "cwd",
selectNextKernel: true,
}),
);
await launchWebSocketKernelEpic(action$, state$).pipe(toArray()).toPromise();
expect(createSpy.lastCall.args[1]).toMatchObject({
kernel: {
name: "kernel1",
},
});
});
});
});
File diff suppressed because it is too large Load Diff
@@ -1,35 +0,0 @@
// This replicates transform loading from:
// https://github.com/nteract/nteract/blob/master/applications/jupyter-extension/nteract_on_jupyter/app/contents/notebook.tsx
export default (props: { addTransform: (component: any) => void }) => {
import(/* webpackChunkName: "plotly" */ "@nteract/transform-plotly").then((module) => {
props.addTransform(module.default);
props.addTransform(module.PlotlyNullTransform);
});
import(/* webpackChunkName: "tabular-dataresource" */ "@nteract/data-explorer").then((module) => {
props.addTransform(module.default);
});
import(/* webpackChunkName: "jupyter-widgets" */ "@nteract/jupyter-widgets").then((module) => {
props.addTransform(module.WidgetDisplay);
});
import("@nteract/transform-model-debug").then((module) => {
props.addTransform(module.default);
});
import(/* webpackChunkName: "vega-transform" */ "@nteract/transform-vega").then((module) => {
props.addTransform(module.VegaLite1);
props.addTransform(module.VegaLite2);
props.addTransform(module.VegaLite3);
props.addTransform(module.VegaLite4);
props.addTransform(module.Vega2);
props.addTransform(module.Vega3);
props.addTransform(module.Vega4);
props.addTransform(module.Vega5);
});
// TODO: The geojson transform will likely need some work because of the basemap URL(s)
// import GeoJSONTransform from "@nteract/transform-geojson";
};
@@ -1,102 +0,0 @@
import { actions, CoreRecord, reducers as nteractReducers } from "@nteract/core";
import { Action } from "redux";
import * as cdbActions from "./actions";
import { CdbRecord } from "./types";
export const coreReducer = (state: CoreRecord, action: Action) => {
let typedAction;
switch (action.type) {
case cdbActions.CLOSE_NOTEBOOK: {
typedAction = action as cdbActions.CloseNotebookAction;
return state.setIn(
["entities", "contents", "byRef"],
state.entities.contents.byRef.delete(typedAction.payload.contentRef),
);
}
case actions.CHANGE_KERNEL_BY_NAME: {
// Update content metadata
typedAction = action as actions.ChangeKernelByName;
const kernelSpecName = typedAction.payload.kernelSpecName;
if (!state.currentKernelspecsRef) {
return state;
}
const currentKernelspecs = state.entities.kernelspecs.byRef.get(state.currentKernelspecsRef);
if (!currentKernelspecs) {
return state;
}
// Find kernelspecs by name
const kernelspecs = currentKernelspecs.byName.get(kernelSpecName);
if (!kernelspecs) {
return state;
}
const path = [
"entities",
"contents",
"byRef",
typedAction.payload.contentRef,
"model",
"notebook",
"metadata",
"kernelspec",
];
// Update metadata
return state
.setIn(path.concat("name"), kernelspecs.name)
.setIn(path.concat("displayName"), kernelspecs.displayName)
.setIn(path.concat("language"), kernelspecs.language);
}
default:
return nteractReducers.core(state as any, action as any);
}
};
export const cdbReducer = (state: CdbRecord, action: Action) => {
if (!state) {
return null;
}
switch (action.type) {
case cdbActions.UPDATE_KERNEL_RESTART_DELAY: {
const typedAction = action as cdbActions.UpdateKernelRestartDelayAction;
return state.set("kernelRestartDelayMs", typedAction.payload.delayMs);
}
case cdbActions.SET_HOVERED_CELL: {
const typedAction = action as cdbActions.SetHoveredCellAction;
return state.set("hoveredCellId", typedAction.payload.cellId);
}
case cdbActions.STORE_CELL_OUTPUT_SNAPSHOT: {
const typedAction = action as cdbActions.StoreCellOutputSnapshotAction;
state.cellOutputSnapshots.set(typedAction.payload.cellId, typedAction.payload.snapshot);
// TODO Simpler datastructure to instantiate new Map?
return state.set("cellOutputSnapshots", new Map(state.cellOutputSnapshots));
}
case cdbActions.STORE_NOTEBOOK_SNAPSHOT: {
const typedAction = action as cdbActions.StoreNotebookSnapshotAction;
// Clear pending request
return state.set("notebookSnapshot", typedAction.payload).set("pendingSnapshotRequest", undefined);
}
case cdbActions.TAKE_NOTEBOOK_SNAPSHOT: {
const typedAction = action as cdbActions.TakeNotebookSnapshotAction;
// Clear previous snapshots
return state
.set("cellOutputSnapshots", new Map())
.set("notebookSnapshot", undefined)
.set("notebookSnapshotError", undefined)
.set("pendingSnapshotRequest", typedAction.payload);
}
case cdbActions.NOTEBOOK_SNAPSHOT_ERROR: {
const typedAction = action as cdbActions.NotebookSnapshotErrorAction;
return state.set("notebookSnapshotError", typedAction.payload.error);
}
}
return state;
};
@@ -1,13 +0,0 @@
import { getCoreEpics } from "./store";
import { epics } from "@nteract/core";
describe("configure redux store", () => {
it("configures store with correct epic if based on autoStartKernelOnNotebookOpen", () => {
// For now, assume launchKernelWhenNotebookSetEpic is the last epic
let filteredEpics = getCoreEpics(true);
expect(filteredEpics.pop()).toEqual(epics.launchKernelWhenNotebookSetEpic);
filteredEpics = getCoreEpics(false);
expect(filteredEpics.pop()).not.toEqual(epics.launchKernelWhenNotebookSetEpic);
});
});
@@ -1,111 +0,0 @@
import { AppState, epics as coreEpics, IContentProvider, reducers } from "@nteract/core";
import { configuration } from "@nteract/mythic-configuration";
import { makeConfigureStore } from "@nteract/myths";
import { stringifyError } from "Common/stringifyError";
import { AnyAction, compose, Dispatch, Middleware, MiddlewareAPI, Store } from "redux";
import { Epic } from "redux-observable";
import { Observable } from "rxjs";
import { catchError } from "rxjs/operators";
import { allEpics } from "./epics";
import { cdbReducer, coreReducer } from "./reducers";
import { CdbAppState } from "./types";
const composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
export default function configureStore(
initialState: Partial<CdbAppState>,
contentProvider: IContentProvider,
onTraceFailure: (title: string, message: string) => void,
customMiddlewares?: Middleware<{}, any, Dispatch<AnyAction>>[],
autoStartKernelOnNotebookOpen?: boolean,
): Store<CdbAppState, AnyAction> {
/**
* Catches errors in reducers
*/
const catchErrorMiddleware: Middleware =
<D extends Dispatch<AnyAction>, S extends AppState>({ dispatch, getState }: MiddlewareAPI<D, S>) =>
(next: Dispatch<AnyAction>) =>
<A extends AnyAction>(action: A): any => {
try {
next(action);
} catch (error) {
traceFailure("Reducer failure", error);
}
};
const protect = (epic: Epic) => {
return (action$: Observable<any>, state$: any, dependencies: any) =>
epic(action$ as any, state$, dependencies).pipe(
catchError((error, caught) => {
traceFailure("Epic failure", error);
return caught;
}) as any,
);
};
const traceFailure = (title: string, error: any) => {
if (error instanceof Error) {
onTraceFailure(title, `${error.message} ${stringifyError(error.stack)}`);
console.error(error);
} else {
onTraceFailure(title, error.message);
}
};
const protectEpics = (epics: Epic[]): Epic[] => {
return epics.map((epic) => protect(epic)) as any;
};
const filteredCoreEpics = getCoreEpics(autoStartKernelOnNotebookOpen);
const mythConfigureStore = makeConfigureStore<CdbAppState>()({
packages: [configuration],
reducers: {
app: reducers.app,
core: coreReducer as any,
cdb: cdbReducer,
},
epics: protectEpics([...filteredCoreEpics, ...allEpics] as any),
epicDependencies: { contentProvider },
epicMiddleware: customMiddlewares.concat(catchErrorMiddleware),
enhancer: composeEnhancers,
});
const store = mythConfigureStore(initialState as any);
// TODO Fix typing issue here: createStore() output type doesn't quite match AppState
// return store as Store<AppState, AnyAction>;
return store as any;
}
export const getCoreEpics = (autoStartKernelOnNotebookOpen: boolean): Epic[] => {
// This list needs to be consistent and in sync with core.allEpics until we figure
// out how to safely filter out the ones we are overriding here.
const filteredCoreEpics = [
coreEpics.executeCellEpic,
coreEpics.executeFocusedCellEpic,
coreEpics.executeCellAfterKernelLaunchEpic,
coreEpics.sendExecuteRequestEpic,
coreEpics.updateDisplayEpic,
coreEpics.executeAllCellsEpic,
coreEpics.commListenEpic,
coreEpics.interruptKernelEpic,
coreEpics.lazyLaunchKernelEpic,
coreEpics.killKernelEpic,
coreEpics.watchExecutionStateEpic,
coreEpics.restartKernelEpic,
coreEpics.fetchKernelspecsEpic,
coreEpics.fetchContentEpic,
coreEpics.updateContentEpic,
coreEpics.saveContentEpic,
coreEpics.publishToBookstore,
coreEpics.publishToBookstoreAfterSave,
coreEpics.sendInputReplyEpic,
];
if (autoStartKernelOnNotebookOpen) {
filteredCoreEpics.push(coreEpics.launchKernelWhenNotebookSetEpic);
}
return filteredCoreEpics as any;
};
@@ -1,79 +0,0 @@
import { CellId } from "@nteract/commutable";
import { AppState } from "@nteract/core";
import { MessageType } from "@nteract/messaging";
import * as Immutable from "immutable";
import { Notebook } from "../../../Common/Constants";
export interface SnapshotFragment {
image: HTMLImageElement;
boundingClientRect: DOMRect;
requestId: string;
}
export type SnapshotRequest = NotebookSnapshotRequest | CellSnapshotRequest;
interface NotebookSnapshotRequestBase {
requestId: string;
aspectRatio: number;
notebookContentRef: string; // notebook redux contentRef
downloadFilename?: string; // Optional: will download as a file
}
interface NotebookSnapshotRequest extends NotebookSnapshotRequestBase {
type: "notebook";
}
interface CellSnapshotRequest extends NotebookSnapshotRequestBase {
type: "celloutput";
cellId: string;
}
export interface CdbRecordProps {
databaseAccountName: string | undefined;
defaultExperience: string | undefined;
kernelRestartDelayMs: number;
hoveredCellId: CellId | undefined;
cellOutputSnapshots: Map<string, SnapshotFragment>;
notebookSnapshot?: { imageSrc: string; requestId: string };
pendingSnapshotRequest?: SnapshotRequest;
notebookSnapshotError?: string;
}
export type CdbRecord = Immutable.RecordOf<CdbRecordProps>;
export interface CdbAppState extends AppState {
cdb: CdbRecord;
}
export const makeCdbRecord = Immutable.Record<CdbRecordProps>({
databaseAccountName: undefined,
defaultExperience: undefined,
kernelRestartDelayMs: Notebook.kernelRestartInitialDelayMs,
hoveredCellId: undefined,
cellOutputSnapshots: new Map(),
notebookSnapshot: undefined,
pendingSnapshotRequest: undefined,
notebookSnapshotError: undefined,
});
export interface JupyterMessage<MT extends MessageType = MessageType, C = any> {
header: JupyterMessageHeader<MT>;
parent_header:
| JupyterMessageHeader<any>
| {
msg_id?: string;
};
metadata: object;
content: C;
channel: string;
buffers?: Uint8Array | null;
}
export interface JupyterMessageHeader<MT extends MessageType = MessageType> {
msg_id: string;
username: string;
date: string;
msg_type: MT;
version: string;
session: string;
token: string;
}
@@ -1,195 +0,0 @@
/**
* Notebook container related stuff
*/
import { useDialog } from "Explorer/Controls/Dialog";
import promiseRetry, { AbortError, Options } from "p-retry";
import { PhoenixClient } from "Phoenix/PhoenixClient";
import * as Constants from "../../Common/Constants";
import { ConnectionStatusType, HttpHeaders, HttpStatusCodes, Notebook, PoolIdType } from "../../Common/Constants";
import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
import * as Logger from "../../Common/Logger";
import * as DataModels from "../../Contracts/DataModels";
import { IPhoenixServiceInfo, IProvisionData, IResponse } from "../../Contracts/DataModels";
import { userContext } from "../../UserContext";
import { getAuthorizationHeader } from "../../Utils/AuthorizationUtils";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { useNotebook } from "./useNotebook";
export class NotebookContainerClient {
private clearReconnectionAttemptMessage? = () => {};
private isResettingWorkspace: boolean;
private phoenixClient: PhoenixClient;
private retryOptions: Options;
private scheduleTimerId: NodeJS.Timeout;
constructor(private onConnectionLost: () => void) {
this.phoenixClient = new PhoenixClient(userContext?.databaseAccount?.id);
this.retryOptions = {
retries: Notebook.retryAttempts,
maxTimeout: Notebook.retryAttemptDelayMs,
minTimeout: Notebook.retryAttemptDelayMs,
};
this.initHeartbeat(Constants.Notebook.heartbeatDelayMs);
}
private initHeartbeat(delayMs: number): void {
this.scheduleHeartbeat(delayMs);
useNotebook.subscribe(
() => this.scheduleHeartbeat(delayMs),
(state) => state.notebookServerInfo,
);
}
private scheduleHeartbeat(delayMs: number) {
if (this.scheduleTimerId) {
clearInterval(this.scheduleTimerId);
}
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
if (notebookServerInfo?.notebookServerEndpoint) {
this.scheduleTimerId = setInterval(async () => {
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
if (notebookServerInfo?.notebookServerEndpoint) {
const memoryUsageInfo = await this.getMemoryUsage();
useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo);
}
}, delayMs);
}
}
public async getMemoryUsage(): Promise<DataModels.MemoryUsageInfo> {
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
if (!notebookServerInfo || !notebookServerInfo.notebookServerEndpoint) {
const error = "No server endpoint detected";
Logger.logError(error, "NotebookContainerClient/getMemoryUsage");
return Promise.reject(error);
}
if (this.isResettingWorkspace) {
return undefined;
}
const { notebookServerEndpoint, authToken } = this.getNotebookServerConfig();
try {
const runMemoryAsync = async () => {
return await this._getMemoryAsync(notebookServerEndpoint, authToken);
};
return await promiseRetry(runMemoryAsync, this.retryOptions);
} catch (error) {
Logger.logError(getErrorMessage(error), "NotebookContainerClient/getMemoryUsage");
if (!this.clearReconnectionAttemptMessage) {
this.clearReconnectionAttemptMessage = logConsoleProgress(
"Connection lost with Notebook server. Attempting to reconnect...",
);
}
this.onConnectionLost();
return undefined;
}
}
private async _getMemoryAsync(
notebookServerEndpoint: string,
authToken: string,
): Promise<DataModels.MemoryUsageInfo> {
if (this.shouldExecuteMemoryCall()) {
const response = await fetch(`${notebookServerEndpoint}api/metrics/memory`, {
method: "GET",
headers: {
Authorization: authToken,
"content-type": "application/json",
},
});
if (response.ok) {
if (this.clearReconnectionAttemptMessage) {
this.clearReconnectionAttemptMessage();
this.clearReconnectionAttemptMessage = undefined;
}
const memoryUsageInfo = await response.json();
if (memoryUsageInfo) {
return {
totalKB: memoryUsageInfo.total,
freeKB: memoryUsageInfo.free,
};
}
} else if (response.status === HttpStatusCodes.NotFound) {
throw new AbortError(response.statusText);
}
throw new Error(response.statusText);
} else {
return undefined;
}
}
private shouldExecuteMemoryCall(): boolean {
return (
useNotebook.getState().containerStatus?.status === Constants.ContainerStatusType.Active &&
useNotebook.getState().connectionInfo?.status === ConnectionStatusType.Connected
);
}
public async resetWorkspace(): Promise<IResponse<IPhoenixServiceInfo>> {
this.isResettingWorkspace = true;
let response: IResponse<IPhoenixServiceInfo>;
try {
response = await this._resetWorkspace();
} catch (error) {
Promise.reject(error);
return response;
}
this.isResettingWorkspace = false;
return response;
}
private async _resetWorkspace(): Promise<IResponse<IPhoenixServiceInfo>> {
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
if (!notebookServerInfo || !notebookServerInfo.notebookServerEndpoint) {
const error = "No server endpoint detected";
Logger.logError(error, "NotebookContainerClient/resetWorkspace");
return Promise.reject(error);
}
try {
if (useNotebook.getState().isPhoenixNotebooks) {
const provisionData: IProvisionData = {
cosmosEndpoint: userContext.databaseAccount.properties.documentEndpoint,
poolId: PoolIdType.DefaultPoolId,
};
return await this.phoenixClient.resetContainer(provisionData);
}
return null;
} catch (error) {
Logger.logError(getErrorMessage(error), "NotebookContainerClient/resetWorkspace");
if (error?.status === HttpStatusCodes.Forbidden && error.message) {
useDialog.getState().showOkModalDialog("Connection Failed", `${error.message}`);
} else {
useDialog
.getState()
.showOkModalDialog(
"Connection Failed",
"We are unable to connect to the temporary workspace. Please try again in a few minutes. If the error persists, file a support ticket.",
);
}
throw error;
}
}
private getNotebookServerConfig(): { notebookServerEndpoint: string; authToken: string } {
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
const authToken: string = notebookServerInfo.authToken ? `Token ${notebookServerInfo.authToken}` : undefined;
return {
notebookServerEndpoint: notebookServerInfo.notebookServerEndpoint,
authToken,
};
}
private getHeaders(): HeadersInit {
const authorizationHeader = getAuthorizationHeader();
return {
[authorizationHeader.header]: authorizationHeader.token,
[HttpHeaders.contentType]: "application/json",
};
}
}
@@ -1,311 +0,0 @@
import { stringifyNotebook } from "@nteract/commutable";
import { FileType, IContent, IContentProvider, IEmptyContent, ServerConfig } from "@nteract/core";
import { cloneDeep } from "lodash";
import { AjaxResponse } from "rxjs/ajax";
import * as StringUtils from "../../Utils/StringUtils";
import * as FileSystemUtil from "./FileSystemUtil";
import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem";
import { NotebookUtil } from "./NotebookUtil";
import { useNotebook } from "./useNotebook";
export class NotebookContentClient {
constructor(private contentProvider: IContentProvider) {}
/**
* This updates the item and points all the children's parent to this item
* @param item
*/
public async updateItemChildren(item: NotebookContentItem): Promise<NotebookContentItem> {
const subItems = await this.fetchNotebookFiles(item.path);
const clonedItem = cloneDeep(item);
subItems.forEach((subItem) => (subItem.parent = clonedItem));
clonedItem.children = subItems;
return clonedItem;
}
// TODO: Delete this function when ResourceTreeAdapter is removed.
public async updateItemChildrenInPlace(item: NotebookContentItem): Promise<void> {
return this.fetchNotebookFiles(item.path).then((subItems) => {
item.children = subItems;
subItems.forEach((subItem) => (subItem.parent = item));
});
}
/**
*
* @param parent parent folder
*/
public async createNewNotebookFile(
parent: NotebookContentItem,
isGithubTree?: boolean,
): Promise<NotebookContentItem> {
if (!parent || parent.type !== NotebookContentItemType.Directory) {
throw new Error(`Parent must be a directory: ${parent}`);
}
const type = "notebook";
return this.contentProvider
.create<"notebook">(this.getServerConfig(), parent.path, { type })
.toPromise()
.then((xhr: AjaxResponse) => {
if (typeof xhr.response === "string") {
throw new Error(`jupyter server response invalid: ${xhr.response}`);
}
if (xhr.response.type !== type) {
throw new Error(`jupyter server response not for notebook: ${xhr.response}`);
}
const notebookFile = xhr.response;
const item = NotebookUtil.createNotebookContentItem(notebookFile.name, notebookFile.path, notebookFile.type);
useNotebook.getState().insertNotebookItem(parent, cloneDeep(item), isGithubTree);
// TODO: delete when ResourceTreeAdapter is removed
if (parent.children) {
item.parent = parent;
parent.children.push(item);
}
return item;
});
}
public async deleteContentItem(item: NotebookContentItem, isGithubTree?: boolean): Promise<void> {
const path = await this.deleteNotebookFile(item.path);
useNotebook.getState().deleteNotebookItem(item, isGithubTree);
// TODO: Delete once old resource tree is removed
if (!path || path !== item.path) {
throw new Error("No path provided");
}
if (item.parent && item.parent.children) {
// Remove deleted child
const newChildren = item.parent.children.filter((child) => child.path !== path);
item.parent.children = newChildren;
}
}
/**
*
* @param name file name
* @param content file content string
* @param parent parent folder
*/
public async uploadFileAsync(
name: string,
content: string,
parent: NotebookContentItem,
isGithubTree?: boolean,
): Promise<NotebookContentItem> {
if (!parent || parent.type !== NotebookContentItemType.Directory) {
throw new Error(`Parent must be a directory: ${parent}`);
}
const filepath = NotebookUtil.getFilePath(parent.path, name);
if (await this.checkIfFilepathExists(filepath)) {
throw new Error(`File already exists: ${filepath}`);
}
const model: Partial<IContent<"file">> = {
content,
format: "text",
name,
type: "file",
};
return this.contentProvider
.save(this.getServerConfig(), filepath, model)
.toPromise()
.then((xhr: AjaxResponse) => {
const notebookFile = xhr.response;
const item = NotebookUtil.createNotebookContentItem(notebookFile.name, notebookFile.path, notebookFile.type);
useNotebook.getState().insertNotebookItem(parent, cloneDeep(item), isGithubTree);
// TODO: delete when ResourceTreeAdapter is removed
if (parent.children) {
item.parent = parent;
parent.children.push(item);
}
return item;
});
}
private async checkIfFilepathExists(filepath: string): Promise<boolean> {
const parentDirPath = NotebookUtil.getParentPath(filepath);
if (parentDirPath) {
const items = await this.fetchNotebookFiles(parentDirPath);
return items.some((value) => FileSystemUtil.isPathEqual(value.path, filepath));
}
return false;
}
/**
*
* @param sourcePath
* @param targetName is not prefixed with path
*/
public renameNotebook(
item: NotebookContentItem,
targetName: string,
isGithubTree?: boolean,
): Promise<NotebookContentItem> {
const sourcePath = item.path;
// Match extension
if (sourcePath.indexOf(".") !== -1) {
const extension = `.${sourcePath.split(".").pop()}`;
if (!StringUtils.endsWith(targetName, extension)) {
targetName += extension;
}
}
const targetPath = NotebookUtil.replaceName(sourcePath, targetName);
return this.contentProvider
.update<"file" | "notebook" | "directory">(this.getServerConfig(), sourcePath, { path: targetPath })
.toPromise()
.then((xhr) => {
if (typeof xhr.response === "string") {
throw new Error(`jupyter server response invalid: ${xhr.response}`);
}
if (xhr.response.type !== "file" && xhr.response.type !== "notebook" && xhr.response.type !== "directory") {
throw new Error(`jupyter server response not for notebook/file/directory: ${xhr.response}`);
}
const notebookFile = xhr.response;
item.name = notebookFile.name;
item.path = notebookFile.path;
item.timestamp = NotebookUtil.getCurrentTimestamp();
useNotebook.getState().updateNotebookItem(item, isGithubTree);
return item;
});
}
/**
*
* @param parent
* @param newDirectoryName basename of the new directory
*/
public async createDirectory(
parent: NotebookContentItem,
newDirectoryName: string,
isGithubTree?: boolean,
): Promise<NotebookContentItem> {
if (parent.type !== NotebookContentItemType.Directory) {
throw new Error(`Parent is not a directory: ${parent.path}`);
}
const targetPath = `${parent.path}/${newDirectoryName}`;
// Reject if already exists
if (await this.checkIfFilepathExists(targetPath)) {
throw new Error(`Directory already exists: ${targetPath}`);
}
const type = "directory";
return this.contentProvider
.save<"directory">(this.getServerConfig(), targetPath, { type, path: targetPath })
.toPromise()
.then((xhr: AjaxResponse) => {
if (typeof xhr.response === "string") {
throw new Error(`jupyter server response invalid: ${xhr.response}`);
}
if (xhr.response.type !== type) {
throw new Error(`jupyter server response not for creating directory: ${xhr.response}`);
}
const dir = xhr.response;
const item = NotebookUtil.createNotebookContentItem(dir.name, dir.path, dir.type);
useNotebook.getState().insertNotebookItem(parent, cloneDeep(item), isGithubTree);
// TODO: delete when ResourceTreeAdapter is removed
item.parent = parent;
parent.children?.push(item);
return item;
});
}
public async readFileContent(filePath: string): Promise<string> {
const xhr = await this.contentProvider.get(this.getServerConfig(), filePath, { content: 1 }).toPromise();
const content = (xhr.response as any).content;
if (!content) {
throw new Error("No content read");
}
const format = (xhr.response as any).format;
switch (format) {
case "text":
return content;
case "base64":
return atob(content);
case "json":
return stringifyNotebook(content);
default:
throw new Error(`Unsupported content format ${format}`);
}
}
private deleteNotebookFile(path: string): Promise<string> {
return this.contentProvider
.remove(this.getServerConfig(), path)
.toPromise()
.then(() => path);
}
/**
* Convert rx-jupyter type to our type
* @param type
*/
public static getType(type: FileType): NotebookContentItemType {
switch (type) {
case "directory":
return NotebookContentItemType.Directory;
case "notebook":
return NotebookContentItemType.Notebook;
case "file":
return NotebookContentItemType.File;
default:
throw new Error(`Unknown file type: ${type}`);
}
}
private fetchNotebookFiles(path: string): Promise<NotebookContentItem[]> {
return this.contentProvider
.get(this.getServerConfig(), path, {
type: "directory",
})
.toPromise()
.then((xhr) => {
if (xhr.status !== 200) {
throw new Error(JSON.stringify(xhr.response));
}
if (typeof xhr.response === "string") {
throw new Error(`jupyter server response invalid: ${xhr.response}`);
}
if (xhr.response.type !== "directory") {
throw new Error(`jupyter server response not for directory: ${xhr.response}`);
}
const list = xhr.response.content as IEmptyContent<FileType>[];
return list.map(
(item: IEmptyContent<FileType>): NotebookContentItem => ({
name: item.name,
path: item.path,
type: NotebookUtil.getType(item.type),
}),
);
});
}
private getServerConfig(): ServerConfig {
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
return {
endpoint: notebookServerInfo?.notebookServerEndpoint,
token: notebookServerInfo?.authToken,
crossDomain: true,
};
}
}
@@ -1,14 +0,0 @@
export interface NotebookContentItem {
name: string;
path: string;
type: NotebookContentItemType;
children?: NotebookContentItem[];
parent?: NotebookContentItem;
timestamp?: number;
}
export enum NotebookContentItemType {
Notebook,
File,
Directory,
}
-202
View File
@@ -1,202 +0,0 @@
/*
* Contains all notebook related stuff meant to be dynamically loaded by explorer
*/
import { ImmutableNotebook } from "@nteract/commutable";
import type { IContentProvider } from "@nteract/core";
import React from "react";
import { contents } from "rx-jupyter";
import { Areas, HttpStatusCodes } from "../../Common/Constants";
import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
import * as Logger from "../../Common/Logger";
import { GitHubClient } from "../../GitHub/GitHubClient";
import { GitHubContentProvider } from "../../GitHub/GitHubContentProvider";
import { GitHubOAuthService } from "../../GitHub/GitHubOAuthService";
import { useSidePanel } from "../../hooks/useSidePanel";
import { JunoClient } from "../../Juno/JunoClient";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext";
import { useDialog } from "../Controls/Dialog";
import Explorer from "../Explorer";
import { CopyNotebookPane } from "../Panes/CopyNotebookPane/CopyNotebookPane";
import { GitHubReposPanel } from "../Panes/GitHubReposPanel/GitHubReposPanel";
import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter";
import { InMemoryContentProvider } from "./NotebookComponent/ContentProviders/InMemoryContentProvider";
import { NotebookContentProvider } from "./NotebookComponent/NotebookContentProvider";
import { NotebookContainerClient } from "./NotebookContainerClient";
import { NotebookContentClient } from "./NotebookContentClient";
import { SchemaAnalyzerNotebook } from "./SchemaAnalyzer/SchemaAnalyzerUtils";
import { useNotebook } from "./useNotebook";
type NotebookPaneContent = string | ImmutableNotebook;
export type { NotebookPaneContent };
export interface NotebookManagerOptions {
container: Explorer;
resourceTree: ResourceTreeAdapter;
refreshCommandBarButtons: () => void;
refreshNotebookList: () => void;
}
export default class NotebookManager {
private params: NotebookManagerOptions;
public junoClient: JunoClient;
public notebookContentProvider: IContentProvider;
public notebookClient: NotebookContainerClient;
public notebookContentClient: NotebookContentClient;
private inMemoryContentProvider: InMemoryContentProvider;
private gitHubContentProvider: GitHubContentProvider;
public gitHubOAuthService: GitHubOAuthService;
public gitHubClient: GitHubClient;
public initialize(params: NotebookManagerOptions): void {
this.params = params;
this.junoClient = new JunoClient();
this.gitHubOAuthService = new GitHubOAuthService(this.junoClient);
this.gitHubClient = new GitHubClient(this.onGitHubClientError);
this.inMemoryContentProvider = new InMemoryContentProvider({
[SchemaAnalyzerNotebook.path]: {
readonly: true,
content: SchemaAnalyzerNotebook,
},
});
this.gitHubContentProvider = new GitHubContentProvider({
gitHubClient: this.gitHubClient,
promptForCommitMsg: this.promptForCommitMsg,
});
this.notebookContentProvider = new NotebookContentProvider(
this.inMemoryContentProvider,
this.gitHubContentProvider,
contents.JupyterContentProvider,
);
this.notebookClient = new NotebookContainerClient(() =>
this.params.container.initNotebooks(userContext?.databaseAccount),
);
this.notebookContentClient = new NotebookContentClient(this.notebookContentProvider);
this.gitHubOAuthService.getTokenObservable().subscribe((token) => {
this.gitHubClient.setToken(token?.access_token);
if (this?.gitHubOAuthService.isLoggedIn()) {
useSidePanel.getState().closeSidePanel();
setTimeout(() => {
useSidePanel
.getState()
.openSidePanel(
"Manage GitHub settings",
<GitHubReposPanel
explorer={this.params.container}
gitHubClientProp={this.params.container.notebookManager.gitHubClient}
junoClientProp={this.junoClient}
/>,
);
}, 200);
}
this.params.refreshCommandBarButtons();
this.params.refreshNotebookList();
});
this.junoClient.subscribeToPinnedRepos((pinnedRepos) => {
this.params.resourceTree.initializeGitHubRepos(pinnedRepos);
this.params.resourceTree.triggerRender();
useNotebook.getState().initializeGitHubRepos(pinnedRepos);
});
this.refreshPinnedRepos();
}
public refreshPinnedRepos(): void {
const token = this.gitHubOAuthService.getTokenObservable()();
if (token) {
this.junoClient.getPinnedRepos(token.scope);
}
}
public openCopyNotebookPane(name: string, content: string): void {
const { container } = this.params;
useSidePanel
.getState()
.openSidePanel(
"Copy Notebook",
<CopyNotebookPane
container={container}
junoClient={this.junoClient}
gitHubOAuthService={this.gitHubOAuthService}
name={name}
content={content}
/>,
);
}
// Octokit's error handler uses any
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private onGitHubClientError = (error: any): void => {
Logger.logError(getErrorMessage(error), "NotebookManager/onGitHubClientError");
if (error.status === HttpStatusCodes.Unauthorized) {
this.gitHubOAuthService.resetToken();
useDialog
.getState()
.showOkCancelModalDialog(
undefined,
"Cosmos DB cannot access your Github account anymore. Please connect to GitHub again.",
"Connect to GitHub",
() =>
useSidePanel
.getState()
.openSidePanel(
"Connect to GitHub",
<GitHubReposPanel
explorer={this.params.container}
gitHubClientProp={this.params.container.notebookManager.gitHubClient}
junoClientProp={this.junoClient}
/>,
),
"Cancel",
undefined,
);
}
};
private promptForCommitMsg = (title: string, primaryButtonLabel: string) => {
return new Promise<string>((resolve, reject) => {
let commitMsg = "Committed from Azure Cosmos DB Notebooks";
useDialog.getState().showOkCancelModalDialog(
title || "Commit",
undefined,
primaryButtonLabel || "Commit",
() => {
TelemetryProcessor.trace(Action.NotebooksGitHubCommit, ActionModifiers.Mark, {
dataExplorerArea: Areas.Notebook,
});
resolve(commitMsg);
},
"Cancel",
() => reject(new Error("Commit dialog canceled")),
undefined,
undefined,
{
label: "Commit message",
autoAdjustHeight: true,
multiline: true,
defaultValue: commitMsg,
rows: 3,
onChange: (_: unknown, newValue: string) => {
commitMsg = newValue;
},
},
!commitMsg,
);
});
};
}
@@ -1,115 +0,0 @@
import { createGlobalStyle } from "styled-components";
const AzureTheme = createGlobalStyle`
:root {
/* --theme-primary-bg-hover: #0078d4;
--theme-primary-bg-focus: #0078d4;
--theme-primary-shadow-hover: #0078d4; */
--theme-app-bg: white;
--theme-app-fg: var(--nt-color-midnight);
--theme-app-border: var(--nt-color-grey-light);
--theme-primary-bg: var(--nt-color-grey-lightest);
--theme-primary-bg-hover: var(--nt-color-grey-lighter);
--theme-primary-bg-focus: var(--nt-color-grey-light);
--theme-primary-fg: var(--nt-color-midnight-light);
--theme-primary-fg-hover: var(--nt-color-midnight);
--theme-primary-fg-focus: var(--theme-app-fg);
--theme-secondary-bg: var(--theme-primary-bg);
--theme-secondary-bg-hover: var(--theme-primary-bg-hover);
--theme-secondary-bg-focus: var(--theme-primary-bg-focus);
--theme-secondary-fg: var(--nt-color-midnight-lighter);
--theme-secondary-fg-hover: var(--nt-color-midnight-light);
--theme-secondary-fg-focus: var(--theme-primary-fg);
/* --theme-primary-shadow-hover: 0px 2px 4px rgba(0, 0, 0, 0.1);
--theme-primary-shadow-focus: 0px 2px 4px rgba(0, 0, 0, 0.1); */
--theme-title-bar-bg: var(--theme-primary-bg-hover);
--theme-menu-bg: var(--theme-primary-bg);
--theme-menu-bg-hover: var(--theme-primary-bg-hover);
--theme-menu-bg-focus: var(--theme-primary-bg-focus);
/* --theme-menu-shadow: var(--theme-primary-shadow-hover); */
--theme-menu-fg: var(--theme-app-fg);
--theme-menu-fg-hover: var(--theme-app-fg);
--theme-menu-fg-focus: var(--theme-app-fg);
--theme-cell-bg: var(--theme-app-bg);
/* --theme-cell-shadow-hover: var(--theme-primary-shadow-hover); */
/* --theme-cell-shadow-focus: var(--theme-primary-shadow-focus); */
--theme-cell-prompt-bg: var(--theme-primary-bg);
--theme-cell-prompt-bg-hover: var(--theme-primary-bg-hover);
--theme-cell-prompt-bg-focus: var(--theme-primary-bg-focus);
--theme-cell-prompt-fg: var(--theme-secondary-fg);
--theme-cell-prompt-fg-hover: var(--theme-secondary-fg-hover);
--theme-cell-prompt-fg-focus: var(--theme-secondary-fg-focus);
--theme-cell-toolbar-bg: var(--theme-primary-bg);
--theme-cell-toolbar-bg-hover: var(--theme-primary-bg-hover);
--theme-cell-toolbar-bg-focus: var(--theme-primary-bg-focus);
--theme-cell-toolbar-fg: var(--theme-secondary-fg);
--theme-cell-toolbar-fg-hover: var(--theme-secondary-fg-hover);
--theme-cell-toolbar-fg-focus: var(--theme-secondary-fg-focus);
--theme-cell-menu-bg: var(--theme-primary-bg);
--theme-cell-menu-bg-hover: var(--theme-primary-bg-hover);
--theme-cell-menu-bg-focus: var(--theme-primary-bg-focus);
--theme-cell-menu-fg: var(--theme-primary-fg);
--theme-cell-menu-fg-hover: var(--theme-primary-fg-hover);
--theme-cell-menu-fg-focus: var(--theme-primary-fg-focus);
--theme-cell-input-bg: var(--theme-secondary-bg);
--theme-cell-input-fg: var(--theme-app-fg);
--theme-cell-output-bg: var(--theme-app-bg);
--theme-cell-output-fg: var(--theme-primary-fg);
--theme-cell-creator-bg: var(--theme-app-bg);
--theme-cell-creator-fg: var(--theme-secondary-fg);
--theme-cell-creator-fg-hover: var(--theme-secondary-fg-hover);
--theme-cell-creator-fg-focus: var(--theme-secondary-fg-focus);
--theme-pager-bg: #fafafa;
--cm-background: #fafafa;
--cm-color: black;
--cm-gutter-bg: white;
--cm-comment: #a86;
--cm-keyword: blue;
--cm-string: #a22;
--cm-builtin: #077;
--cm-special: #0aa;
--cm-variable: black;
--cm-number: #3a3;
--cm-meta: #555;
--cm-link: #3a3;
--cm-operator: black;
--cm-def: black;
--cm-activeline-bg: #e8f2ff;
--cm-matchingbracket-outline: grey;
--cm-matchingbracket-color: black;
--cm-hint-color: var(--cm-color);
--cm-hint-color-active: var(--cm-color);
--cm-hint-bg: var(--theme-app-bg);
--cm-hint-bg-active: #abd1ff;
--status-bar: #eeedee;
}
`;
export { AzureTheme };
@@ -1,68 +0,0 @@
.NotebookReadOnlyRender {
.nteract-cell-container {
margin-bottom: 10px;
}
.nteract-cell {
padding: 0.5px;
border: 1px solid #ffffff;
border-left: 3px solid #ffffff;
}
.CodeMirror-scroll {
overflow: hidden !important;
}
.CodeMirror-lines {
cursor: default;
}
.CodeMirror {
height: inherit;
}
.CodeMirror-scroll,
.CodeMirror-linenumber,
.CodeMirror-gutters {
background-color: #f5f5f5;
}
.nteract-cell:hover {
border: 1px solid #0078d4;
border-left: 3px solid #0078d4;
.CodeMirror-scroll,
.CodeMirror-linenumber,
.CodeMirror-gutters {
background-color: #ffffff;
}
.nteract-cell-outputs {
border-top: 1px solid #d7d7d7;
}
.nteract-md-cell {
background-color: #ffffff;
}
}
.nteract-cell-outputs {
padding: 10px;
border-top: 1px solid #ffffff;
pre {
background-color: #ffffff;
border: none;
padding: 0px;
margin: 0px;
}
}
.nteract-md-cell {
background-color: #f5f5f5;
}
.nteract-cell:hover.nteract-md-cell {
background-color: #ffffff;
}
}
@@ -1,119 +0,0 @@
import { actions, ContentRef } from "@nteract/core";
import { Cells, CodeCell, RawCell } from "@nteract/stateful-components";
import CodeMirrorEditor from "@nteract/stateful-components/lib/inputs/connected-editors/codemirror";
import { PassedEditorProps } from "@nteract/stateful-components/lib/inputs/editor";
import Prompt, { PassedPromptProps } from "@nteract/stateful-components/lib/inputs/prompt";
import * as React from "react";
import { connect } from "react-redux";
import { Dispatch } from "redux";
import { userContext } from "../../../UserContext";
import loadTransform from "../NotebookComponent/loadTransform";
import { AzureTheme } from "./AzureTheme";
import "./base.css";
import "./default.css";
import MarkdownCell from "./markdown-cell";
import "./NotebookReadOnlyRenderer.less";
import SandboxOutputs from "./outputs/SandboxOutputs";
export interface NotebookRendererProps {
contentRef: ContentRef;
hideInputs?: boolean;
hidePrompts?: boolean;
addTransform: (component: React.ComponentType & { MIMETYPE: string }) => void;
}
/**
* This is the class that uses nteract to render a read-only notebook.
*/
class NotebookReadOnlyRenderer extends React.Component<NotebookRendererProps> {
componentDidMount() {
if (!userContext.features.sandboxNotebookOutputs) {
loadTransform(this.props as NotebookRendererProps);
}
}
private renderPrompt(id: string, contentRef: string): JSX.Element {
if (this.props.hidePrompts) {
return <></>;
}
return (
<Prompt id={id} contentRef={contentRef}>
{(props: PassedPromptProps) => {
if (props.status === "busy") {
return <React.Fragment>{"[*]"}</React.Fragment>;
}
if (props.status === "queued") {
return <React.Fragment>{"[…]"}</React.Fragment>;
}
if (typeof props.executionCount === "number") {
return <React.Fragment>{`[${props.executionCount}]`}</React.Fragment>;
}
return <React.Fragment>{"[ ]"}</React.Fragment>;
}}
</Prompt>
);
}
render(): JSX.Element {
return (
<div className="NotebookReadOnlyRender">
<Cells contentRef={this.props.contentRef}>
{{
code: ({ id, contentRef }: { id: string; contentRef: ContentRef }) => (
<CodeCell id={id} contentRef={contentRef}>
{{
prompt: (props: { id: string; contentRef: string }) => this.renderPrompt(props.id, props.contentRef),
outputs: userContext.features.sandboxNotebookOutputs
? () => <SandboxOutputs id={id} contentRef={contentRef} />
: undefined,
editor: {
codemirror: (props: PassedEditorProps) =>
this.props.hideInputs ? <></> : <CodeMirrorEditor {...props} editorType="codemirror" />,
},
}}
</CodeCell>
),
markdown: ({ id, contentRef }: { id: string; contentRef: ContentRef }) => (
<MarkdownCell id={id} contentRef={contentRef} cell_type="markdown">
{{
editor: {},
}}
</MarkdownCell>
),
raw: ({ id, contentRef }: { id: string; contentRef: ContentRef }) => (
<RawCell id={id} contentRef={contentRef} cell_type="raw">
{{
editor: {
codemirror: (props: PassedEditorProps) =>
this.props.hideInputs ? <></> : <CodeMirrorEditor {...props} editorType="codemirror" />,
},
}}
</RawCell>
),
}}
</Cells>
<AzureTheme />
</div>
);
}
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const makeMapDispatchToProps = (initialDispatch: Dispatch, initialProps: NotebookRendererProps) => {
const mapDispatchToProps = (dispatch: Dispatch) => {
return {
addTransform: (transform: React.ComponentType & { MIMETYPE: string }) => {
return dispatch(
actions.addTransform({
mediaType: transform.MIMETYPE,
component: transform,
}),
);
},
};
};
return mapDispatchToProps;
};
export default connect(undefined, makeMapDispatchToProps)(NotebookReadOnlyRenderer);
@@ -1,124 +0,0 @@
// CommandBar
@HoverColor: #d7d7d7;
@HighlightColor: #0078d4;
.NotebookRendererContainer {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
}
.NotebookRenderer {
overflow: auto;
flex-grow: 1;
.nteract-cells {
padding-top: 0px;
}
.nteract-cell-container {
margin-bottom: 10px;
.nteract-cell {
padding: 0.5px;
border: 1px solid #ffffff;
border-left: 3px solid #ffffff;
.CellContextMenuButton {
position: sticky;
z-index: 1;
top: 0px;
right: 0px;
margin: 0px 0px 0px -100%;
float: right;
visibility: hidden;
}
}
.CodeMirror-scroll {
overflow: hidden !important;
}
.CodeMirror-scroll,
.CodeMirror-linenumber,
.CodeMirror-gutters {
background-color: #f5f5f5;
}
.CodeMirror {
height: inherit;
}
.nteract-cell:hover {
border: 1px solid @HoverColor;
border-left: 3px solid @HoverColor;
.CellContextMenuButton {
visibility: visible;
}
}
}
.nteract-cell-container.selected {
.nteract-cell {
border: 1px solid @HighlightColor;
border-left: 3px solid @HighlightColor;
}
}
// White background when hovered or selected
.nteract-cell:hover,
.nteract-cell-container.selected .nteract-cell {
.CodeMirror-scroll,
.CodeMirror-linenumber,
.CodeMirror-gutters {
background-color: #ffffff;
}
.CodeMirror-linenumber {
color: #015cda;
}
.nteract-cell-outputs {
border-top: 1px solid @HoverColor;
}
.nteract-md-cell {
background-color: #ffffff;
}
}
.nteract-cell-outputs {
padding: 10px;
border-top: 1px solid #ffffff;
pre {
background-color: #ffffff;
border: none;
padding: 0px;
margin: 0px;
}
}
.nteract-md-cell {
background-color: #f5f5f5;
}
.nteract-cell:hover.nteract-md-cell {
background-color: #ffffff;
}
.nteract-md-cell .ntreact-cell-source {
width: 100%;
}
}
// Undo tree.less
.expanded::before {
content: "";
}
.monaco-editor .monaco-list .main {
background-color: transparent;
}
@@ -1,223 +0,0 @@
import { CellId } from "@nteract/commutable";
import { CellType } from "@nteract/commutable/src";
import { actions, ContentRef, selectors } from "@nteract/core";
import { Cells, CodeCell, RawCell } from "@nteract/stateful-components";
import CodeMirrorEditor from "@nteract/stateful-components/lib/inputs/connected-editors/codemirror";
import { PassedEditorProps } from "@nteract/stateful-components/lib/inputs/editor";
import * as React from "react";
import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
import { connect } from "react-redux";
import { Dispatch } from "redux";
import { userContext } from "../../../UserContext";
import * as cdbActions from "../NotebookComponent/actions";
import loadTransform from "../NotebookComponent/loadTransform";
import { CdbAppState, SnapshotFragment, SnapshotRequest } from "../NotebookComponent/types";
import { NotebookUtil } from "../NotebookUtil";
import SecurityWarningBar from "../SecurityWarningBar/SecurityWarningBar";
import { AzureTheme } from "./AzureTheme";
import "./base.css";
import CellCreator from "./decorators/CellCreator";
import CellLabeler from "./decorators/CellLabeler";
import HoverableCell from "./decorators/HoverableCell";
import KeyboardShortcuts from "./decorators/kbd-shortcuts";
import "./default.css";
import MarkdownCell from "./markdown-cell";
import "./NotebookRenderer.less";
import SandboxOutputs from "./outputs/SandboxOutputs";
import Prompt from "./Prompt";
import { promptContent } from "./PromptContent";
import StatusBar from "./StatusBar";
import CellToolbar from "./Toolbar";
export interface NotebookRendererBaseProps {
contentRef: any;
}
interface NotebookRendererDispatchProps {
storeNotebookSnapshot: (imageSrc: string, requestId: string) => void;
notebookSnapshotError: (error: string) => void;
}
interface StateProps {
pendingSnapshotRequest: SnapshotRequest;
cellOutputSnapshots: Map<string, SnapshotFragment>;
notebookSnapshot: { imageSrc: string; requestId: string };
nbCodeCells: number;
}
type NotebookRendererProps = NotebookRendererBaseProps & NotebookRendererDispatchProps & StateProps;
const decorate = (id: string, contentRef: ContentRef, cell_type: CellType, children: React.ReactNode) => {
const Cell = () => (
// TODO Draggable and HijackScroll not working anymore. Fix or remove when reworking MarkdownCell.
// <DraggableCell id={id} contentRef={contentRef}>
// <HijackScroll id={id} contentRef={contentRef}>
<CellCreator id={id} contentRef={contentRef}>
<CellLabeler id={id} contentRef={contentRef}>
<HoverableCell id={id} contentRef={contentRef}>
{children}
</HoverableCell>
</CellLabeler>
</CellCreator>
// </HijackScroll>
// </DraggableCell>
);
Cell.defaultProps = { cell_type };
return <Cell />;
};
class BaseNotebookRenderer extends React.Component<NotebookRendererProps> {
private notebookRendererRef = React.createRef<HTMLDivElement>();
componentDidMount() {
if (!userContext.features.sandboxNotebookOutputs) {
loadTransform(this.props as any);
}
}
async componentDidUpdate(): Promise<void> {
// Take a snapshot if there's a pending request and all the outputs are also saved
if (
this.props.pendingSnapshotRequest &&
this.props.pendingSnapshotRequest.type === "notebook" &&
this.props.pendingSnapshotRequest.notebookContentRef === this.props.contentRef &&
(!this.props.notebookSnapshot ||
this.props.pendingSnapshotRequest.requestId !== this.props.notebookSnapshot.requestId) &&
this.props.cellOutputSnapshots.size === this.props.nbCodeCells
) {
try {
// Use Html2Canvas because it is much more reliable and fast than dom-to-file
const result = await NotebookUtil.takeScreenshotHtml2Canvas(
this.notebookRendererRef.current,
this.props.pendingSnapshotRequest.aspectRatio,
[...this.props.cellOutputSnapshots.values()],
this.props.pendingSnapshotRequest.downloadFilename,
);
this.props.storeNotebookSnapshot(result.imageSrc, this.props.pendingSnapshotRequest.requestId);
} catch (error) {
this.props.notebookSnapshotError(error.message);
} finally {
this.setState({ processedSnapshotRequest: undefined });
}
}
}
render(): JSX.Element {
return (
<>
<div className="NotebookRendererContainer">
<SecurityWarningBar contentRef={this.props.contentRef} />
<div className="NotebookRenderer" ref={this.notebookRendererRef}>
<DndProvider backend={HTML5Backend}>
<KeyboardShortcuts contentRef={this.props.contentRef}>
<Cells contentRef={this.props.contentRef}>
{{
code: ({ id, contentRef }: { id: CellId; contentRef: ContentRef }) =>
decorate(
id,
contentRef,
"code",
<CodeCell id={id} contentRef={contentRef} cell_type="code">
{{
editor: {
codemirror: (props: PassedEditorProps) => (
<CodeMirrorEditor {...props} editorType="codemirror" />
),
},
prompt: ({ id, contentRef }: { id: CellId; contentRef: ContentRef }) => (
<Prompt id={id} contentRef={contentRef} isHovered={false}>
{promptContent}
</Prompt>
),
toolbar: () => <CellToolbar id={id} contentRef={contentRef} />,
outputs: userContext.features.sandboxNotebookOutputs
? () => <SandboxOutputs id={id} contentRef={contentRef} />
: undefined,
}}
</CodeCell>,
),
markdown: ({ id, contentRef }: { id: any; contentRef: ContentRef }) =>
decorate(
id,
contentRef,
"markdown",
<MarkdownCell id={id} contentRef={contentRef} cell_type="markdown">
{{
editor: {
codemirror: (props: PassedEditorProps) => (
<CodeMirrorEditor {...props} editorType="codemirror" />
),
},
toolbar: () => <CellToolbar id={id} contentRef={contentRef} />,
}}
</MarkdownCell>,
),
raw: ({ id, contentRef }: { id: any; contentRef: ContentRef }) =>
decorate(
id,
contentRef,
"raw",
<RawCell id={id} contentRef={contentRef} cell_type="raw">
{{
editor: {
codemirror: (props: PassedEditorProps) => (
<CodeMirrorEditor {...props} editorType="codemirror" />
),
},
toolbar: () => <CellToolbar id={id} contentRef={contentRef} />,
}}
</RawCell>,
),
}}
</Cells>
</KeyboardShortcuts>
<AzureTheme />
</DndProvider>
</div>
<StatusBar contentRef={this.props.contentRef} />
</div>
</>
);
}
}
export const makeMapStateToProps = (
initialState: CdbAppState,
ownProps: NotebookRendererProps,
): ((state: CdbAppState) => StateProps) => {
const mapStateToProps = (state: CdbAppState): StateProps => {
const { contentRef } = ownProps;
const model = selectors.model(state, { contentRef });
let nbCodeCells;
if (model && model.type === "notebook") {
nbCodeCells = NotebookUtil.findCodeCellWithDisplay(model.notebook).length;
}
const { pendingSnapshotRequest, cellOutputSnapshots, notebookSnapshot } = state.cdb;
return { pendingSnapshotRequest, cellOutputSnapshots, notebookSnapshot, nbCodeCells };
};
return mapStateToProps;
};
const makeMapDispatchToProps = (initialDispatch: Dispatch, initialProps: NotebookRendererBaseProps) => {
const mapDispatchToProps = (dispatch: Dispatch) => {
return {
addTransform: (transform: React.ComponentType & { MIMETYPE: string }) =>
dispatch(
actions.addTransform({
mediaType: transform.MIMETYPE,
component: transform,
}),
),
storeNotebookSnapshot: (imageSrc: string, requestId: string) =>
dispatch(cdbActions.storeNotebookSnapshot({ imageSrc, requestId })),
notebookSnapshotError: (error: string) => dispatch(cdbActions.notebookSnapshotError({ error })),
};
};
return mapDispatchToProps;
};
export default connect(makeMapStateToProps, makeMapDispatchToProps)(BaseNotebookRenderer);

Some files were not shown because too many files have changed in this diff Show More