mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-06-29 18:19:40 +01:00
Merge branch 'master' into quantized_vector_playwright
This commit is contained in:
+1
-1
@@ -40,7 +40,7 @@ module.exports = {
|
||||
},
|
||||
],
|
||||
rules: {
|
||||
//CTODO uncomment when console debugging is reverted: "no-console": ["error", { allow: ["error", "warn", "dir"] }],
|
||||
"no-console": ["error", { allow: ["error", "warn", "dir"] }],
|
||||
curly: "error",
|
||||
"@typescript-eslint/switch-exhaustiveness-check": "error",
|
||||
"@typescript-eslint/no-unused-vars": "error",
|
||||
|
||||
+73
-52
@@ -299,13 +299,26 @@ jobs:
|
||||
run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --list
|
||||
- name: Run test shard ${{ matrix['shardIndex'] }} of ${{ matrix['shardTotal']}}
|
||||
run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --workers=3
|
||||
- name: Upload blob report to GitHub Actions Artifacts
|
||||
- name: "Re-auth for upload (refresh OIDC token)"
|
||||
if: ${{ !cancelled() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: Azure/login@v2
|
||||
with:
|
||||
name: blob-report-${{ matrix.shardIndex }}
|
||||
path: blob-report
|
||||
retention-days: 1
|
||||
client-id: ${{ secrets.E2E_TESTS_CLIENT_ID }}
|
||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
- name: Upload shard blob-report to Azure Storage
|
||||
if: ${{ !cancelled() }}
|
||||
env:
|
||||
KEY: ${{ github.run_id }}-${{ github.run_attempt }}
|
||||
SHARD: ${{ matrix.shardIndex }}
|
||||
run: |
|
||||
az storage blob upload-batch \
|
||||
--account-name ${{ secrets.PREVIEW_STORAGE_ACCOUNT_NAME }} \
|
||||
--auth-mode login \
|
||||
-d playwright-reports \
|
||||
-s blob-report \
|
||||
--destination-path "${KEY}/shards/shard-${SHARD}" \
|
||||
--overwrite true
|
||||
|
||||
merge-playwright-reports:
|
||||
name: "Merge Playwright Reports"
|
||||
@@ -317,6 +330,7 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
@@ -325,29 +339,67 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Download blob reports from GitHub Actions Artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
- name: "Az CLI login"
|
||||
uses: Azure/login@v2
|
||||
with:
|
||||
path: all-blob-reports
|
||||
pattern: blob-report-*
|
||||
merge-multiple: true
|
||||
client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
subscription-id: ${{ secrets.PREVIEW_SUBSCRIPTION_ID }}
|
||||
|
||||
- name: Download all shard reports from Azure Storage
|
||||
env:
|
||||
KEY: ${{ github.run_id }}-${{ github.run_attempt }}
|
||||
run: |
|
||||
mkdir -p all-blob-reports
|
||||
az storage blob download-batch \
|
||||
--account-name ${{ secrets.PREVIEW_STORAGE_ACCOUNT_NAME }} \
|
||||
--auth-mode login \
|
||||
-s playwright-reports \
|
||||
--pattern "${KEY}/shards/*" \
|
||||
-d ./all-blob-reports
|
||||
find ./all-blob-reports -type f -name "*.zip" -exec mv -t ./all-blob-reports {} +
|
||||
find ./all-blob-reports -type d -empty -delete
|
||||
ls -la ./all-blob-reports
|
||||
|
||||
- name: Merge into HTML Report
|
||||
run: npx playwright merge-reports --reporter html ./all-blob-reports
|
||||
|
||||
- name: Upload HTML report
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: html-report--attempt-${{ github.run_attempt }}
|
||||
path: playwright-report
|
||||
retention-days: 14
|
||||
- name: Bundle merged report into a single zip
|
||||
run: |
|
||||
cd playwright-report
|
||||
zip -r ../report.zip .
|
||||
cd ..
|
||||
|
||||
- name: Upload report.zip to Azure Storage
|
||||
env:
|
||||
KEY: ${{ github.run_id }}-${{ github.run_attempt }}
|
||||
run: |
|
||||
az storage blob upload \
|
||||
--account-name ${{ secrets.PREVIEW_STORAGE_ACCOUNT_NAME }} \
|
||||
--auth-mode login \
|
||||
-c playwright-reports \
|
||||
-n "${KEY}/report.zip" \
|
||||
-f report.zip \
|
||||
--overwrite
|
||||
|
||||
- name: Clean up shard intermediates from Azure Storage
|
||||
if: ${{ always() }}
|
||||
env:
|
||||
KEY: ${{ github.run_id }}-${{ github.run_attempt }}
|
||||
run: |
|
||||
az storage blob delete-batch \
|
||||
--account-name ${{ secrets.PREVIEW_STORAGE_ACCOUNT_NAME }} \
|
||||
--auth-mode login \
|
||||
-s playwright-reports \
|
||||
--pattern "${KEY}/shards/*"
|
||||
|
||||
- name: Comment Playwright results on PR
|
||||
if: ${{ !cancelled() && github.event_name == 'pull_request' }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PR: ${{ github.event.pull_request.number }}
|
||||
REPORT_URL: https://dataexplorerpreview.z5.web.core.windows.net/playwright-reports/${{ github.run_id }}-${{ github.run_attempt }}/index.html
|
||||
SA_ID: /subscriptions/${{ secrets.PREVIEW_SUBSCRIPTION_ID }}/resourceGroups/dataexplorer-preview/providers/Microsoft.Storage/storageAccounts/dataexplorerpreview
|
||||
KEY: ${{ github.run_id }}-${{ github.run_attempt }}
|
||||
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
run: |
|
||||
PLAYWRIGHT_JSON_OUTPUT_NAME=results.json npx playwright merge-reports --reporter json ./all-blob-reports
|
||||
@@ -355,6 +407,8 @@ jobs:
|
||||
BROKEN=$(gh api "repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/attempts/${{ github.run_attempt }}/jobs" \
|
||||
--jq '[.jobs[] | select(.name | startswith("Run Playwright Tests")) | select(.conclusion == "failure")] | length')
|
||||
if [ "$FAILED" -gt 0 ] || [ "$BROKEN" -gt 0 ]; then ICON="❌ failed"; else ICON="✅ passed"; fi
|
||||
SA_ENC=$(printf '%s' "$SA_ID" | jq -sRr @uri)
|
||||
CONTAINER_URL="https://portal.azure.com/#view/Microsoft_Azure_Storage/ContainerMenuBlade/~/overview/storageAccountId/${SA_ENC}/path/playwright-reports"
|
||||
NOTE=""
|
||||
[ "$BROKEN" -gt 0 ] && NOTE="
|
||||
|
||||
@@ -365,38 +419,5 @@ jobs:
|
||||
| :---: | :---: | :---: | :---: |
|
||||
| $PASSED | $FAILED | $FLAKY | ${DURATION}s |
|
||||
|
||||
📊 [Open full report]($REPORT_URL) · [Workflow run]($RUN_URL)$NOTE"
|
||||
|
||||
publish-playwright-report:
|
||||
name: "Publish Playwright Report to Blob"
|
||||
if: ${{ !cancelled() }}
|
||||
needs: [merge-playwright-reports]
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
steps:
|
||||
- name: Download HTML report artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: html-report--attempt-${{ github.run_attempt }}
|
||||
path: playwright-report
|
||||
|
||||
- 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 }}
|
||||
|
||||
- name: Upload Playwright report to blob storage
|
||||
env:
|
||||
KEY: ${{ github.run_id }}-${{ github.run_attempt }}
|
||||
BASE: https://dataexplorerpreview.z5.web.core.windows.net
|
||||
run: |
|
||||
az storage blob upload-batch -d '$web' -s playwright-report \
|
||||
--destination-path "playwright-reports/${KEY}" \
|
||||
--account-name ${{ secrets.PREVIEW_STORAGE_ACCOUNT_NAME }} \
|
||||
--auth-mode login --overwrite true
|
||||
echo "📊 [Open Playwright report](${BASE}/playwright-reports/${KEY}/index.html)" >> $GITHUB_STEP_SUMMARY
|
||||
📁 **Report:** \`${KEY}/report.zip\`
|
||||
[Open container]($CONTAINER_URL) (Azure sign-in required) → click into \`${KEY}\` folder → click \`report.zip\` → **Download** → unzip → open \`index.html\` · [Workflow run]($RUN_URL)$NOTE"
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
# 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), 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 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 four shells
|
||||
still open.
|
||||
|
||||
### Phase 1 — Decouple database shells to CloudShell-only
|
||||
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
|
||||
Delete the notebook tabs, the nteract rendering engine, panes, and the read-only viewer,
|
||||
and remove all UI entry points that open notebooks.
|
||||
- Delete: `src/Explorer/Notebook/NotebookComponent/`,
|
||||
`src/Explorer/Notebook/NotebookRenderer/`, `src/Explorer/Notebook/SecurityWarningBar/`,
|
||||
`NotebookClientV2.ts`, `notebookClientV2.test.ts`, `NotebookContentClient.ts`,
|
||||
`NTeractUtil.ts`, `NotebookContentItem.ts`, `NotebookUtil.ts` (+ test),
|
||||
`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 plugin).
|
||||
- Remove notebook entry points: "New Notebook"/open-notebook buttons in
|
||||
`CommandBarComponentButtonFactory` (+ test), `OpenActions.tsx`,
|
||||
`ContextMenuButtonFactory.tsx`, splash-screen notebook cards & recent-notebook items
|
||||
(`MostRecentActivity` OpenNotebook type), and the `openNotebook*` /
|
||||
`createNotebookContentItemFile` methods on `Explorer`.
|
||||
- Remove notebook deps from `package.json`: `@nteract/*`, `@jupyterlab/*`,
|
||||
`@phosphor/widgets`, `rx-jupyter` (and any now-unused transitive notebook-only libs).
|
||||
- **Outcome:** Notebooks can no longer be authored, opened, or rendered.
|
||||
|
||||
### Phase 3 — Remove Schema Analyzer
|
||||
- Delete `src/Explorer/Notebook/SchemaAnalyzer/` and `src/Explorer/Tabs/SchemaAnalyzerTab.ts`.
|
||||
- Remove Schema Analyzer command-bar button and any tree/menu entry points.
|
||||
|
||||
### Phase 4 — Remove GitHub integration
|
||||
- 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
|
||||
- Delete `src/Phoenix/`, `src/Explorer/Notebook/NotebookContainerClient.ts`,
|
||||
`src/Explorer/Notebook/NotebookManager.tsx`, `src/Explorer/Notebook/useNotebook.ts`,
|
||||
`src/Utils/NotebookConfigurationUtils.ts`, `src/hooks/useNotebookSnapshotStore.ts`.
|
||||
- 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, and VCoreMongo 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.
|
||||
Generated
+77
-104
@@ -113,7 +113,7 @@
|
||||
"react-youtube": "9.0.1",
|
||||
"reflect-metadata": "0.1.13",
|
||||
"rx-jupyter": "5.5.12",
|
||||
"shell-quote": "1.7.3",
|
||||
"shell-quote": "1.8.4",
|
||||
"styled-components": "5.0.1",
|
||||
"swr": "0.4.0",
|
||||
"terser-webpack-plugin": "5.3.9",
|
||||
@@ -122,7 +122,6 @@
|
||||
"utility-types": "3.10.0",
|
||||
"uuid": "9.0.0",
|
||||
"web-vitals": "4.2.4",
|
||||
"ws": "8.17.1",
|
||||
"zustand": "3.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -163,7 +162,7 @@
|
||||
"@webpack-cli/serve": "2.0.5",
|
||||
"babel-jest": "29.7.0",
|
||||
"babel-loader": "8.1.0",
|
||||
"brace-expansion": "1.1.12",
|
||||
"brace-expansion": "1.1.15",
|
||||
"buffer": "5.1.0",
|
||||
"case-sensitive-paths-webpack-plugin": "2.4.0",
|
||||
"create-file-webpack": "1.0.2",
|
||||
@@ -214,7 +213,7 @@
|
||||
"webpack-bundle-analyzer": "5.2.0",
|
||||
"webpack-cli": "5.1.4",
|
||||
"webpack-dev-server": "5.2.3",
|
||||
"ws": "8.17.1"
|
||||
"ws": "8.20.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@aashutoshrathi/word-wrap": {
|
||||
@@ -11604,10 +11603,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "1.20.4",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
|
||||
"integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==",
|
||||
"version": "1.20.5",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz",
|
||||
"integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "~3.1.2",
|
||||
"content-type": "~1.0.5",
|
||||
@@ -11617,7 +11617,7 @@
|
||||
"http-errors": "~2.0.1",
|
||||
"iconv-lite": "~0.4.24",
|
||||
"on-finished": "~2.4.1",
|
||||
"qs": "~6.14.0",
|
||||
"qs": "~6.15.1",
|
||||
"raw-body": "~2.5.3",
|
||||
"type-is": "~1.6.18",
|
||||
"unpipe": "~1.0.0"
|
||||
@@ -11632,6 +11632,7 @@
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
@@ -11641,6 +11642,7 @@
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3"
|
||||
},
|
||||
@@ -11652,7 +11654,8 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bonjour-service": {
|
||||
"version": "1.3.0",
|
||||
@@ -11679,9 +11682,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"version": "1.1.15",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz",
|
||||
"integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
@@ -12347,6 +12351,7 @@
|
||||
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
|
||||
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
@@ -15224,14 +15229,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "4.22.1",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
|
||||
"integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
|
||||
"version": "4.22.2",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz",
|
||||
"integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.8",
|
||||
"array-flatten": "1.1.1",
|
||||
"body-parser": "~1.20.3",
|
||||
"body-parser": "~1.20.5",
|
||||
"content-disposition": "~0.5.4",
|
||||
"content-type": "~1.0.4",
|
||||
"cookie": "~0.7.1",
|
||||
@@ -15250,7 +15256,7 @@
|
||||
"parseurl": "~1.3.3",
|
||||
"path-to-regexp": "~0.1.12",
|
||||
"proxy-addr": "~2.0.7",
|
||||
"qs": "~6.14.0",
|
||||
"qs": "~6.15.1",
|
||||
"range-parser": "~1.2.1",
|
||||
"safe-buffer": "5.2.1",
|
||||
"send": "~0.19.0",
|
||||
@@ -19459,27 +19465,6 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-environment-jsdom/node_modules/ws": {
|
||||
"version": "8.18.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
|
||||
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/jest-environment-jsdom/node_modules/xml-name-validator": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz",
|
||||
@@ -22317,20 +22302,14 @@
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/launch-editor": {
|
||||
"version": "2.6.1",
|
||||
"version": "2.14.1",
|
||||
"resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.14.1.tgz",
|
||||
"integrity": "sha512-QWBrQsMpH7gPr965dsKD/3cKWiNoTjpATQf++Xq63N6sKRGMwlVXz41O1IZTMfZQgBctD/K5Zt06+/I6pP6+HA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"picocolors": "^1.0.0",
|
||||
"shell-quote": "^1.8.1"
|
||||
}
|
||||
},
|
||||
"node_modules/launch-editor/node_modules/shell-quote": {
|
||||
"version": "1.8.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
"picocolors": "^1.1.1",
|
||||
"shell-quote": "^1.8.4"
|
||||
}
|
||||
},
|
||||
"node_modules/leaflet": {
|
||||
@@ -22762,6 +22741,7 @@
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
@@ -24033,7 +24013,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.4.31",
|
||||
"version": "8.5.15",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
|
||||
"integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -24050,9 +24032,9 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.6",
|
||||
"picocolors": "^1.0.0",
|
||||
"source-map-js": "^1.0.2"
|
||||
"nanoid": "^3.3.12",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || >=14"
|
||||
@@ -24129,6 +24111,24 @@
|
||||
"version": "4.2.0",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/postcss/node_modules/nanoid": {
|
||||
"version": "3.3.12",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
|
||||
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/prebuild-install": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
|
||||
@@ -24498,9 +24498,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.14.1",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
|
||||
"integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==",
|
||||
"version": "6.15.2",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz",
|
||||
"integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"side-channel": "^1.1.0"
|
||||
},
|
||||
@@ -24585,6 +24586,7 @@
|
||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
|
||||
"integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "~3.1.2",
|
||||
"http-errors": "~2.0.1",
|
||||
@@ -24600,6 +24602,7 @@
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3"
|
||||
},
|
||||
@@ -26303,8 +26306,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/shell-quote": {
|
||||
"version": "1.7.3",
|
||||
"license": "MIT"
|
||||
"version": "1.8.4",
|
||||
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.4.tgz",
|
||||
"integrity": "sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/shiki": {
|
||||
"version": "1.12.0",
|
||||
@@ -26524,7 +26535,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.0.2",
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -27399,6 +27412,7 @@
|
||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
|
||||
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"media-typer": "0.3.0",
|
||||
"mime-types": "~2.1.24"
|
||||
@@ -28278,27 +28292,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/webpack-bundle-analyzer/node_modules/ws": {
|
||||
"version": "8.19.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
|
||||
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/webpack-cli": {
|
||||
"version": "5.1.4",
|
||||
"dev": true,
|
||||
@@ -29105,27 +29098,6 @@
|
||||
"url": "https://opencollective.com/webpack"
|
||||
}
|
||||
},
|
||||
"node_modules/webpack-dev-server/node_modules/ws": {
|
||||
"version": "8.19.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
|
||||
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/webpack-merge": {
|
||||
"version": "5.10.0",
|
||||
"dev": true,
|
||||
@@ -29503,10 +29475,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
||||
"version": "8.20.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz",
|
||||
"integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
|
||||
+4
-4
@@ -108,7 +108,7 @@
|
||||
"react-youtube": "9.0.1",
|
||||
"reflect-metadata": "0.1.13",
|
||||
"rx-jupyter": "5.5.12",
|
||||
"shell-quote": "1.7.3",
|
||||
"shell-quote": "1.8.4",
|
||||
"styled-components": "5.0.1",
|
||||
"swr": "0.4.0",
|
||||
"terser-webpack-plugin": "5.3.9",
|
||||
@@ -117,7 +117,7 @@
|
||||
"utility-types": "3.10.0",
|
||||
"uuid": "9.0.0",
|
||||
"web-vitals": "4.2.4",
|
||||
"ws": "8.17.1",
|
||||
"ws": "8.20.1",
|
||||
"zustand": "3.5.0"
|
||||
},
|
||||
"overrides": {
|
||||
@@ -173,7 +173,7 @@
|
||||
"@webpack-cli/serve": "2.0.5",
|
||||
"babel-jest": "29.7.0",
|
||||
"babel-loader": "8.1.0",
|
||||
"brace-expansion": "1.1.12",
|
||||
"brace-expansion": "1.1.15",
|
||||
"buffer": "5.1.0",
|
||||
"case-sensitive-paths-webpack-plugin": "2.4.0",
|
||||
"create-file-webpack": "1.0.2",
|
||||
@@ -224,7 +224,7 @@
|
||||
"webpack-bundle-analyzer": "5.2.0",
|
||||
"webpack-cli": "5.1.4",
|
||||
"webpack-dev-server": "5.2.3",
|
||||
"ws": "8.17.1"
|
||||
"ws": "8.20.1"
|
||||
},
|
||||
"scripts": {
|
||||
"postinstall": "patch-package && npm run generate:i18n-keys",
|
||||
|
||||
Generated
+3
-1
@@ -695,7 +695,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.14.1",
|
||||
"version": "6.15.2",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz",
|
||||
"integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"side-channel": "^1.1.0"
|
||||
|
||||
@@ -21,7 +21,6 @@ export const handleError = (
|
||||
consoleErrorPrefix?: string,
|
||||
options?: HandleErrorOptions,
|
||||
): void => {
|
||||
console.log("{{cdbp}} in handleError(): raw error: " + stringifyError(error)); //CTODO in case a stray error happens
|
||||
const errorMessage = getErrorMessage(error);
|
||||
const errorCode = error instanceof ARMError ? error.code : undefined;
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { stringifyError } from "Common/stringifyError";
|
||||
import { CosmosDbArtifactType } from "Contracts/FabricMessagesContract";
|
||||
import { isFabric, isFabricMirroredKey, isFabricNative } from "Platform/Fabric/FabricUtil";
|
||||
import { AuthType } from "../../AuthType";
|
||||
@@ -27,7 +26,6 @@ export async function readDatabases(): Promise<DataModels.Database[]> {
|
||||
(userContext.fabricContext?.artifactInfo as FabricArtifactInfo[CosmosDbArtifactType.MIRRORED_KEY]).resourceTokenInfo
|
||||
.resourceTokens
|
||||
) {
|
||||
console.log("{{cdbp}} in readDatabases(): isFabricMirroredKey && has resourceTokens"); //CTODO should not get here
|
||||
const tokensData = (userContext.fabricContext.artifactInfo as FabricArtifactInfo[CosmosDbArtifactType.MIRRORED_KEY])
|
||||
.resourceTokenInfo;
|
||||
|
||||
@@ -61,7 +59,6 @@ export async function readDatabases(): Promise<DataModels.Database[]> {
|
||||
clearMessage();
|
||||
return databases;
|
||||
} else if (isFabricNative() && userContext.fabricContext?.databaseName) {
|
||||
console.log("{{cdbp}} in readDatabases(): isFabricNative"); //CTODO should not get here
|
||||
const databaseId = userContext.fabricContext.databaseName;
|
||||
databases = [
|
||||
{
|
||||
@@ -84,15 +81,9 @@ export async function readDatabases(): Promise<DataModels.Database[]> {
|
||||
userContext.apiType !== "Tables" &&
|
||||
!isFabric()
|
||||
) {
|
||||
console.log("{{cdbp}} in readDatabases(): authType == AAD, enableSDKOperations, apiType != Tables, !isFabric");
|
||||
console.log("{{cdbp}} in readDatabases(): databaseaccount: " + userContext.databaseAccount);
|
||||
console.log("{{cdbp}} in readDatabases(): calling readDatabasesWithARM");
|
||||
databases = await readDatabasesWithARM();
|
||||
console.log("{{cdbp}} in readDatabases(): done readDatabasesWithARM");
|
||||
} else {
|
||||
console.log("{{cdbp}} in readDatabases(): calling SDK");
|
||||
const sdkResponse = await client().databases.readAll().fetchAll();
|
||||
console.log("{{cdbp}} in readDatabases(): done SDK");
|
||||
databases = sdkResponse.resources as DataModels.Database[];
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -117,30 +108,22 @@ export async function readDatabasesWithARM(accountOverride?: {
|
||||
const accountName = accountOverride?.accountName ?? userContext?.databaseAccount?.name ?? "";
|
||||
const apiType = accountOverride?.apiType ?? userContext.apiType;
|
||||
|
||||
try {
|
||||
switch (apiType) {
|
||||
case "SQL":
|
||||
console.log("{{cdbp}} in readDatabasesWithARM(): calling listSqlDatabases");
|
||||
rpResponse = await listSqlDatabases(subscriptionId, resourceGroup, accountName);
|
||||
console.log("{{cdbp}} in readDatabasesWithARM(): done listSqlDatabases");
|
||||
break;
|
||||
case "Mongo":
|
||||
rpResponse = await listMongoDBDatabases(subscriptionId, resourceGroup, accountName);
|
||||
break;
|
||||
case "Cassandra":
|
||||
rpResponse = await listCassandraKeyspaces(subscriptionId, resourceGroup, accountName);
|
||||
break;
|
||||
case "Gremlin":
|
||||
rpResponse = await listGremlinDatabases(subscriptionId, resourceGroup, accountName);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported default experience type: ${apiType}`);
|
||||
}
|
||||
|
||||
console.log("{{cdbp}} in readDatabasesWithARM(): response: " + JSON.stringify(rpResponse));
|
||||
return rpResponse?.value?.map((database) => database.properties?.resource as DataModels.Database) ?? [];
|
||||
} catch (error) {
|
||||
console.log("{{cdbp}} in readDatabasesWithARM(): ERROR: " + stringifyError(error));
|
||||
throw error;
|
||||
switch (apiType) {
|
||||
case "SQL":
|
||||
rpResponse = await listSqlDatabases(subscriptionId, resourceGroup, accountName);
|
||||
break;
|
||||
case "Mongo":
|
||||
rpResponse = await listMongoDBDatabases(subscriptionId, resourceGroup, accountName);
|
||||
break;
|
||||
case "Cassandra":
|
||||
rpResponse = await listCassandraKeyspaces(subscriptionId, resourceGroup, accountName);
|
||||
break;
|
||||
case "Gremlin":
|
||||
rpResponse = await listGremlinDatabases(subscriptionId, resourceGroup, accountName);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported default experience type: ${apiType}`);
|
||||
}
|
||||
|
||||
return rpResponse?.value?.map((database) => database.properties?.resource as DataModels.Database) ?? [];
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
@@ -402,27 +402,19 @@ export default class Explorer {
|
||||
},
|
||||
startKey,
|
||||
);
|
||||
console.log("{{cdbp}} in refreshAllDatabases(): done readDatabases");
|
||||
const currentDatabases = useDatabases.getState().databases;
|
||||
console.log("{{cdbp}} in refreshAllDatabases(): currentDatabases: " + currentDatabases);
|
||||
const deltaDatabases = this.getDeltaDatabases(databases, currentDatabases);
|
||||
console.log("{{cdbp}} in refreshAllDatabases(): deltaDatabases: " + deltaDatabases);
|
||||
let updatedDatabases = currentDatabases.filter(
|
||||
(database) => !deltaDatabases.toDelete.some((deletedDatabase) => deletedDatabase.id() === database.id()),
|
||||
);
|
||||
console.log("{{cdbp}} in refreshAllDatabases(): updatedDatabases after filter: " + updatedDatabases);
|
||||
updatedDatabases = [...updatedDatabases, ...deltaDatabases.toAdd].sort((db1, db2) =>
|
||||
db1.id().localeCompare(db2.id()),
|
||||
);
|
||||
console.log("{{cdbp}} in refreshAllDatabases(): updatedDatabases after sort: " + updatedDatabases);
|
||||
useDatabases.setState({ databases: updatedDatabases, databasesFetchedSuccessfully: true });
|
||||
scenarioMonitor.completePhase(MetricScenario.DatabaseLoad, ApplicationMetricPhase.DatabasesFetched);
|
||||
|
||||
console.log("{{cdbp}} in refreshAllDatabases(): calling refreshAndExpandNewDatabases");
|
||||
await this.refreshAndExpandNewDatabases(deltaDatabases.toAdd, updatedDatabases);
|
||||
console.log("{{cdbp}} in refreshAllDatabases(): done refreshAndExpandNewDatabases");
|
||||
} catch (error) {
|
||||
console.log("{{cdbp}} in refreshAllDatabases(): ERROR: " + stringifyError(error)); //CTODO this should be logged already but just in case
|
||||
const errorMessage = getErrorMessage(error);
|
||||
TelemetryProcessor.traceFailure(
|
||||
Action.LoadDatabases,
|
||||
@@ -612,7 +604,6 @@ export default class Explorer {
|
||||
? databases
|
||||
: databases.filter((db) => db.isDatabaseExpanded() || db.id() === Constants.SavedQueries.DatabaseName);
|
||||
|
||||
console.log("{{cdbp}} in refreshAndExpandNewDatabases(): databasesToLoad: " + databasesToLoad);
|
||||
const startKey: number = TelemetryProcessor.traceStart(Action.LoadCollections, {
|
||||
dataExplorerArea: Constants.Areas.ResourceTree,
|
||||
});
|
||||
@@ -621,7 +612,6 @@ export default class Explorer {
|
||||
try {
|
||||
await Promise.all(
|
||||
databasesToLoad.map(async (database: ViewModels.Database) => {
|
||||
console.log("{{cdbp}} in refreshAndExpandNewDatabases(): loadCollections for database: " + database.id);
|
||||
await database.loadCollections(true);
|
||||
const isNewDatabase: boolean = _.some(newDatabases, (db: ViewModels.Database) => db.id() === database.id());
|
||||
if (isNewDatabase) {
|
||||
@@ -641,7 +631,6 @@ export default class Explorer {
|
||||
// Start DatabaseTreeRendered — React render cycle will complete it in ResourceTree
|
||||
scenarioMonitor.startPhase(MetricScenario.DatabaseLoad, ApplicationMetricPhase.DatabaseTreeRendered);
|
||||
} catch (error) {
|
||||
console.log("{{cdbp}} in refreshAndExpandNewDatabases(): ERROR: " + stringifyError(error)); //CTODO this should be logged already but just in case
|
||||
TelemetryProcessor.traceFailure(
|
||||
Action.LoadCollections,
|
||||
{
|
||||
@@ -970,25 +959,8 @@ export default class Explorer {
|
||||
await this.notebookManager?.notebookContentClient.updateItemChildrenInPlace(item);
|
||||
}
|
||||
|
||||
public async openNotebookTerminal(kind: ViewModels.TerminalKind): Promise<void> {
|
||||
if (userContext.features.enableCloudShell) {
|
||||
this.connectToNotebookTerminal(kind);
|
||||
} else if (useNotebook.getState().isPhoenixFeatures) {
|
||||
await this.allocateContainer();
|
||||
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
||||
if (notebookServerInfo && notebookServerInfo.notebookServerEndpoint !== undefined) {
|
||||
this.connectToNotebookTerminal(kind);
|
||||
} else {
|
||||
useDialog
|
||||
.getState()
|
||||
.showOkModalDialog(
|
||||
"Failed to connect",
|
||||
"Failed to connect to temporary workspace. This could happen because of network issues. Please refresh the page and try again.",
|
||||
);
|
||||
}
|
||||
} else {
|
||||
this.connectToNotebookTerminal(kind);
|
||||
}
|
||||
public openNotebookTerminal(kind: ViewModels.TerminalKind): void {
|
||||
this.connectToNotebookTerminal(kind);
|
||||
}
|
||||
|
||||
private connectToNotebookTerminal(kind: ViewModels.TerminalKind): void {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useTerminal } from "hooks/useTerminal";
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import { Terminal } from "xterm";
|
||||
import { FitAddon } from "xterm-addon-fit";
|
||||
@@ -59,7 +60,14 @@ export const CloudShellTerminalComponent: React.FC<CloudShellTerminalComponentPr
|
||||
});
|
||||
resizeObserver.observe(terminalRef.current);
|
||||
|
||||
socketRef.current = startCloudShellTerminal(terminal, props.shellType);
|
||||
const initTerminal = async () => {
|
||||
const socket = await startCloudShellTerminal(terminal, props.shellType);
|
||||
socketRef.current = socket;
|
||||
if (socket) {
|
||||
useTerminal.getState().setSocket(socket);
|
||||
}
|
||||
};
|
||||
initTerminal();
|
||||
|
||||
// Cleanup function to close WebSocket and dispose terminal
|
||||
return () => {
|
||||
@@ -73,6 +81,7 @@ export const CloudShellTerminalComponent: React.FC<CloudShellTerminalComponentPr
|
||||
resizeObserver.unobserve(terminalRef.current);
|
||||
}
|
||||
terminal.dispose(); // Clean up XTerm instance
|
||||
useTerminal.getState().setSocket(undefined);
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import { CosmosFluentProvider } from "Explorer/Theme/ThemeUtil";
|
||||
import { KeyboardAction } from "KeyboardShortcuts";
|
||||
import { Keys, t } from "Localization";
|
||||
import { QueryConstants } from "Shared/Constants";
|
||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||
import { LocalStorageUtility, StorageKey, getDefaultQueryResultsView } from "Shared/StorageUtility";
|
||||
import { Allotment } from "allotment";
|
||||
import { useClientWriteEnabled } from "hooks/useClientWriteEnabled";
|
||||
import { TabsState, useTabs } from "hooks/useTabs";
|
||||
@@ -163,8 +163,11 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
||||
isExecuting: false,
|
||||
cancelQueryTimeoutID: undefined,
|
||||
currentTabActive: true,
|
||||
queryResultsView:
|
||||
props.splitterDirection === "vertical" ? SplitterDirection.Vertical : SplitterDirection.Horizontal,
|
||||
queryResultsView: props.splitterDirection
|
||||
? props.splitterDirection === "vertical"
|
||||
? SplitterDirection.Vertical
|
||||
: SplitterDirection.Horizontal
|
||||
: getDefaultQueryResultsView(),
|
||||
queryViewSizePercent: props.queryViewSizePercent,
|
||||
};
|
||||
this.isCloseClicked = false;
|
||||
@@ -486,6 +489,13 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
||||
commandButtonLabel: t(Keys.tabs.query.view),
|
||||
ariaLabel: t(Keys.tabs.query.view),
|
||||
hasPopup: true,
|
||||
onCommandClick: () => {
|
||||
const newDirection =
|
||||
this.state.queryResultsView === SplitterDirection.Vertical
|
||||
? SplitterDirection.Horizontal
|
||||
: SplitterDirection.Vertical;
|
||||
this._setViewLayout(newDirection);
|
||||
},
|
||||
children: [verticalButton, horizontalButton],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,31 +1,18 @@
|
||||
import { Spinner, SpinnerSize, Stack, Text } from "@fluentui/react";
|
||||
import { Stack } from "@fluentui/react";
|
||||
|
||||
import { NotebookWorkspaceConnectionInfo } from "Contracts/DataModels";
|
||||
import { MessageTypes } from "Contracts/ExplorerContracts";
|
||||
import { NotebookTerminalComponent } from "Explorer/Controls/Notebook/NotebookTerminalComponent";
|
||||
import Explorer from "Explorer/Explorer";
|
||||
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
||||
import { TerminalKind } from "Contracts/ViewModels";
|
||||
import { QuickstartFirewallNotification } from "Explorer/Quickstart/QuickstartFirewallNotification";
|
||||
import { QuickstartGuide } from "Explorer/Quickstart/QuickstartGuide";
|
||||
import { CloudShellTerminalComponent } from "Explorer/Tabs/CloudShellTab/CloudShellTerminalComponent";
|
||||
import { checkFirewallRules } from "Explorer/Tabs/Shared/CheckFirewallRules";
|
||||
import { userContext } from "UserContext";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import FirewallRuleScreenshot from "../../../images/firewallRule.png";
|
||||
|
||||
interface QuickstartTabProps {
|
||||
explorer: Explorer;
|
||||
}
|
||||
|
||||
export const QuickstartTab: React.FC<QuickstartTabProps> = ({ explorer }: QuickstartTabProps): JSX.Element => {
|
||||
const notebookServerInfo = useNotebook((state) => state.notebookServerInfo);
|
||||
export const QuickstartTab: React.FC = (): JSX.Element => {
|
||||
const [isAllPublicIPAddressEnabled, setIsAllPublicIPAddressEnabled] = useState<boolean>(true);
|
||||
|
||||
const getNotebookServerInfo = (): NotebookWorkspaceConnectionInfo => ({
|
||||
authToken: notebookServerInfo.authToken,
|
||||
notebookServerEndpoint: `${notebookServerInfo.notebookServerEndpoint?.replace(/\/+$/, "")}/postgresql`,
|
||||
forwardingId: notebookServerInfo.forwardingId,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
checkFirewallRules(
|
||||
"2022-11-08",
|
||||
@@ -34,10 +21,6 @@ export const QuickstartTab: React.FC<QuickstartTabProps> = ({ explorer }: Quicks
|
||||
);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
explorer.allocateContainer();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Stack style={{ width: "100%" }} horizontal>
|
||||
<Stack style={{ width: "50%" }}>
|
||||
@@ -51,24 +34,13 @@ export const QuickstartTab: React.FC<QuickstartTabProps> = ({ explorer }: Quicks
|
||||
shellName="PostgreSQL"
|
||||
/>
|
||||
)}
|
||||
{isAllPublicIPAddressEnabled && notebookServerInfo?.notebookServerEndpoint && (
|
||||
<NotebookTerminalComponent
|
||||
notebookServerInfo={getNotebookServerInfo()}
|
||||
{isAllPublicIPAddressEnabled && (
|
||||
<CloudShellTerminalComponent
|
||||
databaseAccount={userContext.databaseAccount}
|
||||
tabId="QuickstartPSQLShell"
|
||||
shellType={TerminalKind.Postgres}
|
||||
/>
|
||||
)}
|
||||
{isAllPublicIPAddressEnabled && !notebookServerInfo?.notebookServerEndpoint && (
|
||||
<Stack style={{ margin: "auto 0" }}>
|
||||
<Text block style={{ margin: "auto" }}>
|
||||
Connecting to the PostgreSQL shell.
|
||||
</Text>
|
||||
<Text block style={{ margin: "auto" }}>
|
||||
If the cluster was just created, this could take up to a minute.
|
||||
</Text>
|
||||
<Spinner styles={{ root: { marginTop: 16 } }} size={SpinnerSize.large}></Spinner>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import { NotebookTerminalComponent } from "Explorer/Controls/Notebook/NotebookTerminalComponent";
|
||||
import * as React from "react";
|
||||
import * as DataModels from "../../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { BaseTerminalComponentAdapter } from "./BaseTerminalComponentAdapter";
|
||||
|
||||
/**
|
||||
* Notebook terminal tab
|
||||
*/
|
||||
export class NotebookTerminalComponentAdapter extends BaseTerminalComponentAdapter {
|
||||
constructor(
|
||||
private getNotebookServerInfo: () => DataModels.NotebookWorkspaceConnectionInfo,
|
||||
getDatabaseAccount: () => DataModels.DatabaseAccount,
|
||||
getTabId: () => string,
|
||||
getUsername: () => string,
|
||||
isAllPublicIPAddressesEnabled: ko.Observable<boolean>,
|
||||
kind: ViewModels.TerminalKind,
|
||||
) {
|
||||
super(getDatabaseAccount, getTabId, getUsername, isAllPublicIPAddressesEnabled, kind);
|
||||
}
|
||||
|
||||
protected renderTerminalComponent(): JSX.Element {
|
||||
return (
|
||||
<NotebookTerminalComponent
|
||||
notebookServerInfo={this.getNotebookServerInfo()}
|
||||
databaseAccount={this.getDatabaseAccount()}
|
||||
tabId={this.getTabId()}
|
||||
username={this.getUsername()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -333,11 +333,7 @@ const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): J
|
||||
return <SplashScreen explorer={explorer} />;
|
||||
}
|
||||
case ReactTabKind.Quickstart:
|
||||
return userContext.apiType === "VCoreMongo" ? (
|
||||
<VcoreMongoQuickstartTab explorer={explorer} />
|
||||
) : (
|
||||
<QuickstartTab explorer={explorer} />
|
||||
);
|
||||
return userContext.apiType === "VCoreMongo" ? <VcoreMongoQuickstartTab /> : <QuickstartTab />;
|
||||
default:
|
||||
throw new Error(`Unsupported tab kind ${ReactTabKind[activeReactTab]}`);
|
||||
}
|
||||
|
||||
@@ -7,8 +7,6 @@ import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
||||
import Explorer from "../Explorer";
|
||||
import { useNotebook } from "../Notebook/useNotebook";
|
||||
import { NotebookTerminalComponentAdapter } from "./ShellAdapters/NotebookTerminalComponentAdapter";
|
||||
import TabsBase from "./TabsBase";
|
||||
|
||||
export interface TerminalTabOptions extends ViewModels.TabOptions {
|
||||
@@ -29,41 +27,17 @@ export default class TerminalTab extends TabsBase {
|
||||
this.container = options.container;
|
||||
this.isAllPublicIPAddressesEnabled = ko.observable(true);
|
||||
|
||||
const commonArgs: [
|
||||
() => DataModels.DatabaseAccount,
|
||||
() => string,
|
||||
() => string,
|
||||
ko.Observable<boolean>,
|
||||
ViewModels.TerminalKind,
|
||||
] = [
|
||||
this.notebookTerminalComponentAdapter = new CloudShellTerminalComponentAdapter(
|
||||
() => userContext?.databaseAccount,
|
||||
() => this.tabId,
|
||||
() => this.getUsername(),
|
||||
this.isAllPublicIPAddressesEnabled,
|
||||
options.kind,
|
||||
];
|
||||
);
|
||||
|
||||
if (userContext.features.enableCloudShell) {
|
||||
this.notebookTerminalComponentAdapter = new CloudShellTerminalComponentAdapter(...commonArgs);
|
||||
|
||||
this.notebookTerminalComponentAdapter.parameters = ko.computed<boolean>(() => {
|
||||
return this.isTemplateReady() && this.isAllPublicIPAddressesEnabled();
|
||||
});
|
||||
} else {
|
||||
this.notebookTerminalComponentAdapter = new NotebookTerminalComponentAdapter(
|
||||
() => this.getNotebookServerInfo(options),
|
||||
...commonArgs,
|
||||
);
|
||||
|
||||
this.notebookTerminalComponentAdapter.parameters = ko.computed<boolean>(() => {
|
||||
return (
|
||||
this.isTemplateReady() &&
|
||||
useNotebook.getState().isNotebookEnabled &&
|
||||
useNotebook.getState().notebookServerInfo?.notebookServerEndpoint &&
|
||||
this.isAllPublicIPAddressesEnabled()
|
||||
);
|
||||
});
|
||||
}
|
||||
this.notebookTerminalComponentAdapter.parameters = ko.computed<boolean>(() => {
|
||||
return this.isTemplateReady() && this.isAllPublicIPAddressesEnabled();
|
||||
});
|
||||
|
||||
if (options.kind === ViewModels.TerminalKind.Postgres) {
|
||||
checkFirewallRules(
|
||||
@@ -72,16 +46,6 @@ export default class TerminalTab extends TabsBase {
|
||||
this.isAllPublicIPAddressesEnabled,
|
||||
);
|
||||
}
|
||||
|
||||
if (options.kind === ViewModels.TerminalKind.VCoreMongo) {
|
||||
checkFirewallRules(
|
||||
"2023-03-01-preview",
|
||||
(rule) =>
|
||||
rule.name.startsWith("AllowAllAzureServicesAndResourcesWithinAzureIps") ||
|
||||
(rule.properties.startIpAddress === "0.0.0.0" && rule.properties.endIpAddress === "255.255.255.255"),
|
||||
this.isAllPublicIPAddressesEnabled,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public getContainer(): Explorer {
|
||||
@@ -96,42 +60,6 @@ export default class TerminalTab extends TabsBase {
|
||||
this.updateNavbarWithTabsButtons();
|
||||
}
|
||||
|
||||
private getNotebookServerInfo(options: TerminalTabOptions): DataModels.NotebookWorkspaceConnectionInfo {
|
||||
let endpointSuffix: string;
|
||||
|
||||
switch (options.kind) {
|
||||
case ViewModels.TerminalKind.Default:
|
||||
endpointSuffix = "";
|
||||
break;
|
||||
|
||||
case ViewModels.TerminalKind.Mongo:
|
||||
endpointSuffix = "mongo";
|
||||
break;
|
||||
|
||||
case ViewModels.TerminalKind.Cassandra:
|
||||
endpointSuffix = "cassandra";
|
||||
break;
|
||||
|
||||
case ViewModels.TerminalKind.Postgres:
|
||||
endpointSuffix = "postgresql";
|
||||
break;
|
||||
|
||||
case ViewModels.TerminalKind.VCoreMongo:
|
||||
endpointSuffix = "mongovcore";
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error(`Terminal kind: ${options.kind} not supported`);
|
||||
}
|
||||
|
||||
const info: DataModels.NotebookWorkspaceConnectionInfo = useNotebook.getState().notebookServerInfo;
|
||||
return {
|
||||
authToken: info.authToken,
|
||||
notebookServerEndpoint: `${info.notebookServerEndpoint.replace(/\/+$/, "")}/${endpointSuffix}`,
|
||||
forwardingId: info.forwardingId,
|
||||
};
|
||||
}
|
||||
|
||||
private getUsername(): string {
|
||||
if (userContext.apiType !== "VCoreMongo" || !userContext?.vcoreMongoConnectionParams?.adminLogin) {
|
||||
return undefined;
|
||||
|
||||
@@ -1,79 +1,24 @@
|
||||
import { Spinner, SpinnerSize, Stack, Text } from "@fluentui/react";
|
||||
import { Stack } from "@fluentui/react";
|
||||
|
||||
import { NotebookWorkspaceConnectionInfo } from "Contracts/DataModels";
|
||||
import { MessageTypes } from "Contracts/ExplorerContracts";
|
||||
import { NotebookTerminalComponent } from "Explorer/Controls/Notebook/NotebookTerminalComponent";
|
||||
import Explorer from "Explorer/Explorer";
|
||||
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
||||
import { QuickstartFirewallNotification } from "Explorer/Quickstart/QuickstartFirewallNotification";
|
||||
import { TerminalKind } from "Contracts/ViewModels";
|
||||
import { VcoreMongoQuickstartGuide } from "Explorer/Quickstart/VCoreMongoQuickstartGuide";
|
||||
import { checkFirewallRules } from "Explorer/Tabs/Shared/CheckFirewallRules";
|
||||
import { CloudShellTerminalComponent } from "Explorer/Tabs/CloudShellTab/CloudShellTerminalComponent";
|
||||
import { userContext } from "UserContext";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import FirewallRuleScreenshot from "../../../images/vcoreMongoFirewallRule.png";
|
||||
|
||||
interface VCoreMongoQuickstartTabProps {
|
||||
explorer: Explorer;
|
||||
}
|
||||
|
||||
export const VcoreMongoQuickstartTab: React.FC<VCoreMongoQuickstartTabProps> = ({
|
||||
explorer,
|
||||
}: VCoreMongoQuickstartTabProps): JSX.Element => {
|
||||
const notebookServerInfo = useNotebook((state) => state.notebookServerInfo);
|
||||
const [isAllPublicIPAddressEnabled, setIsAllPublicIPAddressEnabled] = useState<boolean>(true);
|
||||
|
||||
const getNotebookServerInfo = (): NotebookWorkspaceConnectionInfo => ({
|
||||
authToken: notebookServerInfo.authToken,
|
||||
notebookServerEndpoint: `${notebookServerInfo.notebookServerEndpoint?.replace(/\/+$/, "")}/mongovcore`,
|
||||
forwardingId: notebookServerInfo.forwardingId,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
checkFirewallRules(
|
||||
"2023-03-01-preview",
|
||||
(rule) =>
|
||||
rule.name.startsWith("AllowAllAzureServicesAndResourcesWithinAzureIps") ||
|
||||
(rule.properties.startIpAddress === "0.0.0.0" && rule.properties.endIpAddress === "255.255.255.255"),
|
||||
setIsAllPublicIPAddressEnabled,
|
||||
);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
explorer.allocateContainer();
|
||||
}, []);
|
||||
import React from "react";
|
||||
|
||||
export const VcoreMongoQuickstartTab: React.FC = (): JSX.Element => {
|
||||
return (
|
||||
<Stack style={{ width: "100%" }} horizontal>
|
||||
<Stack style={{ width: "50%" }}>
|
||||
<VcoreMongoQuickstartGuide />
|
||||
</Stack>
|
||||
<Stack style={{ width: "50%", borderLeft: "black solid 1px" }}>
|
||||
{!isAllPublicIPAddressEnabled && (
|
||||
<QuickstartFirewallNotification
|
||||
messageType={MessageTypes.OpenVCoreMongoNetworkingBlade}
|
||||
screenshot={FirewallRuleScreenshot}
|
||||
shellName="MongoDB"
|
||||
/>
|
||||
)}
|
||||
{isAllPublicIPAddressEnabled && notebookServerInfo?.notebookServerEndpoint && (
|
||||
<NotebookTerminalComponent
|
||||
notebookServerInfo={getNotebookServerInfo()}
|
||||
databaseAccount={userContext.databaseAccount}
|
||||
tabId="QuickstartVcoreMongoShell"
|
||||
username={userContext.vcoreMongoConnectionParams.adminLogin}
|
||||
/>
|
||||
)}
|
||||
{isAllPublicIPAddressEnabled && !notebookServerInfo?.notebookServerEndpoint && (
|
||||
<Stack style={{ margin: "auto 0" }}>
|
||||
<Text block style={{ margin: "auto" }}>
|
||||
Connecting to the Mongo shell.
|
||||
</Text>
|
||||
<Text block style={{ margin: "auto" }}>
|
||||
If the cluster was just created, this could take up to a minute.
|
||||
</Text>
|
||||
<Spinner styles={{ root: { marginTop: 16 } }} size={SpinnerSize.large}></Spinner>
|
||||
</Stack>
|
||||
)}
|
||||
<CloudShellTerminalComponent
|
||||
databaseAccount={userContext.databaseAccount}
|
||||
tabId="QuickstartVcoreMongoShell"
|
||||
username={userContext.vcoreMongoConnectionParams?.adminLogin}
|
||||
shellType={TerminalKind.VCoreMongo}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
@@ -47,7 +47,6 @@ import "./Explorer/Controls/CollapsiblePanel/CollapsiblePanelComponent.less";
|
||||
import { Dialog } from "./Explorer/Controls/Dialog";
|
||||
import "./Explorer/Controls/ErrorDisplayComponent/ErrorDisplayComponent.less";
|
||||
import "./Explorer/Controls/JsonEditor/JsonEditorComponent.less";
|
||||
import "./Explorer/Controls/Notebook/NotebookTerminalComponent.less";
|
||||
import "./Explorer/Controls/TreeComponent/treeComponent.less";
|
||||
import { ErrorBoundary } from "./Explorer/ErrorBoundary";
|
||||
import "./Explorer/Graph/GraphExplorerComponent/graphExplorer.less";
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
/**
|
||||
* JupyterLab applications based on jupyterLab components
|
||||
*/
|
||||
import { ServerConnection, TerminalManager } from "@jupyterlab/services";
|
||||
import { IMessage, ITerminalConnection } from "@jupyterlab/services/lib/terminal/terminal";
|
||||
import { Terminal } from "@jupyterlab/terminal";
|
||||
import { Panel, Widget } from "@phosphor/widgets";
|
||||
import { userContext } from "UserContext";
|
||||
|
||||
export class JupyterLabAppFactory {
|
||||
private isShellStarted: boolean | undefined;
|
||||
private checkShellStarted: ((content: string | undefined) => void) | undefined;
|
||||
private onShellExited: (restartShell: boolean) => void;
|
||||
private restartShell: boolean;
|
||||
|
||||
private isShellExited(content: string | undefined) {
|
||||
if (userContext.apiType === "VCoreMongo" && content?.includes("MongoServerError: Invalid key")) {
|
||||
this.restartShell = true;
|
||||
}
|
||||
return content?.includes("cosmosshelluser@");
|
||||
}
|
||||
|
||||
private isMongoShellStarted(content: string | undefined) {
|
||||
this.isShellStarted = content?.includes("MongoDB shell version");
|
||||
}
|
||||
|
||||
private isCassandraShellStarted(content: string | undefined) {
|
||||
this.isShellStarted = content?.includes("Connected to") && content?.includes("cqlsh");
|
||||
}
|
||||
|
||||
private isPostgresShellStarted(content: string | undefined) {
|
||||
this.isShellStarted = content?.includes("citus=>");
|
||||
}
|
||||
|
||||
private isVCoreMongoShellStarted(content: string | undefined) {
|
||||
this.isShellStarted = content?.includes("Enter password");
|
||||
}
|
||||
|
||||
constructor(closeTab: (restartShell: boolean) => void) {
|
||||
this.onShellExited = closeTab;
|
||||
this.isShellStarted = false;
|
||||
this.checkShellStarted = undefined;
|
||||
this.restartShell = false;
|
||||
|
||||
switch (userContext.apiType) {
|
||||
case "Mongo":
|
||||
this.checkShellStarted = this.isMongoShellStarted;
|
||||
break;
|
||||
case "Cassandra":
|
||||
this.checkShellStarted = this.isCassandraShellStarted;
|
||||
break;
|
||||
case "Postgres":
|
||||
this.checkShellStarted = this.isPostgresShellStarted;
|
||||
break;
|
||||
case "VCoreMongo":
|
||||
this.checkShellStarted = this.isVCoreMongoShellStarted;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public async createTerminalApp(serverSettings: ServerConnection.ISettings): Promise<ITerminalConnection | undefined> {
|
||||
const configurationSettings: Partial<ServerConnection.ISettings> = serverSettings;
|
||||
(configurationSettings.appendToken as boolean) = false;
|
||||
serverSettings = ServerConnection.makeSettings(configurationSettings);
|
||||
const manager = new TerminalManager({
|
||||
serverSettings: serverSettings,
|
||||
});
|
||||
const session = await manager.startNew();
|
||||
session.messageReceived.connect(async (_, message: IMessage) => {
|
||||
const content = message.content && message.content[0]?.toString();
|
||||
if (this.checkShellStarted && message.type == "stdout") {
|
||||
//Close the terminal tab once the shell closed messages are received
|
||||
if (!this.isShellStarted) {
|
||||
this.checkShellStarted(content);
|
||||
} else if (this.isShellExited(content)) {
|
||||
this.onShellExited(this.restartShell);
|
||||
}
|
||||
}
|
||||
}, this);
|
||||
|
||||
let internalSend = session.send;
|
||||
session.send = (message: IMessage) => {
|
||||
message?.content?.push(serverSettings?.token);
|
||||
internalSend.call(session, message);
|
||||
};
|
||||
const term = new Terminal(session, { theme: "dark", shutdownOnClose: true });
|
||||
|
||||
if (!term) {
|
||||
console.error("Failed starting terminal");
|
||||
return undefined;
|
||||
}
|
||||
|
||||
term.title.closable = false;
|
||||
term.addClass("terminalWidget");
|
||||
|
||||
let panel = new Panel();
|
||||
panel.addWidget(term as any);
|
||||
panel.id = "main";
|
||||
|
||||
// Attach the widget to the dom.
|
||||
Widget.attach(panel, document.body);
|
||||
|
||||
// Switch focus to the terminal
|
||||
term.activate();
|
||||
|
||||
// Handle resize events.
|
||||
window.addEventListener("resize", () => {
|
||||
panel.update();
|
||||
});
|
||||
|
||||
// Dispose terminal when unloading.
|
||||
window.addEventListener("unload", () => {
|
||||
panel.dispose();
|
||||
});
|
||||
|
||||
// Close terminal when Ctrl key is pressed
|
||||
term.node.addEventListener("keydown", (event: KeyboardEvent) => {
|
||||
if (event.ctrlKey) {
|
||||
this.onShellExited(false);
|
||||
}
|
||||
});
|
||||
|
||||
return session;
|
||||
}
|
||||
}
|
||||
-111
@@ -1,111 +0,0 @@
|
||||
/**
|
||||
* Message handling with iframe parent
|
||||
*/
|
||||
export interface UpdateMessage {
|
||||
command: string;
|
||||
arg?: any;
|
||||
}
|
||||
export declare type ContentType = "notebook" | "file" | "directory";
|
||||
export interface ContentItem {
|
||||
name: string;
|
||||
path: string;
|
||||
type: ContentType;
|
||||
}
|
||||
export interface UploadData {
|
||||
filepath: string;
|
||||
content: string;
|
||||
}
|
||||
export interface RenameFileData {
|
||||
sourcePath: string;
|
||||
targetPath: string;
|
||||
}
|
||||
export interface RenameFileResult {
|
||||
source: string;
|
||||
target: ContentItem;
|
||||
}
|
||||
export interface FromDataExplorerMessage {
|
||||
type: MessageTypes;
|
||||
params: any;
|
||||
id: string;
|
||||
}
|
||||
export declare type KernelStatusStates =
|
||||
| "unknown"
|
||||
| "starting"
|
||||
| "reconnecting"
|
||||
| "idle"
|
||||
| "busy"
|
||||
| "restarting"
|
||||
| "autorestarting"
|
||||
| "dead"
|
||||
| "connected";
|
||||
/**
|
||||
* Unsolicited message
|
||||
*/
|
||||
export interface FromNotebookUpdateMessage {
|
||||
type: NotebookUpdateTypes;
|
||||
arg?: any;
|
||||
}
|
||||
/**
|
||||
* Response to a Data Explorer request
|
||||
*/
|
||||
export interface FromNotebookResponseMessage {
|
||||
id: string;
|
||||
data?: any;
|
||||
error?: any;
|
||||
}
|
||||
export interface FromNotebookMessage {
|
||||
actionType: ActionTypes;
|
||||
message: FromNotebookUpdateMessage | FromNotebookResponseMessage;
|
||||
}
|
||||
export declare type KernelOption = {
|
||||
name: string;
|
||||
displayName: string;
|
||||
};
|
||||
export interface KernelSpecs {
|
||||
defaultName: string;
|
||||
kernelSpecs: {
|
||||
[name: string]: KernelOption;
|
||||
};
|
||||
}
|
||||
export declare enum ActionTypes {
|
||||
Update = 0,
|
||||
Response = 1,
|
||||
}
|
||||
/**
|
||||
* Messages Data Explorer -> JupyterLabApp
|
||||
*/
|
||||
export declare enum MessageTypes {
|
||||
FileList = 0,
|
||||
CreateInDir = 1,
|
||||
DeleteFile = 2,
|
||||
UploadFile = 3,
|
||||
RenameFile = 4,
|
||||
ReadFileContent = 5,
|
||||
CreateDirectory = 6,
|
||||
InsertBelow = 7,
|
||||
RunAndAdvance = 8,
|
||||
Copy = 9,
|
||||
Cut = 10,
|
||||
Paste = 11,
|
||||
Undo = 12,
|
||||
ClearAllOutputs = 13,
|
||||
RunAll = 14,
|
||||
Redo = 15,
|
||||
Save = 16,
|
||||
RestartKernel = 17,
|
||||
ChangeCellType = 18,
|
||||
SwitchKernel = 19,
|
||||
ChangeKernel = 20,
|
||||
Status = 21,
|
||||
KernelList = 22,
|
||||
IsDirty = 23,
|
||||
Shutdown = 24,
|
||||
}
|
||||
export declare enum NotebookUpdateTypes {
|
||||
Ready = 0,
|
||||
ClickEvent = 1,
|
||||
ActiveCellType = 2,
|
||||
KernelChange = 3,
|
||||
FileSaved = 4,
|
||||
SessionStatusChange = 5,
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
# Summary
|
||||
This describes how to run a custom version of the Data Explorer in the Emulator which can open a jupyter notebook from with a tab.
|
||||
|
||||
# Requirements
|
||||
This requires:
|
||||
* a running instance of CosmosDB Emulator
|
||||
* a running instance of the jupyter server
|
||||
* access to the cosmosdb-dataexplorer git repository
|
||||
|
||||
# Installation
|
||||
## Install CosmosDB Emulator
|
||||
* Download from https://docs.microsoft.com/en-us/azure/cosmos-db/local-emulator
|
||||
* Open the Emulator and create at least one Collection
|
||||
|
||||
## Install Jupyter server on local machine (Windows)
|
||||
We use the Anaconda distribution which comes with a packaged jupyter and python.
|
||||
* Download and install Anaconda from https://www.anaconda.com/distribution/ (python3 64-bit version)
|
||||
Keep all default options. Install Visual Studio Code as well.
|
||||
### Verify Jupyter installation and create mynotebook
|
||||
* Open an "Anaconda Prompt" (hit the Window key, type "Anaconda", select "Anaconda Prompt" hit Enter)
|
||||
> cd src/jupyter-server (the notebooks will be saved in this directory)
|
||||
> jupyter notebook
|
||||
* It should open the browser at http://localhost:8888/ with the jupyter notebook.
|
||||
* Edit the notebook and save it as "mynotebook" (This should create a file: mynotebook.ipynb).
|
||||
We do this, because right now, the notebook filename is hardcoded as mynotebook.
|
||||
|
||||
### Modify jupyter server install
|
||||
In order to serve the jupyter frontend from the emulator, we need to turn off a bunch of things.
|
||||
* Stop the jupyter server (Ctrl-C twice from the Anaconda Prompt where you started jupyter notebook)
|
||||
* From the Anaconda Prompt, type: juypter notebook --generate-config
|
||||
* This should create the file: .jupyter/jupyter_notebook in your home directory.
|
||||
* Edit this file:
|
||||
|
||||
Enable embedding the jupyter frontend inside an iFrame in DataExplorer:
|
||||
c.NotebookApp.tornado_settings = { 'headers': { 'Content-Security-Policy': "frame-ancestors * localhost:1234 localhost:12900"} }
|
||||
|
||||
Enable a remotely-served jupyter frontend to still talk to the jupyter server:
|
||||
c.NotebookApp.allow_origin = '*'
|
||||
c.NotebookApp.allow_remote_access = True <--- not sure if this one matters
|
||||
c.NotebookApp.token = ''
|
||||
c.NotebookApp.disable_check_xsrf = True
|
||||
|
||||
## Install custom Data Explorer in Emulator
|
||||
* Install git from https://git-scm.com/download/win (keep all default options)
|
||||
* Install nodejs and npm from: https://nodejs.org/en/ (10.15.1 LTS)
|
||||
|
||||
### Download and build Data Explorer
|
||||
* From the Git Bash terminal:
|
||||
* cd ~/src
|
||||
* git clone https://msdata.visualstudio.com/DefaultCollection/CosmosDB/_git/cosmosdb-dataexplorer
|
||||
* cd cosmosdb-dataexplorer/Product/Portal
|
||||
* git checkout users/languye/spark-in-dataexplorer
|
||||
* cd JupyterLab
|
||||
* npm i
|
||||
* npm run build (this builds jupyterlab (the frontend of jupyter) and copies it into ../DataExplorer/notebookapp/)
|
||||
* cd ../DataExplorer
|
||||
* npm i
|
||||
* npm run build (this builds and copies DataExplorer into the Emulator folder)
|
||||
|
||||
# How to run the setup
|
||||
* Run the jupter-server by opening an Anaconda Prompt and typing: jupyter notebook
|
||||
* Open the emulator at: http://localhost:8081/_explorer/index.html
|
||||
* Click on any Collection
|
||||
* Click on "New Notebook" button in the Command bar
|
||||
* You should see the "mynotebook" jupyter notebook displayed in tab (inside an iframe).
|
||||
* There is a "New Cell" button in the CommandBar outside the jupyter iframe which will add a cell inside the notebook.
|
||||
|
||||
# Notes
|
||||
* The Emulator is located in: C:\Program Files\Azure Cosmos DB Emulator\Packages\DataExplorer
|
||||
* Running "jupyter notebook" serves the jupyter traditional frontend. There is an alternate frontend also developed by jupyter which is modular and customizable called: JupyterLab. We use their "notebook" example in this project slightly modified to pass the server and notebook pathname via iframe url's parameters:
|
||||
https://github.com/jupyterlab/jupyterlab/tree/master/examples/notebook
|
||||
jupyterlab uses the same communication protocol as the traditional frontend, so it can connect to any jupyter-server,
|
||||
so one can use multiple frontends (at the same time) to connect to a given jupyter-server.
|
||||
* The jupyter frontend and the server use websockets to communicate.
|
||||
@@ -1,15 +0,0 @@
|
||||
import { AuthType } from "../AuthType";
|
||||
import * as DataModels from "../Contracts/DataModels";
|
||||
import { ApiType } from "../UserContext";
|
||||
|
||||
export interface TerminalProps {
|
||||
authToken: string;
|
||||
notebookServerEndpoint: string;
|
||||
terminalEndpoint: string;
|
||||
databaseAccount: DataModels.DatabaseAccount;
|
||||
authType: AuthType;
|
||||
apiType: ApiType;
|
||||
subscriptionId: string;
|
||||
tabId: string;
|
||||
username?: string;
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
/*-----------------------------------------------------------------------------
|
||||
| Copyright (c) Jupyter Development Team.
|
||||
| Distributed under the terms of the Modified BSD License.
|
||||
|----------------------------------------------------------------------------*/
|
||||
|
||||
body {
|
||||
background: white;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#main {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
}
|
||||
|
||||
.jp-NotebookPanel {
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.terminalWidget {
|
||||
height: 100%;
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Notebook</title>
|
||||
</head>
|
||||
<body>
|
||||
<script id="jupyter-config-data" type="application/json">
|
||||
{
|
||||
"terminalsAvailable": "true"
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,125 +0,0 @@
|
||||
import { ServerConnection } from "@jupyterlab/services";
|
||||
import { IMessage, ITerminalConnection } from "@jupyterlab/services/lib/terminal/terminal";
|
||||
import "@jupyterlab/terminal/style/index.css";
|
||||
import { MessageTypes } from "Contracts/ExplorerContracts";
|
||||
import postRobot from "post-robot";
|
||||
import { HttpHeaders } from "../Common/Constants";
|
||||
import { Action } from "../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
|
||||
import { updateUserContext } from "../UserContext";
|
||||
import { JupyterLabAppFactory } from "./JupyterLabAppFactory";
|
||||
import { TerminalProps } from "./TerminalProps";
|
||||
import "./index.css";
|
||||
|
||||
let session: ITerminalConnection | undefined;
|
||||
|
||||
const createServerSettings = (props: TerminalProps): ServerConnection.ISettings => {
|
||||
let body: BodyInit | undefined;
|
||||
let headers: HeadersInit | undefined;
|
||||
if (props.terminalEndpoint) {
|
||||
let bodyObj: { endpoint: string; username?: string } = {
|
||||
endpoint: props.terminalEndpoint,
|
||||
};
|
||||
if (props.username) {
|
||||
bodyObj = {
|
||||
...bodyObj,
|
||||
username: props.username,
|
||||
};
|
||||
}
|
||||
body = JSON.stringify(bodyObj);
|
||||
headers = {
|
||||
[HttpHeaders.contentType]: "application/json",
|
||||
};
|
||||
}
|
||||
|
||||
const server = props.notebookServerEndpoint;
|
||||
let options: Partial<ServerConnection.ISettings> = {
|
||||
baseUrl: server,
|
||||
init: { body, headers },
|
||||
fetch: window.parent.fetch,
|
||||
};
|
||||
if (props.authToken) {
|
||||
options = {
|
||||
baseUrl: server,
|
||||
token: props.authToken,
|
||||
appendToken: true,
|
||||
init: { body, headers },
|
||||
fetch: window.parent.fetch,
|
||||
};
|
||||
}
|
||||
|
||||
return ServerConnection.makeSettings(options);
|
||||
};
|
||||
|
||||
const initTerminal = async (props: TerminalProps): Promise<void> => {
|
||||
// Initialize userContext (only properties which are needed by TelemetryProcessor)
|
||||
updateUserContext({
|
||||
subscriptionId: props.subscriptionId,
|
||||
apiType: props.apiType,
|
||||
authType: props.authType,
|
||||
databaseAccount: props.databaseAccount,
|
||||
});
|
||||
|
||||
const serverSettings = createServerSettings(props);
|
||||
|
||||
createTerminalApp(props, serverSettings);
|
||||
};
|
||||
|
||||
const createTerminalApp = async (props: TerminalProps, serverSettings: ServerConnection.ISettings) => {
|
||||
const data = { baseUrl: serverSettings.baseUrl };
|
||||
const startTime = TelemetryProcessor.traceStart(Action.OpenTerminal, data);
|
||||
|
||||
try {
|
||||
session = await new JupyterLabAppFactory((restartShell: boolean) =>
|
||||
closeTab(props, serverSettings, restartShell),
|
||||
).createTerminalApp(serverSettings);
|
||||
TelemetryProcessor.traceSuccess(Action.OpenTerminal, data, startTime);
|
||||
} catch (error) {
|
||||
TelemetryProcessor.traceFailure(Action.OpenTerminal, data, startTime);
|
||||
session = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const closeTab = (props: TerminalProps, serverSettings: ServerConnection.ISettings, restartShell: boolean): void => {
|
||||
if (restartShell) {
|
||||
createTerminalApp(props, serverSettings);
|
||||
} else {
|
||||
window.parent.postMessage(
|
||||
{ type: MessageTypes.CloseTab, data: { tabId: props.tabId }, signature: "pcIframe" },
|
||||
window.document.referrer,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const main = async (): Promise<void> => {
|
||||
postRobot.on(
|
||||
"props",
|
||||
{
|
||||
window: window.parent,
|
||||
domain: window.location.origin,
|
||||
},
|
||||
async (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 TerminalProps;
|
||||
await initTerminal(props);
|
||||
},
|
||||
);
|
||||
|
||||
postRobot.on(
|
||||
"sendMessage",
|
||||
{
|
||||
window: window.parent,
|
||||
domain: window.location.origin,
|
||||
},
|
||||
async (event) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const message = (event as any).data as IMessage;
|
||||
if (session) {
|
||||
session.send(message);
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
window.addEventListener("load", main);
|
||||
@@ -6,7 +6,6 @@
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/DocumentDB/preview/2025-11-01-preview/cosmos-db.json
|
||||
*/
|
||||
|
||||
import { stringifyError } from "Common/stringifyError";
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
import { armRequest } from "../../request";
|
||||
import * as Types from "./types";
|
||||
@@ -19,14 +18,7 @@ export async function listSqlDatabases(
|
||||
accountName: string,
|
||||
): Promise<Types.SqlDatabaseListResult> {
|
||||
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/sqlDatabases`;
|
||||
console.log("{{cdbp}} in listSqlDatabases(): path: " + path);
|
||||
try {
|
||||
console.log("{{cdbp}} in listSqlDatabases(): calling armRequest");
|
||||
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "GET", apiVersion });
|
||||
} catch (error) {
|
||||
console.log("{{cdbp}} in listSqlDatabases(): ERROR: " + stringifyError(error));
|
||||
throw error;
|
||||
}
|
||||
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "GET", apiVersion });
|
||||
}
|
||||
|
||||
/* Gets the SQL database under an existing Azure Cosmos DB database account with the provided name. */
|
||||
|
||||
@@ -5,7 +5,6 @@ Instead, generate ARM clients that consume this function with stricter typing.
|
||||
|
||||
*/
|
||||
|
||||
import { stringifyError } from "Common/stringifyError";
|
||||
import promiseRetry, { AbortError } from "p-retry";
|
||||
import { HttpHeaders } from "../../Common/Constants";
|
||||
import { configContext } from "../../ConfigContext";
|
||||
@@ -78,9 +77,6 @@ export async function armRequestWithoutPolling<T>({
|
||||
}
|
||||
|
||||
if (!userContext?.authorizationToken && !customHeaders?.["Authorization"]) {
|
||||
console.log(
|
||||
"{{cdbp}} in armRequestWithoutPolling(): condition '!userContext?.authorizationToken && !customHeaders?.['Authorization']' met, throwing 'No authority token provided' error",
|
||||
);
|
||||
throw new Error("No authority token provided");
|
||||
}
|
||||
|
||||
@@ -98,9 +94,6 @@ export async function armRequestWithoutPolling<T>({
|
||||
};
|
||||
|
||||
const effectiveTimeoutMs = timeoutMs ?? DEFAULT_ARM_TIMEOUT_MS;
|
||||
console.log(
|
||||
`{{cdbp}} in armRequestWithoutPolling(): calling fetchWithRetry (method=${method}, timeoutMs=${effectiveTimeoutMs}, hasSignal=${!!signal})`,
|
||||
);
|
||||
const response = await fetchWithRetry(url.href, fetchInit, method, effectiveTimeoutMs, signal);
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -115,11 +108,9 @@ export async function armRequestWithoutPolling<T>({
|
||||
error.code = errorResponse.code;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("{{cdbp}} in armRequestWithoutPolling(): ERROR: " + stringifyError(error));
|
||||
throw new Error(await response.text());
|
||||
}
|
||||
|
||||
console.log("{{cdbp}} in armRequestWithoutPolling(): ERROR: " + stringifyError(error));
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -184,7 +175,6 @@ async function fetchWithRetry(
|
||||
(attemptNumber: number) => {
|
||||
const attemptTimeoutMs =
|
||||
timeoutMs * RETRY_TIMEOUT_MULTIPLIERS[Math.min(attemptNumber - 1, RETRY_TIMEOUT_MULTIPLIERS.length - 1)];
|
||||
console.log(`{{cdbp}} in fetchWithRetry(): calling fetchWithTimeout: attempt=${attemptNumber} url=${url}`);
|
||||
return fetchWithTimeout(url, fetchInit, attemptTimeoutMs);
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,26 +1,20 @@
|
||||
import postRobot from "post-robot";
|
||||
import create, { UseStore } from "zustand";
|
||||
|
||||
interface TerminalState {
|
||||
terminalWindow: Window;
|
||||
setTerminal: (terminalWindow: Window) => void;
|
||||
socket: WebSocket | undefined;
|
||||
setSocket: (socket: WebSocket | undefined) => void;
|
||||
sendMessage: (message: string) => void;
|
||||
}
|
||||
|
||||
export const useTerminal: UseStore<TerminalState> = create((set, get) => ({
|
||||
terminalWindow: undefined,
|
||||
setTerminal: (terminalWindow: Window) => {
|
||||
set({ terminalWindow });
|
||||
socket: undefined,
|
||||
setSocket: (socket: WebSocket | undefined) => {
|
||||
set({ socket });
|
||||
},
|
||||
sendMessage: (message: string) => {
|
||||
const terminalWindow = get().terminalWindow;
|
||||
postRobot.send(
|
||||
terminalWindow,
|
||||
"sendMessage",
|
||||
{ type: "stdin", content: [message] },
|
||||
{
|
||||
domain: window.location.origin,
|
||||
},
|
||||
);
|
||||
const socket = get().socket;
|
||||
if (socket && socket.readyState === WebSocket.OPEN) {
|
||||
socket.send(message + "\r");
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
+1
-3
@@ -122,9 +122,6 @@ export async function getTestExplorerUrl(accountType: TestAccount, options?: Tes
|
||||
params.set("feature.enableCopilot", "false");
|
||||
|
||||
const nosqlRbacToken = getNoSqlRbacToken();
|
||||
if (!nosqlRbacToken) {
|
||||
throw new Error("No NOSQL RBAC token found.");
|
||||
}
|
||||
|
||||
const nosqlReadOnlyRbacToken = process.env.NOSQL_READONLY_TESTACCOUNT_TOKEN;
|
||||
const nosqlContainerCopyRbacToken = process.env.NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN;
|
||||
@@ -411,6 +408,7 @@ export enum CommandBarButton {
|
||||
ExecuteQuery = "Execute Query",
|
||||
UploadItem = "Upload Item",
|
||||
NewDocument = "New Document",
|
||||
View = "View",
|
||||
}
|
||||
|
||||
/** Helper class that provides locator methods for DataExplorer components, on top of a Frame */
|
||||
|
||||
@@ -91,3 +91,51 @@ test("Query errors", async () => {
|
||||
await expect(queryTab.errorList.getByTestId("Row:1/Column:code")).toHaveText("SC2005");
|
||||
await expect(queryTab.errorList.getByTestId("Row:1/Column:location")).toHaveText("Line 3");
|
||||
});
|
||||
|
||||
test("View button toggles between vertical and horizontal layout", async () => {
|
||||
// The default layout is Horizontal (Allotment vertical={true} → split-view-vertical class)
|
||||
const allotment = queryTab.locator.locator(".split-view-vertical, .split-view-horizontal");
|
||||
await expect(allotment).toBeAttached();
|
||||
await expect(allotment).toHaveClass(/split-view-vertical/);
|
||||
|
||||
// Click the main View button (not the chevron) to toggle to Vertical layout
|
||||
const viewButton = explorer.commandBarButton(CommandBarButton.View);
|
||||
await viewButton.click();
|
||||
|
||||
// After toggle, Allotment should have split-view-horizontal class (Vertical direction)
|
||||
const allotmentAfterToggle = queryTab.locator.locator(".split-view-vertical, .split-view-horizontal");
|
||||
await expect(allotmentAfterToggle).toHaveClass(/split-view-horizontal/);
|
||||
|
||||
// Click again to toggle back to Horizontal
|
||||
await viewButton.click();
|
||||
const allotmentAfterSecondToggle = queryTab.locator.locator(".split-view-vertical, .split-view-horizontal");
|
||||
await expect(allotmentAfterSecondToggle).toHaveClass(/split-view-vertical/);
|
||||
});
|
||||
|
||||
test("View dropdown allows selecting vertical or horizontal layout", async () => {
|
||||
// The default layout is Horizontal (split-view-vertical)
|
||||
const allotment = queryTab.locator.locator(".split-view-vertical, .split-view-horizontal");
|
||||
await expect(allotment).toHaveClass(/split-view-vertical/);
|
||||
|
||||
// Find the View split button's menu trigger (chevron).
|
||||
// The primary button has data-test, so navigate to the split button container and find the menu button.
|
||||
const viewPrimaryButton = explorer.commandBarButton(CommandBarButton.View);
|
||||
const splitContainer = viewPrimaryButton.locator("..");
|
||||
const menuButton = splitContainer.locator('[aria-haspopup="true"]');
|
||||
await menuButton.click();
|
||||
|
||||
// Select "Vertical" from the dropdown menu
|
||||
await explorer.frame.getByRole("menuitem", { name: "Vertical" }).click();
|
||||
|
||||
// Verify the layout changed to Vertical (split-view-horizontal)
|
||||
const allotmentAfterVertical = queryTab.locator.locator(".split-view-vertical, .split-view-horizontal");
|
||||
await expect(allotmentAfterVertical).toHaveClass(/split-view-horizontal/);
|
||||
|
||||
// Open dropdown again and select "Horizontal"
|
||||
await menuButton.click();
|
||||
await explorer.frame.getByRole("menuitem", { name: "Horizontal" }).click();
|
||||
|
||||
// Verify it reverted to Horizontal (split-view-vertical)
|
||||
const allotmentAfterHorizontal = queryTab.locator.locator(".split-view-vertical, .split-view-horizontal");
|
||||
await expect(allotmentAfterHorizontal).toHaveClass(/split-view-vertical/);
|
||||
});
|
||||
|
||||
@@ -145,7 +145,6 @@
|
||||
"src/Platform/Emulator/**/*",
|
||||
"src/SelfServe/Documentation/**/*",
|
||||
"src/Shared/Telemetry/**/*",
|
||||
"src/Terminal/**/*",
|
||||
"src/Utils/arm/**/*"
|
||||
]
|
||||
}
|
||||
@@ -111,7 +111,6 @@ module.exports = function (_env = {}, argv = {}) {
|
||||
index: "./src/Index.tsx",
|
||||
quickstart: "./src/quickstart.ts",
|
||||
hostedExplorer: "./src/HostedExplorer.tsx",
|
||||
terminal: "./src/Terminal/index.ts",
|
||||
cellOutputViewer: "./src/CellOutputViewer/CellOutputViewer.tsx",
|
||||
selfServe: "./src/SelfServe/SelfServe.tsx",
|
||||
connectToGitHub: "./src/GitHub/GitHubConnector.ts",
|
||||
@@ -128,11 +127,6 @@ module.exports = function (_env = {}, argv = {}) {
|
||||
template: "src/explorer.html",
|
||||
chunks: ["main"],
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
filename: "terminal.html",
|
||||
template: "src/Terminal/index.html",
|
||||
chunks: ["terminal"],
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
filename: "quickstart.html",
|
||||
template: "src/quickstart.html",
|
||||
|
||||
Reference in New Issue
Block a user