mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-08-30 02:08:40 +01:00
Merge branch 'master' into locfiles/486ca1c2-3f14-4217-8aae-5aae878d249c
This commit is contained in:
+53
-123
@@ -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: |
|
||||
|
||||
@@ -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
|
||||
@@ -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`
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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",
|
||||
|
||||
Generated
+345
-2707
File diff suppressed because it is too large
Load Diff
+19
-38
@@ -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",
|
||||
|
||||
@@ -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
@@ -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");
|
||||
|
||||
Generated
+7
-32
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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 +0,0 @@
|
||||
export { Vega2 as default } from "@nteract/transform-vega";
|
||||
@@ -1 +0,0 @@
|
||||
export { Vega3 as default } from "@nteract/transform-vega";
|
||||
@@ -1 +0,0 @@
|
||||
export { Vega4 as default } from "@nteract/transform-vega";
|
||||
@@ -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";
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
-197
@@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
+1
-1
@@ -44,7 +44,7 @@ describe("ThroughputInputAutoPilotV3Component", () => {
|
||||
},
|
||||
instantMaximumThroughput: 5000,
|
||||
softAllowedMaximumThroughput: 1000000,
|
||||
isGlobalSecondaryIndex: false,
|
||||
isGlobalSecondaryIndexTarget: false,
|
||||
};
|
||||
|
||||
it("throughput input visible", () => {
|
||||
|
||||
+2
-2
@@ -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 = {
|
||||
const labelStyles = (props: ILabelStyleProps) => {
|
||||
return {
|
||||
root: {
|
||||
fontSize: 12,
|
||||
color: "var(--colorNeutralForeground1)",
|
||||
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,
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
+11
-582
@@ -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) {
|
||||
public openNotebookTerminal(kind: ViewModels.TerminalKind): void {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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,11 +35,11 @@ const migrateOldData = () => {
|
||||
componentName: AppStateComponentNames.MostRecentActivity,
|
||||
globalAccountName: accountName,
|
||||
},
|
||||
itemsMap[accountId].map((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;
|
||||
} else if ((item.type as unknown as number) === 1) {
|
||||
item.type = Type.OpenNotebook;
|
||||
}
|
||||
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 = (
|
||||
|
||||
@@ -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(".");
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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",
|
||||
};
|
||||
}
|
||||
}
|
||||
-15
@@ -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,
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user