This commit is contained in:
Senthamil Sindhu 2024-06-14 12:18:14 -07:00
commit 4d8bb5c3ea
196 changed files with 16322 additions and 7450 deletions

10
.editorconfig Normal file
View File

@ -0,0 +1,10 @@
# NOTE: Prettier reads EditorConfig settings, so be careful adjusting settings here and assuming they'll only affect your editor ;).
# top-most EditorConfig file
root = true
[*.yml]
indent_size = 2
[*.{js,jsx,ts,tsx}]
indent_size = 2

View File

@ -1,3 +1,5 @@
playwright.config.ts
**/node_modules/ **/node_modules/
src/**/__mocks__/**/* src/**/__mocks__/**/*
dist/ dist/
@ -89,10 +91,7 @@ src/Explorer/Tables/TableEntityProcessor.ts
src/Explorer/Tables/Utilities.ts src/Explorer/Tables/Utilities.ts
src/Explorer/Tabs/ConflictsTab.ts src/Explorer/Tabs/ConflictsTab.ts
src/Explorer/Tabs/DatabaseSettingsTab.ts src/Explorer/Tabs/DatabaseSettingsTab.ts
src/Explorer/Tabs/DocumentsTab.test.ts
src/Explorer/Tabs/DocumentsTab.ts
src/Explorer/Tabs/GraphTab.ts src/Explorer/Tabs/GraphTab.ts
src/Explorer/Tabs/MongoDocumentsTab.ts
src/Explorer/Tabs/NotebookV2Tab.ts src/Explorer/Tabs/NotebookV2Tab.ts
src/Explorer/Tabs/ScriptTabBase.ts src/Explorer/Tabs/ScriptTabBase.ts
src/Explorer/Tabs/TabComponents.ts src/Explorer/Tabs/TabComponents.ts
@ -128,7 +127,7 @@ src/Explorer/Controls/InputTypeahead/InputTypeaheadComponent.tsx
src/Explorer/Controls/Notebook/NotebookTerminalComponent.test.tsx src/Explorer/Controls/Notebook/NotebookTerminalComponent.test.tsx
src/Explorer/Controls/Notebook/NotebookTerminalComponent.tsx src/Explorer/Controls/Notebook/NotebookTerminalComponent.tsx
src/Explorer/Controls/NotebookViewer/NotebookViewerComponent.tsx src/Explorer/Controls/NotebookViewer/NotebookViewerComponent.tsx
src/Explorer/Controls/TreeComponent/TreeComponent.tsx src/Explorer/Controls/TreeComponent/LegacyTreeComponent.tsx
src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.test.tsx src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.test.tsx
src/Explorer/Graph/GraphExplorerComponent/NodePropertiesComponent.tsx src/Explorer/Graph/GraphExplorerComponent/NodePropertiesComponent.tsx
src/Explorer/Graph/GraphExplorerComponent/ReadOnlyNodePropertiesComponent.test.tsx src/Explorer/Graph/GraphExplorerComponent/ReadOnlyNodePropertiesComponent.test.tsx

View File

@ -8,6 +8,9 @@ on:
pull_request: pull_request:
branches: branches:
- master - master
permissions:
id-token: write
contents: read
jobs: jobs:
codemetrics: codemetrics:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -101,72 +104,6 @@ jobs:
run: az storage blob upload -c '$web' -f ./preview/config.json --account-name cosmosexplorerpreview --name "${{github.event.pull_request.head.sha || github.sha}}/config.json" --account-key="${PREVIEW_STORAGE_KEY}" --overwrite true run: az storage blob upload -c '$web' -f ./preview/config.json --account-name cosmosexplorerpreview --name "${{github.event.pull_request.head.sha || github.sha}}/config.json" --account-key="${PREVIEW_STORAGE_KEY}" --overwrite true
env: env:
PREVIEW_STORAGE_KEY: ${{ secrets.PREVIEW_STORAGE_KEY }} PREVIEW_STORAGE_KEY: ${{ secrets.PREVIEW_STORAGE_KEY }}
endtoendemulator:
name: "End To End Emulator Tests"
# Temporarily disabled. This test needs to be rewritten in playwright
if: false
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Use Node.js 18.x
uses: actions/setup-node@v4
with:
node-version: 18.x
- uses: southpolesteve/cosmos-emulator-github-action@v1
- name: End to End Tests
run: |
npm ci
npm start &
npm run wait-for-server
npx jest -c ./jest.config.e2e.js --detectOpenHandles test/sql/container.spec.ts
shell: bash
env:
DATA_EXPLORER_ENDPOINT: "https://localhost:1234/explorer.html?platform=Emulator"
PLATFORM: "Emulator"
NODE_TLS_REJECT_UNAUTHORIZED: 0
- uses: actions/upload-artifact@v3
if: failure()
with:
name: screenshots
path: failed-*
endtoend:
name: "E2E"
runs-on: ubuntu-latest
env:
NODE_TLS_REJECT_UNAUTHORIZED: 0
NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET }}
strategy:
fail-fast: false
matrix:
test-file:
- ./test/cassandra/container.spec.ts
- ./test/graph/container.spec.ts
- ./test/sql/container.spec.ts
- ./test/mongo/container.spec.ts
- ./test/mongo/container32.spec.ts
- ./test/selfServe/selfServeExample.spec.ts
# - ./test/notebooks/upload.spec.ts // TEMP disabled since notebooks service is off
- ./test/sql/resourceToken.spec.ts
- ./test/tables/container.spec.ts
steps:
- uses: actions/checkout@v4
- name: Use Node.js 18.x
uses: actions/setup-node@v4
with:
node-version: 18.x
- run: npm ci
- run: npm start &
- run: npm run wait-for-server
- name: ${{ matrix['test-file'] }}
run: |
# Run tests up to three times
for i in $(seq 1 3); do npx jest -c ./jest.config.playwright.js ${{ matrix['test-file'] }} && s=0 && break || s=$? && sleep 1; done; (exit $s)
shell: bash
- uses: actions/upload-artifact@v3
if: failure()
with:
name: screenshots
path: screenshots/
nuget: nuget:
name: Publish Nuget name: Publish Nuget
if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/') if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')
@ -216,3 +153,70 @@ jobs:
name: packages name: packages
with: with:
path: "*.nupkg" path: "*.nupkg"
playwright-tests:
name: "Run Playwright Tests (Shard ${{ matrix.shardIndex }} of ${{ matrix.shardTotal }})"
runs-on: ubuntu-latest
env:
NODE_TLS_REJECT_UNAUTHORIZED: 0
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
strategy:
fail-fast: false
matrix:
shardIndex: [1, 2, 3, 4, 5, 6, 7, 8]
shardTotal: [8]
steps:
- uses: actions/checkout@v4
- name: "Az CLI login"
uses: azure/login@v1
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Use Node.js 18.x
uses: actions/setup-node@v4
with:
node-version: 18.x
- run: npm ci
- run: npx playwright install --with-deps
- name: Run test shard ${{ matrix['shardIndex'] }} of ${{ matrix['shardTotal']}}
run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
- name: Upload blob report to GitHub Actions Artifacts
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4
with:
name: blob-report-${{ matrix.shardIndex }}
path: blob-report
retention-days: 1
merge-playwright-reports:
name: "Merge Playwright Reports"
# Merge reports after playwright-tests, even if some shards have failed
if: ${{ !cancelled() }}
needs: [playwright-tests]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 18
- name: Install dependencies
run: npm ci
- name: Download blob reports from GitHub Actions Artifacts
uses: actions/download-artifact@v4
with:
path: all-blob-reports
pattern: blob-report-*
merge-multiple: true
- 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

View File

@ -9,6 +9,10 @@ on:
# Once every hour # Once every hour
- cron: "0 15 * * *" - cron: "0 15 * * *"
permissions:
id-token: write
contents: read
# A workflow run is made up of one or more jobs that can run sequentially or in parallel # A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs: jobs:
# This workflow contains a single job called "build" # This workflow contains a single job called "build"
@ -16,10 +20,17 @@ jobs:
name: "Cleanup Test Database Accounts" name: "Cleanup Test Database Accounts"
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
NOTEBOOKS_TEST_RUNNER_CLIENT_ID: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_ID }} AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET }}
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: "Az CLI login"
uses: azure/login@v1
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Use Node.js 18.x - name: Use Node.js 18.x
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:

6
.gitignore vendored
View File

@ -16,4 +16,8 @@ Contracts/*
.env .env
failure.png failure.png
screenshots/* screenshots/*
GettingStarted-ignore*.ipynb GettingStarted-ignore*.ipynb
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/

View File

@ -1,13 +0,0 @@
const isCI = require("is-ci");
module.exports = {
exitOnPageError: false,
launchOptions: {
headless: isCI,
slowMo: 10,
timeout: 60000,
},
contextOptions: {
ignoreHTTPSErrors: true,
},
};

View File

@ -31,7 +31,7 @@ module.exports = {
coveragePathIgnorePatterns: ["/node_modules/"], coveragePathIgnorePatterns: ["/node_modules/"],
// A list of reporter names that Jest uses when writing coverage reports // A list of reporter names that Jest uses when writing coverage reports
coverageReporters: ["json", "text", "cobertura"], coverageReporters: ["json", "text", "cobertura", "lcov"],
// An object that configures minimum threshold enforcement for coverage results // An object that configures minimum threshold enforcement for coverage results
coverageThreshold: { coverageThreshold: {
@ -76,6 +76,10 @@ module.exports = {
"^dnd-core$": "dnd-core/dist/cjs", "^dnd-core$": "dnd-core/dist/cjs",
"^react-dnd$": "react-dnd/dist/cjs", "^react-dnd$": "react-dnd/dist/cjs",
"^react-dnd-html5-backend$": "react-dnd-html5-backend/dist/cjs", "^react-dnd-html5-backend$": "react-dnd-html5-backend/dist/cjs",
"d3-force": "<rootDir>/node_modules/d3-force/dist/d3-force.min.js",
"d3-quadtree": "<rootDir>/node_modules/d3-quadtree/dist/d3-quadtree.min.js",
"d3-scale-chromatic": "<rootDir>/node_modules/d3-scale-chromatic/dist/d3-scale-chromatic.min.js",
"d3-zoom": "<rootDir>/node_modules/d3-zoom/dist/d3-zoom.min.js",
}, },
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
@ -130,7 +134,6 @@ module.exports = {
// The test environment that will be used for testing // The test environment that will be used for testing
// testEnvironment: "jest-environment-jsdom", // testEnvironment: "jest-environment-jsdom",
modulePaths: ["node_modules", "<rootDir>/src"], modulePaths: ["node_modules", "<rootDir>/src"],
// Options that will be passed to the testEnvironment // Options that will be passed to the testEnvironment

View File

@ -1,7 +0,0 @@
module.exports = {
preset: "jest-playwright-preset",
testMatch: ["<rootDir>/test/**/*.spec.[jt]s?(x)"],
setupFiles: ["dotenv/config"],
testEnvironment: "./test/playwrightEnv.js",
setupFilesAfterEnv: ["expect-playwright"],
};

View File

@ -130,6 +130,7 @@
@ActiveTabWidth: 141px; @ActiveTabWidth: 141px;
@TabsHeight: 30px; @TabsHeight: 30px;
@TabsWidth: 140px; @TabsWidth: 140px;
@ContentWrapper: 111px;
@StatusIconContainerSize: 18px; @StatusIconContainerSize: 18px;
@LoadingErrorIconSize: 14px; @LoadingErrorIconSize: 14px;
@ErrorIconContainer: 16px; @ErrorIconContainer: 16px;
@ -335,4 +336,11 @@
width: 0; width: 0;
height: 0; height: 0;
border-color: @InfoPointerColor transparent; border-color: @InfoPointerColor transparent;
}
/*********************************************************************************************************
Screen Reader Only
**********************************************************************************************************/
.screenReaderOnly {
position: absolute;
left: -9999px;
} }

View File

@ -2264,33 +2264,33 @@ a:link {
width: 82px; width: 82px;
} }
.tabdocuments .scrollable { // .tabdocuments .scrollable {
height: 100%; // height: 100%;
overflow-y: auto; // overflow-y: auto;
overflow-x: hidden; // overflow-x: hidden;
padding-left: 5px; // padding-left: 5px;
padding-right: 5px; // padding-right: 5px;
width: 100%; // width: 100%;
} // }
.tabdocuments > .tabdocumentsGridElement { // .tabdocuments > .tabdocumentsGridElement {
width: 50%; // width: 50%;
} // }
.tabdocuments > .evenlySpacedHeader { // .tabdocuments > .evenlySpacedHeader {
width: 30%; // width: 30%;
} // }
.tabdocuments.scrollable:focus, // .tabdocuments.scrollable:focus,
.tabdocuments.scrollable:active { // .tabdocuments.scrollable:active {
outline: 1px dotted; // outline: 1px dotted;
} // }
.tabdocuments .scrollable table td { // .tabdocuments .scrollable table td {
white-space: nowrap; // white-space: nowrap;
overflow: hidden; // overflow: hidden;
text-overflow: ellipsis; // text-overflow: ellipsis;
} // }
.mongoDocumentEditor .monaco-editor.vs .redsquiggly { .mongoDocumentEditor .monaco-editor.vs .redsquiggly {
display: none !important; display: none !important;
@ -2316,10 +2316,9 @@ td a:hover {
} }
.loadMore { .loadMore {
display: block;
width: 100%; width: 100%;
padding-left: 30%; text-align: center;
padding-top: 2px;
cursor: pointer;
} }
.loadMore > a:focus { .loadMore > a:focus {
@ -2367,9 +2366,9 @@ a:link {
.tabsManagerContainer { .tabsManagerContainer {
height: 100%; height: 100%;
flex-grow: 1; flex-grow: 1;
overflow: hidden; display: flex;
flex-direction: column;
min-height: 300px; min-height: 300px;
overflow-y: scroll;
} }
.tabs { .tabs {
@ -2558,10 +2557,12 @@ a:link {
} }
.filterdivs { .filterdivs {
padding-top: 15px; margin: 10px 0px;
height: 45px;
margin-bottom: 8px;
white-space: nowrap; white-space: nowrap;
input {
line-height: 14px; // To correct vertical position of the down arrow of the input
outline: none; // Remove the dotted border on focus, because fluent has its own focus style (underlined)
}
} }
.editFilterContainer { .editFilterContainer {
@ -2578,6 +2579,18 @@ a:link {
cursor: pointer; cursor: pointer;
} }
.documentsTab {
.documentsTable {
.documentsTableCell {
border-left: 1px solid @BaseMedium;
height: 100%;
}
.documentsTableHeader {
border-bottom: 1px solid @BaseMedium;
}
}
}
.querydropdown { .querydropdown {
border: 1px solid @BaseMedium; border: 1px solid @BaseMedium;
font-style: normal; font-style: normal;
@ -2658,7 +2671,7 @@ a:link {
width: @ActiveTabWidth; width: @ActiveTabWidth;
} }
.nav-tabs > li.active > .tabNavContentContainer > .tab_Content > .tabNavText { .nav-tabs > li.active > .tabNavContentContainer > .tab_Content > .contentWrapper > .tabNavText {
font-weight: bolder; font-weight: bolder;
border-bottom: 2px solid rgba(0, 120, 212, 1); border-bottom: 2px solid rgba(0, 120, 212, 1);
} }
@ -2694,67 +2707,71 @@ a:link {
width: @TabsWidth; width: @TabsWidth;
border-right: @ButtonBorderWidth solid @BaseMedium; border-right: @ButtonBorderWidth solid @BaseMedium;
white-space: nowrap; white-space: nowrap;
.contentWrapper {
.flex-display();
width: @ContentWrapper;
.statusIconContainer { .statusIconContainer {
width: @StatusIconContainerSize; width: @StatusIconContainerSize;
height: @StatusIconContainerSize; height: @StatusIconContainerSize;
margin-left: @SmallSpace; margin-left: @SmallSpace;
display: inline-flex; display: inline-flex;
.errorIconContainer { .errorIconContainer {
width: @ErrorIconContainer; width: @ErrorIconContainer;
height: @ErrorIconContainer; height: @ErrorIconContainer;
margin-top: 1px; margin-top: 1px;
.errorIcon { .errorIcon {
width: @ErrorIconWidth; width: @ErrorIconWidth;
height: @LoadingErrorIconSize;
background-image: url(../images/error_no_outline.svg);
background-repeat: no-repeat;
background-position: center;
background-size: 3px;
display: block;
margin: 1px 0px 0px 6px;
}
}
.errorIconContainer.actionsEnabled {
&:hover {
.hover();
}
&:focus {
.focus();
}
&:active {
.active();
}
}
.errorIconContainer[tabindex]:active {
outline: none;
}
.loadingIcon {
width: @LoadingErrorIconSize;
height: @LoadingErrorIconSize; height: @LoadingErrorIconSize;
background-image: url(../images/error_no_outline.svg); margin: 0px 0px @SmallSpace @SmallSpace;
background-repeat: no-repeat;
background-position: center;
background-size: 3px;
display: block;
margin: 1px 0px 0px 6px;
} }
} }
.errorIconContainer.actionsEnabled { .tabNavText {
&:hover { margin-left: @SmallSpace;
.hover(); margin-right: 2px;
} color: @BaseDark;
text-overflow: ellipsis;
&:focus { overflow: hidden;
.focus(); white-space: nowrap;
} flex-grow: 1;
&:active {
.active();
}
} }
.errorIconContainer[tabindex]:active {
outline: none;
}
.loadingIcon {
width: @LoadingErrorIconSize;
height: @LoadingErrorIconSize;
margin: 0px 0px @SmallSpace @SmallSpace;
}
}
.tabNavText {
margin-left: @SmallSpace;
margin-right: 2px;
color: @BaseDark;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
flex-grow: 1;
} }
.tabIconSection { .tabIconSection {
width: 30px; width: 29px;
position: relative; position: relative;
padding-top: 2px; padding-top: 2px;

View File

@ -75,7 +75,7 @@ a:focus {
border-bottom: 2px solid @FabricAccentMedium; border-bottom: 2px solid @FabricAccentMedium;
} }
.nav-tabs>li.active>.tabNavContentContainer>.tab_Content>.tabNavText { .nav-tabs>li.active>.tabNavContentContainer>.tab_Content>.contentWrapper>.tabNavText {
border-bottom: 0px none transparent; border-bottom: 0px none transparent;
} }
@ -93,9 +93,11 @@ a:focus {
width: calc(@TabsWidth - (@SmallSpace * 2)); width: calc(@TabsWidth - (@SmallSpace * 2));
padding-bottom: @SmallSpace; padding-bottom: @SmallSpace;
.statusIconContainer { .contentWrapper {
margin-left: 0px; .statusIconContainer {
} margin-left: 0px;
}
}
.tabIconSection { .tabIconSection {
.cancelButton { .cancelButton {

3985
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -5,10 +5,10 @@
"main": "index.js", "main": "index.js",
"dependencies": { "dependencies": {
"@azure/arm-cosmosdb": "9.1.0", "@azure/arm-cosmosdb": "9.1.0",
"@azure/cosmos": "4.0.1-beta.2", "@azure/cosmos": "4.0.1-beta.3",
"@azure/cosmos-language-service": "0.0.5", "@azure/cosmos-language-service": "0.0.5",
"@azure/identity": "1.2.1", "@azure/identity": "1.5.2",
"@azure/ms-rest-nodeauth": "3.0.7", "@azure/ms-rest-nodeauth": "3.1.1",
"@azure/msal-browser": "2.14.2", "@azure/msal-browser": "2.14.2",
"@babel/plugin-proposal-class-properties": "7.12.1", "@babel/plugin-proposal-class-properties": "7.12.1",
"@babel/plugin-proposal-decorators": "7.12.12", "@babel/plugin-proposal-decorators": "7.12.12",
@ -46,6 +46,8 @@
"@types/lodash": "4.14.171", "@types/lodash": "4.14.171",
"@types/mkdirp": "1.0.1", "@types/mkdirp": "1.0.1",
"@types/node-fetch": "2.5.7", "@types/node-fetch": "2.5.7",
"@uiw/react-split": "5.9.3",
"@xmldom/xmldom": "0.7.13",
"applicationinsights": "1.8.0", "applicationinsights": "1.8.0",
"bootstrap": "3.4.1", "bootstrap": "3.4.1",
"canvas": "file:./canvas", "canvas": "file:./canvas",
@ -54,7 +56,7 @@
"copy-webpack-plugin": "11.0.0", "copy-webpack-plugin": "11.0.0",
"crossroads": "0.12.2", "crossroads": "0.12.2",
"css-element-queries": "1.1.1", "css-element-queries": "1.1.1",
"d3": "6.1.1", "d3": "7.8.5",
"datatables.net-colreorder-dt": "1.7.0", "datatables.net-colreorder-dt": "1.7.0",
"datatables.net-dt": "1.13.8", "datatables.net-dt": "1.13.8",
"date-fns": "1.29.0", "date-fns": "1.29.0",
@ -69,12 +71,14 @@
"i18next-browser-languagedetector": "6.0.1", "i18next-browser-languagedetector": "6.0.1",
"i18next-http-backend": "1.0.23", "i18next-http-backend": "1.0.23",
"iframe-resizer-react": "1.1.0", "iframe-resizer-react": "1.1.0",
"immer": "9.0.6",
"immutable": "4.0.0-rc.12", "immutable": "4.0.0-rc.12",
"is-ci": "2.0.0", "is-ci": "2.0.0",
"jquery": "3.7.1", "jquery": "3.7.1",
"jquery-typeahead": "2.11.1", "jquery-typeahead": "2.11.1",
"jquery-ui-dist": "1.13.2", "jquery-ui-dist": "1.13.2",
"knockout": "3.5.1", "knockout": "3.5.1",
"loader-utils": "2.0.3",
"mkdirp": "1.0.4", "mkdirp": "1.0.4",
"monaco-editor": "0.44.0", "monaco-editor": "0.44.0",
"ms": "2.1.3", "ms": "2.1.3",
@ -95,13 +99,16 @@
"react-splitter-layout": "4.0.0", "react-splitter-layout": "4.0.0",
"react-string-format": "1.0.1", "react-string-format": "1.0.1",
"react-youtube": "9.0.1", "react-youtube": "9.0.1",
"react-window": "1.8.10",
"reflect-metadata": "0.1.13", "reflect-metadata": "0.1.13",
"rx-jupyter": "5.5.12", "rx-jupyter": "5.5.12",
"sanitize-html": "2.3.3", "sanitize-html": "2.3.3",
"shell-quote": "1.7.3",
"styled-components": "5.0.1", "styled-components": "5.0.1",
"swr": "0.4.0", "swr": "0.4.0",
"terser-webpack-plugin": "5.3.9", "terser-webpack-plugin": "5.3.9",
"underscore": "1.9.1", "tinykeys": "2.1.0",
"underscore": "1.12.1",
"utility-types": "3.10.0", "utility-types": "3.10.0",
"zustand": "3.5.0" "zustand": "3.5.0"
}, },
@ -110,6 +117,7 @@
"@babel/preset-env": "7.9.0", "@babel/preset-env": "7.9.0",
"@babel/preset-react": "7.9.4", "@babel/preset-react": "7.9.4",
"@babel/preset-typescript": "7.9.0", "@babel/preset-typescript": "7.9.0",
"@playwright/test": "1.44.0",
"@testing-library/react": "11.2.3", "@testing-library/react": "11.2.3",
"@types/applicationinsights-js": "1.0.7", "@types/applicationinsights-js": "1.0.7",
"@types/codemirror": "0.0.56", "@types/codemirror": "0.0.56",
@ -118,8 +126,8 @@
"@types/datatables.net": "1.10.28", "@types/datatables.net": "1.10.28",
"@types/datatables.net-colreorder": "1.4.5", "@types/datatables.net-colreorder": "1.4.5",
"@types/dom-to-image": "2.6.2", "@types/dom-to-image": "2.6.2",
"@types/enzyme": "3.10.7", "@types/enzyme": "3.10.12",
"@types/enzyme-adapter-react-16": "1.0.6", "@types/enzyme-adapter-react-16": "1.0.9",
"@types/hasher": "0.0.31", "@types/hasher": "0.0.31",
"@types/jest": "26.0.20", "@types/jest": "26.0.20",
"@types/jquery": "3.5.29", "@types/jquery": "3.5.29",
@ -131,6 +139,7 @@
"@types/react-notification-system": "0.2.39", "@types/react-notification-system": "0.2.39",
"@types/react-redux": "7.1.7", "@types/react-redux": "7.1.7",
"@types/react-splitter-layout": "3.0.1", "@types/react-splitter-layout": "3.0.1",
"@types/react-window": "1.8.8",
"@types/sanitize-html": "1.27.2", "@types/sanitize-html": "1.27.2",
"@types/sinon": "2.3.3", "@types/sinon": "2.3.3",
"@types/styled-components": "5.1.1", "@types/styled-components": "5.1.1",
@ -146,14 +155,13 @@
"create-file-webpack": "1.0.2", "create-file-webpack": "1.0.2",
"css-loader": "6.8.1", "css-loader": "6.8.1",
"enzyme": "3.11.0", "enzyme": "3.11.0",
"enzyme-adapter-react-16": "1.15.5", "enzyme-adapter-react-16": "1.15.8",
"enzyme-to-json": "3.6.1", "enzyme-to-json": "3.6.2",
"eslint": "8.50.0", "eslint": "8.50.0",
"eslint-cli": "1.1.1", "eslint-cli": "1.1.1",
"eslint-plugin-no-null": "1.0.2", "eslint-plugin-no-null": "1.0.2",
"eslint-plugin-prefer-arrow": "1.2.3", "eslint-plugin-prefer-arrow": "1.2.3",
"eslint-plugin-react-hooks": "4.6.0", "eslint-plugin-react-hooks": "4.6.0",
"expect-playwright": "0.3.3",
"fast-glob": "3.2.5", "fast-glob": "3.2.5",
"fs-extra": "7.0.0", "fs-extra": "7.0.0",
"html-inline-css-webpack-plugin": "1.11.2", "html-inline-css-webpack-plugin": "1.11.2",
@ -162,7 +170,6 @@
"html-webpack-plugin": "5.5.3", "html-webpack-plugin": "5.5.3",
"jest": "26.6.3", "jest": "26.6.3",
"jest-canvas-mock": "2.3.1", "jest-canvas-mock": "2.3.1",
"jest-playwright-preset": "1.5.1",
"jest-react-hooks-shallow": "1.5.1", "jest-react-hooks-shallow": "1.5.1",
"jest-trx-results-processor": "0.0.7", "jest-trx-results-processor": "0.0.7",
"less": "3.8.1", "less": "3.8.1",
@ -170,25 +177,24 @@
"less-vars-loader": "1.1.0", "less-vars-loader": "1.1.0",
"mini-css-extract-plugin": "2.1.0", "mini-css-extract-plugin": "2.1.0",
"monaco-editor-webpack-plugin": "7.1.0", "monaco-editor-webpack-plugin": "7.1.0",
"node-fetch": "2.6.1", "node-fetch": "2.6.7",
"playwright": "1.13.0",
"prettier": "3.0.3", "prettier": "3.0.3",
"process": "0.11.10", "process": "0.11.10",
"querystring-es3": "0.2.1", "querystring-es3": "0.2.1",
"raw-loader": "0.5.1", "raw-loader": "0.5.1",
"react-dev-utils": "11.0.4", "react-dev-utils": "12.0.1",
"rimraf": "3.0.0", "rimraf": "3.0.0",
"sinon": "3.2.1", "sinon": "3.2.1",
"style-loader": "0.23.0", "style-loader": "0.23.0",
"ts-loader": "9.2.4", "ts-loader": "9.2.4",
"typedoc": "0.21.5", "typedoc": "0.22.15",
"typescript": "4.3.5", "typescript": "4.3.5",
"url-loader": "4.1.1", "url-loader": "4.1.1",
"wait-on": "4.0.2", "wait-on": "4.0.2",
"webpack": "5.88.2", "webpack": "5.88.2",
"webpack-bundle-analyzer": "4.9.1", "webpack-bundle-analyzer": "4.9.1",
"webpack-cli": "5.1.4", "webpack-cli": "5.1.4",
"webpack-dev-server": "4.15.1" "webpack-dev-server": "4.15.2"
}, },
"scripts": { "scripts": {
"postinstall": "patch-package", "postinstall": "patch-package",
@ -203,6 +209,7 @@
"test": "rimraf coverage && jest", "test": "rimraf coverage && jest",
"test:debug": "jest --runInBand", "test:debug": "jest --runInBand",
"test:e2e": "jest -c ./jest.config.playwright.js --detectOpenHandles", "test:e2e": "jest -c ./jest.config.playwright.js --detectOpenHandles",
"test:file": "jest --coverage=false",
"watch": "npm run start", "watch": "npm run start",
"wait-for-server": "wait-on -t 240000 -i 5000 -v https-get://0.0.0.0:1234/", "wait-for-server": "wait-on -t 240000 -i 5000 -v https-get://0.0.0.0:1234/",
"build:ase": "gulp build:ase", "build:ase": "gulp build:ase",

View File

@ -0,0 +1,11 @@
diff --git a/node_modules/@uiw/react-split/cjs/index.d.ts b/node_modules/@uiw/react-split/cjs/index.d.ts
index 644bcc3..f794760 100644
--- a/node_modules/@uiw/react-split/cjs/index.d.ts
+++ b/node_modules/@uiw/react-split/cjs/index.d.ts
@@ -56,5 +56,5 @@ export default class Split extends React.Component<SplitProps, SplitState> {
onMouseDown(paneNumber: number, env: React.MouseEvent<HTMLDivElement, MouseEvent>): void;
onDragging(env: Event): void;
onDragEnd(): void;
- render(): import("react/jsx-runtime").JSX.Element;
+ render(): JSX.Element;
}

53
playwright.config.ts Normal file
View File

@ -0,0 +1,53 @@
import { defineConfig, devices } from '@playwright/test';
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: 'test',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 3 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: process.env.CI ? 'blob' : 'html',
timeout: 10 * 60 * 1000,
use: {
actionTimeout: 5 * 60 * 1000,
trace: 'off',
video: 'off',
screenshot: 'on',
testIdAttribute: 'data-test',
contextOptions: {
ignoreHTTPSErrors: true,
},
},
expect: {
timeout: 5 * 60 * 1000,
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
],
webServer: {
command: 'npm run start',
url: 'https://127.0.0.1:1234/_ready',
timeout: 120 * 1000,
ignoreHTTPSErrors: true,
reuseExistingServer: !process.env.CI,
},
});

View File

@ -88,6 +88,12 @@ export class CapabilityNames {
public static readonly EnableStorageAnalytics: string = "EnableStorageAnalytics"; public static readonly EnableStorageAnalytics: string = "EnableStorageAnalytics";
public static readonly EnableMongo: string = "EnableMongo"; public static readonly EnableMongo: string = "EnableMongo";
public static readonly EnableServerless: string = "EnableServerless"; public static readonly EnableServerless: string = "EnableServerless";
public static readonly EnableNoSQLVectorSearch: string = "EnableNoSQLVectorSearch";
}
export enum CapacityMode {
Provisioned = "Provisioned",
Serverless = "Serverless",
} }
// flight names returned from the portal are always lowercase // flight names returned from the portal are always lowercase
@ -138,7 +144,7 @@ export class PortalBackendEndpoints {
} }
export class MongoProxyEndpoints { export class MongoProxyEndpoints {
public static readonly Development: string = "https://localhost:7238"; public static readonly Local: string = "https://localhost:7238";
public static readonly Mpac: string = "https://cdb-ms-mpac-mp.cosmos.azure.com"; public static readonly Mpac: string = "https://cdb-ms-mpac-mp.cosmos.azure.com";
public static readonly Prod: string = "https://cdb-ms-prod-mp.cosmos.azure.com"; public static readonly Prod: string = "https://cdb-ms-prod-mp.cosmos.azure.com";
public static readonly Fairfax: string = "https://cdb-ff-prod-mp.cosmos.azure.us"; public static readonly Fairfax: string = "https://cdb-ff-prod-mp.cosmos.azure.us";
@ -249,6 +255,7 @@ export class HttpHeaders {
public static partitionKey: string = "x-ms-documentdb-partitionkey"; public static partitionKey: string = "x-ms-documentdb-partitionkey";
public static migrateOfferToManualThroughput: string = "x-ms-cosmos-migrate-offer-to-manual-throughput"; public static migrateOfferToManualThroughput: string = "x-ms-cosmos-migrate-offer-to-manual-throughput";
public static migrateOfferToAutopilot: string = "x-ms-cosmos-migrate-offer-to-autopilot"; public static migrateOfferToAutopilot: string = "x-ms-cosmos-migrate-offer-to-autopilot";
public static xAPIKey: string = "X-API-Key";
} }
export class ContentType { export class ContentType {

View File

@ -1,6 +1,6 @@
import * as Cosmos from "@azure/cosmos"; import * as Cosmos from "@azure/cosmos";
import { getAuthorizationTokenUsingResourceTokens } from "Common/getAuthorizationTokenUsingResourceTokens"; import { getAuthorizationTokenUsingResourceTokens } from "Common/getAuthorizationTokenUsingResourceTokens";
import { AuthorizationToken } from "Contracts/MessageTypes"; import { AuthorizationToken } from "Contracts/FabricMessageTypes";
import { checkDatabaseResourceTokensValidity } from "Platform/Fabric/FabricUtil"; import { checkDatabaseResourceTokensValidity } from "Platform/Fabric/FabricUtil";
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility"; import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
import { AuthType } from "../AuthType"; import { AuthType } from "../AuthType";
@ -59,7 +59,7 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
/* ************** TODO: Uncomment this code if we need to support these operations ************** /* ************** TODO: Uncomment this code if we need to support these operations **************
// User master tokens // User master tokens
const authorizationToken = await sendCachedDataMessage<AuthorizationToken>( const authorizationToken = await sendCachedDataMessage<AuthorizationToken>(
MessageTypes.GetAuthorizationToken, FabricMessageTypes.GetAuthorizationToken,
[requestInfo], [requestInfo],
userContext.fabricContext.connectionId, userContext.fabricContext.connectionId,
); );

View File

@ -1,9 +1,9 @@
import { userContext } from "../UserContext"; import { userContext } from "../UserContext";
export const getEntityName = (): string => { export const getEntityName = (multiple?: boolean): string => {
if (userContext.apiType === "Mongo") { if (userContext.apiType === "Mongo") {
return "document"; return multiple ? "documents" : "document";
} }
return "item"; return multiple ? "items" : "item";
}; };

View File

@ -1,3 +1,4 @@
import { FabricMessageTypes } from "Contracts/FabricMessageTypes";
import Q from "q"; import Q from "q";
import * as _ from "underscore"; import * as _ from "underscore";
import { MessageTypes } from "../Contracts/ExplorerContracts"; import { MessageTypes } from "../Contracts/ExplorerContracts";
@ -36,7 +37,7 @@ export function handleCachedDataMessage(message: any): void {
* @returns * @returns
*/ */
export function sendCachedDataMessage<TResponseDataModel>( export function sendCachedDataMessage<TResponseDataModel>(
messageType: MessageTypes, messageType: MessageTypes | FabricMessageTypes,
params: Object[], params: Object[],
scope?: string, scope?: string,
timeoutInMs?: number, timeoutInMs?: number,

View File

@ -672,6 +672,28 @@ export function getEndpoint(endpoint: string): string {
return url; return url;
} }
export function useMongoProxyEndpoint(api: string): boolean {
const activeMongoProxyEndpoints: string[] = [
MongoProxyEndpoints.Local,
MongoProxyEndpoints.Mpac,
MongoProxyEndpoints.Prod,
MongoProxyEndpoints.Fairfax,
];
let canAccessMongoProxy: boolean = userContext.databaseAccount.properties.publicNetworkAccess === "Enabled";
if (
configContext.MONGO_PROXY_ENDPOINT !== MongoProxyEndpoints.Local &&
userContext.databaseAccount.properties.ipRules?.length > 0
) {
canAccessMongoProxy = canAccessMongoProxy && configContext.MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED;
}
return (
canAccessMongoProxy &&
configContext.NEW_MONGO_APIS?.includes(api) &&
activeMongoProxyEndpoints.includes(configContext.MONGO_PROXY_ENDPOINT)
);
}
// TODO: This function throws most of the time except on Forbidden which is a bit strange // TODO: This function throws most of the time except on Forbidden which is a bit strange
// It causes problems for TypeScript understanding the types // It causes problems for TypeScript understanding the types
async function errorHandling(response: Response, action: string, params: unknown): Promise<void> { async function errorHandling(response: Response, action: string, params: unknown): Promise<void> {
@ -688,17 +710,3 @@ async function errorHandling(response: Response, action: string, params: unknown
export function getARMCreateCollectionEndpoint(params: DataModels.MongoParameters): string { export function getARMCreateCollectionEndpoint(params: DataModels.MongoParameters): string {
return `subscriptions/${params.sid}/resourceGroups/${params.rg}/providers/Microsoft.DocumentDB/databaseAccounts/${userContext.databaseAccount.name}/mongodbDatabases/${params.db}/collections/${params.coll}`; return `subscriptions/${params.sid}/resourceGroups/${params.rg}/providers/Microsoft.DocumentDB/databaseAccounts/${userContext.databaseAccount.name}/mongodbDatabases/${params.db}/collections/${params.coll}`;
} }
function useMongoProxyEndpoint(api: string): boolean {
const activeMongoProxyEndpoints: string[] = [MongoProxyEndpoints.Development, MongoProxyEndpoints.Mpac];
let canAccessMongoProxy: boolean = userContext.databaseAccount.properties.publicNetworkAccess === "Enabled";
if (userContext.databaseAccount.properties.ipRules?.length > 0) {
canAccessMongoProxy = canAccessMongoProxy && configContext.MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED;
}
return (
canAccessMongoProxy &&
configContext.NEW_MONGO_APIS?.includes(api) &&
activeMongoProxyEndpoints.includes(configContext.MONGO_PROXY_ENDPOINT)
);
}

View File

@ -3,8 +3,7 @@ import * as _ from "underscore";
import * as DataModels from "../Contracts/DataModels"; import * as DataModels from "../Contracts/DataModels";
import * as ViewModels from "../Contracts/ViewModels"; import * as ViewModels from "../Contracts/ViewModels";
import Explorer from "../Explorer/Explorer"; import Explorer from "../Explorer/Explorer";
import DocumentsTab from "../Explorer/Tabs/DocumentsTab"; import DocumentId, { IDocumentIdContainer } from "../Explorer/Tree/DocumentId";
import DocumentId from "../Explorer/Tree/DocumentId";
import { useDatabases } from "../Explorer/useDatabases"; import { useDatabases } from "../Explorer/useDatabases";
import { userContext } from "../UserContext"; import { userContext } from "../UserContext";
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
@ -162,10 +161,10 @@ export class QueriesClient {
{ {
partitionKey: QueriesClient.PartitionKey, partitionKey: QueriesClient.PartitionKey,
partitionKeyProperties: ["id"], partitionKeyProperties: ["id"],
} as DocumentsTab, } as IDocumentIdContainer,
query, query,
[query.queryName], [query.queryName],
); // TODO: Remove DocumentId's dependency on DocumentsTab );
const options: any = { partitionKey: query.resourceId }; const options: any = { partitionKey: query.resourceId };
return deleteDocument(queriesCollection, documentId) return deleteDocument(queriesCollection, documentId)
.then( .then(

View File

@ -1,14 +1,10 @@
import { ResourceTree } from "Explorer/Tree/ResourceTree";
import React, { FunctionComponent, MutableRefObject, useEffect, useRef } from "react"; import React, { FunctionComponent, MutableRefObject, useEffect, useRef } from "react";
import arrowLeftImg from "../../images/imgarrowlefticon.svg"; import arrowLeftImg from "../../images/imgarrowlefticon.svg";
import refreshImg from "../../images/refresh-cosmos.svg"; import refreshImg from "../../images/refresh-cosmos.svg";
import { AuthType } from "../AuthType";
import Explorer from "../Explorer/Explorer"; import Explorer from "../Explorer/Explorer";
import { ResourceTokenTree } from "../Explorer/Tree/ResourceTokenTree"; import { ResourceTree } from "../Explorer/Tree/ResourceTree";
import { ResourceTree2 } from "../Explorer/Tree2/ResourceTree";
import { userContext } from "../UserContext"; import { userContext } from "../UserContext";
import { getApiShortDisplayName } from "../Utils/APITypeUtils"; import { getApiShortDisplayName } from "../Utils/APITypeUtils";
import { Platform, configContext } from "./../ConfigContext";
import { NormalizedEventKey } from "./Constants"; import { NormalizedEventKey } from "./Constants";
export interface ResourceTreeContainerProps { export interface ResourceTreeContainerProps {
@ -74,12 +70,8 @@ export const ResourceTreeContainer: FunctionComponent<ResourceTreeContainerProps
</div> </div>
</div> </div>
</div> </div>
{userContext.authType === AuthType.ResourceToken ? ( {userContext.features.enableKoResourceTree ? (
<ResourceTokenTree />
) : userContext.features.enableKoResourceTree ? (
<div style={{ overflowY: "auto" }} data-bind="react:resourceTree" /> <div style={{ overflowY: "auto" }} data-bind="react:resourceTree" />
) : configContext.platform === Platform.Fabric ? (
<ResourceTree2 container={container} />
) : ( ) : (
<ResourceTree container={container} /> <ResourceTree container={container} />
)} )}

View File

@ -142,7 +142,7 @@ export const TableEntity: FunctionComponent<TableEntityProps> = ({
<Image <Image
{...imageProps} {...imageProps}
src={EditIcon} src={EditIcon}
alt="editEntity" alt={`Edit ${entityProperty} entity`}
onClick={onEditEntity} onClick={onEditEntity}
tabIndex={0} tabIndex={0}
onKeyPress={handleKeyPress} onKeyPress={handleKeyPress}
@ -156,7 +156,7 @@ export const TableEntity: FunctionComponent<TableEntityProps> = ({
<Image <Image
{...imageProps} {...imageProps}
src={DeleteIcon} src={DeleteIcon}
alt="delete entity" alt={`Delete ${entityProperty} entity`}
id="deleteEntity" id="deleteEntity"
onClick={onDeleteEntity} onClick={onDeleteEntity}
tabIndex={0} tabIndex={0}

View File

@ -2,6 +2,7 @@
exports[`getCommonQueryOptions builds the correct default options objects 1`] = ` exports[`getCommonQueryOptions builds the correct default options objects 1`] = `
Object { Object {
"disableNonStreamingOrderByQuery": true,
"enableScanInQuery": true, "enableScanInQuery": true,
"forceQueryPlan": true, "forceQueryPlan": true,
"maxDegreeOfParallelism": 0, "maxDegreeOfParallelism": 0,
@ -12,6 +13,7 @@ Object {
exports[`getCommonQueryOptions reads from localStorage 1`] = ` exports[`getCommonQueryOptions reads from localStorage 1`] = `
Object { Object {
"disableNonStreamingOrderByQuery": true,
"enableScanInQuery": true, "enableScanInQuery": true,
"forceQueryPlan": true, "forceQueryPlan": true,
"maxDegreeOfParallelism": 17, "maxDegreeOfParallelism": 17,

View File

@ -6,13 +6,13 @@ import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstan
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import { getCollectionName } from "../../Utils/APITypeUtils"; import { getCollectionName } from "../../Utils/APITypeUtils";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { createUpdateCassandraTable } from "../../Utils/arm/generatedClients/cosmos/cassandraResources"; import { createUpdateCassandraTable } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
import { createUpdateGremlinGraph } from "../../Utils/arm/generatedClients/cosmos/gremlinResources"; import { createUpdateGremlinGraph } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
import { createUpdateMongoDBCollection } from "../../Utils/arm/generatedClients/cosmos/mongoDBResources"; import { createUpdateMongoDBCollection } from "../../Utils/arm/generatedClients/cosmos/mongoDBResources";
import { createUpdateSqlContainer } from "../../Utils/arm/generatedClients/cosmos/sqlResources"; import { createUpdateSqlContainer } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
import { createUpdateTable } from "../../Utils/arm/generatedClients/cosmos/tableResources"; import { createUpdateTable } from "../../Utils/arm/generatedClients/cosmos/tableResources";
import * as ARMTypes from "../../Utils/arm/generatedClients/cosmos/types"; import * as ARMTypes from "../../Utils/arm/generatedClients/cosmos/types";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client } from "../CosmosClient"; import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils"; import { handleError } from "../ErrorHandlingUtils";
import { createMongoCollectionWithProxy } from "../MongoProxyClient"; import { createMongoCollectionWithProxy } from "../MongoProxyClient";
@ -96,6 +96,9 @@ const createSqlContainer = async (params: DataModels.CreateCollectionParams): Pr
if (params.uniqueKeyPolicy) { if (params.uniqueKeyPolicy) {
resource.uniqueKeyPolicy = params.uniqueKeyPolicy; resource.uniqueKeyPolicy = params.uniqueKeyPolicy;
} }
if (params.vectorEmbeddingPolicy) {
resource.vectorEmbeddingPolicy = params.vectorEmbeddingPolicy;
}
const rpPayload: ARMTypes.SqlDatabaseCreateUpdateParameters = { const rpPayload: ARMTypes.SqlDatabaseCreateUpdateParameters = {
properties: { properties: {

View File

@ -1,3 +1,4 @@
import { BulkOperationType, OperationInput } from "@azure/cosmos";
import { CollectionBase } from "../../Contracts/ViewModels"; import { CollectionBase } from "../../Contracts/ViewModels";
import DocumentId from "../../Explorer/Tree/DocumentId"; import DocumentId from "../../Explorer/Tree/DocumentId";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
@ -24,3 +25,58 @@ export const deleteDocument = async (collection: CollectionBase, documentId: Doc
clearMessage(); clearMessage();
} }
}; };
/**
* Bulk delete documents
* @param collection
* @param documentId
* @returns array of ids that were successfully deleted
*/
export const deleteDocuments = async (collection: CollectionBase, documentIds: DocumentId[]): Promise<DocumentId[]> => {
const nbDocuments = documentIds.length;
const clearMessage = logConsoleProgress(`Deleting ${documentIds.length} ${getEntityName(true)}`);
try {
const v2Container = await client().database(collection.databaseId).container(collection.id());
// Bulk can only delete 100 documents at a time
const BULK_DELETE_LIMIT = 100;
const promiseArray = [];
while (documentIds.length > 0) {
const documentIdsChunk = documentIds.splice(0, BULK_DELETE_LIMIT);
const operations: OperationInput[] = documentIdsChunk.map((documentId) => ({
id: documentId.id(),
// bulk delete: if not partition key is specified, do not pass empty array, but undefined
partitionKey:
documentId.partitionKeyValue &&
Array.isArray(documentId.partitionKeyValue) &&
documentId.partitionKeyValue.length === 0
? undefined
: documentId.partitionKeyValue,
operationType: BulkOperationType.Delete,
}));
const promise = v2Container.items.bulk(operations).then((bulkResult) => {
return documentIdsChunk.filter((_, index) => bulkResult[index].statusCode === 204);
});
promiseArray.push(promise);
}
const allResult = await Promise.all(promiseArray);
const flatAllResult = Array.prototype.concat.apply([], allResult);
logConsoleInfo(
`Successfully deleted ${getEntityName(flatAllResult.length > 1)}: ${flatAllResult.length} out of ${nbDocuments}`,
);
// TODO: handle case result.length != nbDocuments
return flatAllResult;
} catch (error) {
handleError(
error,
"DeleteDocuments",
`Error while deleting ${documentIds.length} ${getEntityName(documentIds.length > 1)}`,
);
throw error;
} finally {
clearMessage();
}
};

View File

@ -1,4 +1,5 @@
import { FeedOptions, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos"; import { FeedOptions, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
import { isVectorSearchEnabled } from "Utils/CapabilityUtils";
import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility"; import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility";
import { Queries } from "../Constants"; import { Queries } from "../Constants";
import { client } from "../CosmosClient"; import { client } from "../CosmosClient";
@ -26,5 +27,6 @@ export const getCommonQueryOptions = (options: FeedOptions): FeedOptions => {
(storedItemPerPageSetting !== undefined && storedItemPerPageSetting) || (storedItemPerPageSetting !== undefined && storedItemPerPageSetting) ||
Queries.itemsPerPage; Queries.itemsPerPage;
options.maxDegreeOfParallelism = LocalStorageUtility.getEntryNumber(StorageKey.MaxDegreeOfParellism); options.maxDegreeOfParallelism = LocalStorageUtility.getEntryNumber(StorageKey.MaxDegreeOfParellism);
options.disableNonStreamingOrderByQuery = !isVectorSearchEnabled();
return options; return options;
}; };

View File

@ -42,6 +42,10 @@ export interface ConfigContext {
ARM_API_VERSION: string; ARM_API_VERSION: string;
GRAPH_ENDPOINT: string; GRAPH_ENDPOINT: string;
GRAPH_API_VERSION: string; GRAPH_API_VERSION: string;
// This is the endpoint to get offering Ids to be used to fetch prices. Refer to this doc: https://learn.microsoft.com/en-us/rest/api/marketplacecatalog/dataplane/skus/list?view=rest-marketplacecatalog-dataplane-2023-05-01-preview&tabs=HTTP
CATALOG_ENDPOINT: string;
CATALOG_API_VERSION: string;
CATALOG_API_KEY: string;
ARCADIA_ENDPOINT: string; ARCADIA_ENDPOINT: string;
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: string; ARCADIA_LIVY_ENDPOINT_DNS_ZONE: string;
BACKEND_ENDPOINT?: string; BACKEND_ENDPOINT?: string;
@ -83,6 +87,7 @@ let configContext: Readonly<ConfigContext> = {
`^https:\\/\\/.*\\.analysis-df\\.net$`, `^https:\\/\\/.*\\.analysis-df\\.net$`,
`^https:\\/\\/.*\\.analysis-df\\.windows\\.net$`, `^https:\\/\\/.*\\.analysis-df\\.windows\\.net$`,
`^https:\\/\\/.*\\.azure-test\\.net$`, `^https:\\/\\/.*\\.azure-test\\.net$`,
`^https:\\/\\/cosmos-explorer-preview\\.azurewebsites\\.net`,
], // Webpack injects this at build time ], // Webpack injects this at build time
gitSha: process.env.GIT_SHA, gitSha: process.env.GIT_SHA,
hostedExplorerURL: "https://cosmos.azure.com/", hostedExplorerURL: "https://cosmos.azure.com/",
@ -92,6 +97,9 @@ let configContext: Readonly<ConfigContext> = {
ARM_API_VERSION: "2016-06-01", ARM_API_VERSION: "2016-06-01",
GRAPH_ENDPOINT: "https://graph.microsoft.com", GRAPH_ENDPOINT: "https://graph.microsoft.com",
GRAPH_API_VERSION: "1.6", GRAPH_API_VERSION: "1.6",
CATALOG_ENDPOINT: "https://catalogapi.azure.com/",
CATALOG_API_VERSION: "2023-05-01-preview",
CATALOG_API_KEY: "",
ARCADIA_ENDPOINT: "https://workspaceartifacts.projectarcadia.net", ARCADIA_ENDPOINT: "https://workspaceartifacts.projectarcadia.net",
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: "dev.azuresynapse.net", ARCADIA_LIVY_ENDPOINT_DNS_ZONE: "dev.azuresynapse.net",
GITHUB_CLIENT_ID: "6cb2f63cf6f7b5cbdeca", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1189306 GITHUB_CLIENT_ID: "6cb2f63cf6f7b5cbdeca", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1189306
@ -108,6 +116,7 @@ let configContext: Readonly<ConfigContext> = {
"updateDocument", "updateDocument",
"deleteDocument", "deleteDocument",
"createCollectionWithProxy", "createCollectionWithProxy",
"legacyMongoShell",
], ],
MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED: false, MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED: false,
CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Prod, CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Prod,
@ -190,6 +199,9 @@ if (process.env.NODE_ENV === "development") {
updateConfigContext({ updateConfigContext({
PROXY_PATH: "/proxy", PROXY_PATH: "/proxy",
EMULATOR_ENDPOINT: "https://localhost:8081", EMULATOR_ENDPOINT: "https://localhost:8081",
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Mpac,
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Mpac,
CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Mpac,
}); });
} }

View File

@ -1,37 +1,22 @@
import { MessageTypes } from "./MessageTypes"; import { FabricMessageTypes } from "./FabricMessageTypes";
// This is the current version of these messages // This is the current version of these messages
export const DATA_EXPLORER_RPC_VERSION = "2"; export const DATA_EXPLORER_RPC_VERSION = "3";
// Data Explorer to Fabric // Data Explorer to Fabric
export type DataExploreMessageV3 =
// TODO Remove when upgrading to Fabric v2
export type DataExploreMessageV1 =
| "ready"
| { | {
type: MessageTypes.GetAuthorizationToken; type: FabricMessageTypes.Ready;
id: string;
params: GetCosmosTokenMessageOptions[];
}
| {
type: MessageTypes.GetAllResourceTokens;
id: string;
};
// -----------------------------
export type DataExploreMessageV2 =
| {
type: MessageTypes.Ready;
id: string; id: string;
params: [string]; // version params: [string]; // version
} }
| { | {
type: MessageTypes.GetAuthorizationToken; type: FabricMessageTypes.GetAuthorizationToken;
id: string; id: string;
params: GetCosmosTokenMessageOptions[]; params: GetCosmosTokenMessageOptions[];
} }
| { | {
type: MessageTypes.GetAllResourceTokens; type: FabricMessageTypes.GetAllResourceTokens;
id: string; id: string;
}; };

View File

@ -1,4 +1,4 @@
import { ConnectionStatusType, ContainerStatusType } from "../Common/Constants"; import { CapacityMode, ConnectionStatusType, ContainerStatusType } from "../Common/Constants";
export interface ArmEntity { export interface ArmEntity {
id: string; id: string;
@ -35,6 +35,7 @@ export interface DatabaseAccountExtendedProperties {
ipRules?: IpRule[]; ipRules?: IpRule[];
privateEndpointConnections?: unknown[]; privateEndpointConnections?: unknown[];
capacity?: { totalThroughputLimit: number }; capacity?: { totalThroughputLimit: number };
capacityMode?: CapacityMode;
locations?: DatabaseAccountResponseLocation[]; locations?: DatabaseAccountResponseLocation[];
postgresqlEndpoint?: string; postgresqlEndpoint?: string;
publicNetworkAccess?: string; publicNetworkAccess?: string;
@ -157,6 +158,7 @@ export interface Collection extends Resource {
changeFeedPolicy?: ChangeFeedPolicy; changeFeedPolicy?: ChangeFeedPolicy;
analyticalStorageTtl?: number; analyticalStorageTtl?: number;
geospatialConfig?: GeospatialConfig; geospatialConfig?: GeospatialConfig;
vectorEmbeddingPolicy?: VectorEmbeddingPolicy;
schema?: ISchema; schema?: ISchema;
requestSchema?: () => void; requestSchema?: () => void;
computedProperties?: ComputedProperties; computedProperties?: ComputedProperties;
@ -194,8 +196,14 @@ export interface IndexingPolicy {
indexingMode: "consistent" | "lazy" | "none"; indexingMode: "consistent" | "lazy" | "none";
includedPaths: any; includedPaths: any;
excludedPaths: any; excludedPaths: any;
compositeIndexes?: any; compositeIndexes?: any[];
spatialIndexes?: any; spatialIndexes?: any[];
vectorIndexes?: VectorIndex[];
}
export interface VectorIndex {
path: string;
type: "flat" | "diskANN" | "quantizedFlat";
} }
export interface ComputedProperty { export interface ComputedProperty {
@ -333,6 +341,18 @@ export interface CreateCollectionParams {
partitionKey?: PartitionKey; partitionKey?: PartitionKey;
uniqueKeyPolicy?: UniqueKeyPolicy; uniqueKeyPolicy?: UniqueKeyPolicy;
createMongoWildcardIndex?: boolean; createMongoWildcardIndex?: boolean;
vectorEmbeddingPolicy?: VectorEmbeddingPolicy;
}
export interface VectorEmbeddingPolicy {
vectorEmbeddings: VectorEmbedding[];
}
export interface VectorEmbedding {
dataType: "float16" | "float32" | "uint8" | "int8";
dimensions: number;
distanceFunction: "euclidean" | "cosine" | "dotproduct";
path: string;
} }
export interface ReadDatabaseOfferParams { export interface ReadDatabaseOfferParams {

View File

@ -0,0 +1,13 @@
/**
* Data Explorer -> Fabric communication.
*/
export enum FabricMessageTypes {
GetAuthorizationToken = "GetAuthorizationToken",
GetAllResourceTokens = "GetAllResourceTokens",
Ready = "Ready",
}
export interface AuthorizationToken {
XDate: string;
PrimaryReadWriteToken: string;
}

View File

@ -1,4 +1,4 @@
import { AuthorizationToken } from "./MessageTypes"; import { AuthorizationToken } from "Contracts/FabricMessageTypes";
// This is the version of these messages // This is the version of these messages
export const FABRIC_RPC_VERSION = "2"; export const FABRIC_RPC_VERSION = "2";

View File

@ -1,12 +1,13 @@
/** /**
* Messaging types used with Data Explorer <-> Portal communication, * Messaging types used with Data Explorer <-> Portal communication,
* Hosted <-> Explorer communication and Data Explorer -> Fabric communication. * Hosted <-> Explorer communication
* *
* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
* WARNING: !!!!!!! YOU CAN ONLY ADD NEW TYPES TO THE END OF THIS ENUM !!!!!!! * WARNING: !!!!!!! YOU CAN ONLY ADD NEW TYPES TO THE END OF THIS ENUM !!!!!!!
* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
* *
* Enum are integers, so inserting or deleting a type will break the communication. * Enum are integers, so inserting or deleting a type will break the communication.
*
*/ */
export enum MessageTypes { export enum MessageTypes {
TelemetryInfo, TelemetryInfo,
@ -43,14 +44,9 @@ export enum MessageTypes {
DisplayNPSSurvey, DisplayNPSSurvey,
OpenVCoreMongoNetworkingBlade, OpenVCoreMongoNetworkingBlade,
OpenVCoreMongoConnectionStringsBlade, OpenVCoreMongoConnectionStringsBlade,
GetAuthorizationToken, // Data Explorer -> Fabric GetAuthorizationToken, // unused. Can be removed if the portal uses the same list of enums.
GetAllResourceTokens, // Data Explorer -> Fabric GetAllResourceTokens, // unused. Can be removed if the portal uses the same list of enums.
Ready, // Data Explorer -> Fabric Ready, // unused. Can be removed if the portal uses the same list of enums.
OpenCESCVAFeedbackBlade, OpenCESCVAFeedbackBlade,
ActivateTab, ActivateTab,
} }
export interface AuthorizationToken {
XDate: string;
PrimaryReadWriteToken: string;
}

View File

@ -176,6 +176,11 @@ export interface Collection extends CollectionBase {
loadTriggers(): Promise<any>; loadTriggers(): Promise<any>;
loadOffer(): Promise<void>; loadOffer(): Promise<void>;
showStoredProcedures: ko.Observable<boolean>;
showTriggers: ko.Observable<boolean>;
showUserDefinedFunctions: ko.Observable<boolean>;
showConflicts: ko.Observable<boolean>;
createStoredProcedureNode(data: StoredProcedureDefinition & Resource): StoredProcedure; createStoredProcedureNode(data: StoredProcedureDefinition & Resource): StoredProcedure;
createUserDefinedFunctionNode(data: UserDefinedFunctionDefinition & Resource): UserDefinedFunction; createUserDefinedFunctionNode(data: UserDefinedFunctionDefinition & Resource): UserDefinedFunction;
createTriggerNode(data: TriggerDefinition | SqlTriggerResource): Trigger; createTriggerNode(data: TriggerDefinition | SqlTriggerResource): Trigger;
@ -324,9 +329,9 @@ export enum DocumentExplorerState {
noDocumentSelected, noDocumentSelected,
newDocumentValid, newDocumentValid,
newDocumentInvalid, newDocumentInvalid,
exisitingDocumentNoEdits, existingDocumentNoEdits,
exisitingDocumentDirtyValid, existingDocumentDirtyValid,
exisitingDocumentDirtyInvalid, existingDocumentDirtyInvalid,
} }
export enum IndexingPolicyEditorState { export enum IndexingPolicyEditorState {
@ -339,9 +344,9 @@ export enum IndexingPolicyEditorState {
export enum ScriptEditorState { export enum ScriptEditorState {
newInvalid, newInvalid,
newValid, newValid,
exisitingNoEdits, existingNoEdits,
exisitingDirtyValid, existingDirtyValid,
exisitingDirtyInvalid, existingDirtyInvalid,
} }
export enum CollectionTabKind { export enum CollectionTabKind {
@ -420,6 +425,7 @@ export interface SelfServeFrameInputs {
authorizationToken: string; authorizationToken: string;
csmEndpoint: string; csmEndpoint: string;
flights?: readonly string[]; flights?: readonly string[];
catalogAPIKey: string;
} }
export class MonacoEditorSettings { export class MonacoEditorSettings {

View File

@ -1,3 +1,4 @@
import { TreeNodeMenuItem } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
import { useDatabases } from "Explorer/useDatabases"; import { useDatabases } from "Explorer/useDatabases";
import { Action } from "Shared/Telemetry/TelemetryConstants"; import { Action } from "Shared/Telemetry/TelemetryConstants";
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor"; import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
@ -19,7 +20,6 @@ import { userContext } from "../UserContext";
import { getCollectionName, getDatabaseName } from "../Utils/APITypeUtils"; import { getCollectionName, getDatabaseName } from "../Utils/APITypeUtils";
import { useSidePanel } from "../hooks/useSidePanel"; import { useSidePanel } from "../hooks/useSidePanel";
import { Platform, configContext } from "./../ConfigContext"; import { Platform, configContext } from "./../ConfigContext";
import { TreeNodeMenuItem } from "./Controls/TreeComponent/TreeComponent";
import Explorer from "./Explorer"; import Explorer from "./Explorer";
import { useNotebook } from "./Notebook/useNotebook"; import { useNotebook } from "./Notebook/useNotebook";
import { DeleteCollectionConfirmationPane } from "./Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane"; import { DeleteCollectionConfirmationPane } from "./Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane";

View File

@ -1,4 +1,4 @@
import { Icon, Label, Stack } from "@fluentui/react"; import { DirectionalHint, Icon, Label, Stack, TooltipHost } from "@fluentui/react";
import * as React from "react"; import * as React from "react";
import { NormalizedEventKey } from "../../../Common/Constants"; import { NormalizedEventKey } from "../../../Common/Constants";
import { accordionStackTokens } from "../Settings/SettingsRenderUtils"; import { accordionStackTokens } from "../Settings/SettingsRenderUtils";
@ -8,6 +8,7 @@ export interface CollapsibleSectionProps {
isExpandedByDefault: boolean; isExpandedByDefault: boolean;
onExpand?: () => void; onExpand?: () => void;
children: JSX.Element; children: JSX.Element;
tooltipContent?: string | JSX.Element | JSX.Element[];
} }
export interface CollapsibleSectionState { export interface CollapsibleSectionState {
@ -26,8 +27,8 @@ export class CollapsibleSectionComponent extends React.Component<CollapsibleSect
this.setState({ isExpanded: !this.state.isExpanded }); this.setState({ isExpanded: !this.state.isExpanded });
}; };
public componentDidUpdate(): void { public componentDidUpdate(_prevProps: CollapsibleSectionProps, prevState: CollapsibleSectionState): void {
if (this.state.isExpanded && this.props.onExpand) { if (!prevState.isExpanded && this.state.isExpanded && this.props.onExpand) {
this.props.onExpand(); this.props.onExpand();
} }
} }
@ -43,7 +44,7 @@ export class CollapsibleSectionComponent extends React.Component<CollapsibleSect
return ( return (
<> <>
<Stack <Stack
className="collapsibleSection" className={"collapsibleSection"}
horizontal horizontal
verticalAlign="center" verticalAlign="center"
tokens={accordionStackTokens} tokens={accordionStackTokens}
@ -55,6 +56,19 @@ export class CollapsibleSectionComponent extends React.Component<CollapsibleSect
> >
<Icon iconName={this.state.isExpanded ? "ChevronDown" : "ChevronRight"} /> <Icon iconName={this.state.isExpanded ? "ChevronDown" : "ChevronRight"} />
<Label>{this.props.title}</Label> <Label>{this.props.title}</Label>
{this.props.tooltipContent && (
<TooltipHost
directionalHint={DirectionalHint.bottomLeftEdge}
content={this.props.tooltipContent}
styles={{
root: {
marginLeft: "0 !important",
},
}}
>
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
</TooltipHost>
)}
</Stack> </Stack>
{this.state.isExpanded && this.props.children} {this.state.isExpanded && this.props.children}
</> </>

View File

@ -1,6 +1,7 @@
/** /**
* React component for Command button component. * React component for Command button component.
*/ */
import { KeyboardAction } from "KeyboardShortcuts";
import * as React from "react"; import * as React from "react";
import CollapseChevronDownIcon from "../../../../images/QueryBuilder/CollapseChevronDown_16x.png"; import CollapseChevronDownIcon from "../../../../images/QueryBuilder/CollapseChevronDown_16x.png";
import { KeyCodes } from "../../../Common/Constants"; import { KeyCodes } from "../../../Common/Constants";
@ -30,7 +31,7 @@ export interface CommandButtonComponentProps {
/** /**
* Click handler for command button click * Click handler for command button click
*/ */
onCommandClick: (e: React.SyntheticEvent) => void; onCommandClick?: (e: React.SyntheticEvent | KeyboardEvent) => void;
/** /**
* Label for the button * Label for the button
@ -107,10 +108,17 @@ export interface CommandButtonComponentProps {
* Vertical bar to divide buttons * Vertical bar to divide buttons
*/ */
isDivider?: boolean; isDivider?: boolean;
/** /**
* Aria-label for the button * Aria-label for the button
*/ */
ariaLabel: string; ariaLabel: string;
/**
* If specified, a keyboard action that should trigger this button's onCommandClick handler when activated.
* If not specified, the button will not be triggerable by keyboard shortcuts.
*/
keyboardAction?: KeyboardAction;
} }
export class CommandButtonComponent extends React.Component<CommandButtonComponentProps> { export class CommandButtonComponent extends React.Component<CommandButtonComponentProps> {

View File

@ -20,7 +20,10 @@ export interface EditorReactProps {
lineDecorationsWidth?: monaco.editor.IEditorOptions["lineDecorationsWidth"]; lineDecorationsWidth?: monaco.editor.IEditorOptions["lineDecorationsWidth"];
minimap?: monaco.editor.IEditorOptions["minimap"]; minimap?: monaco.editor.IEditorOptions["minimap"];
scrollBeyondLastLine?: monaco.editor.IEditorOptions["scrollBeyondLastLine"]; scrollBeyondLastLine?: monaco.editor.IEditorOptions["scrollBeyondLastLine"];
fontSize?: monaco.editor.IEditorOptions["fontSize"];
monacoContainerStyles?: React.CSSProperties; monacoContainerStyles?: React.CSSProperties;
className?: string;
spinnerClassName?: string;
} }
export class EditorReact extends React.Component<EditorReactProps, EditorReactStates> { export class EditorReact extends React.Component<EditorReactProps, EditorReactStates> {
@ -54,13 +57,17 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
const existingContent = this.editor.getModel().getValue(); const existingContent = this.editor.getModel().getValue();
if (this.props.content !== existingContent) { if (this.props.content !== existingContent) {
this.editor.pushUndoStop(); if (this.props.isReadOnly) {
this.editor.executeEdits("", [ this.editor.setValue(this.props.content);
{ } else {
range: this.editor.getModel().getFullModelRange(), this.editor.pushUndoStop();
text: this.props.content, this.editor.executeEdits("", [
}, {
]); range: this.editor.getModel().getFullModelRange(),
text: this.props.content,
},
]);
}
} }
} }
@ -71,9 +78,11 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
public render(): JSX.Element { public render(): JSX.Element {
return ( return (
<React.Fragment> <React.Fragment>
{!this.state.showEditor && <Spinner size={SpinnerSize.large} className="spinner" />} {!this.state.showEditor && (
<Spinner size={SpinnerSize.large} className={this.props.spinnerClassName || "spinner"} />
)}
<div <div
className="jsonEditor" className={this.props.className || "jsonEditor"}
style={this.props.monacoContainerStyles} style={this.props.monacoContainerStyles}
ref={(elt: HTMLElement) => this.setRef(elt)} ref={(elt: HTMLElement) => this.setRef(elt)}
/> />
@ -115,7 +124,7 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
value: this.props.content, value: this.props.content,
readOnly: this.props.isReadOnly, readOnly: this.props.isReadOnly,
ariaLabel: this.props.ariaLabel, ariaLabel: this.props.ariaLabel,
fontSize: 12, fontSize: this.props.fontSize || 12,
automaticLayout: true, automaticLayout: true,
theme: this.props.theme, theme: this.props.theme,
wordWrap: this.props.wordWrap || "off", wordWrap: this.props.wordWrap || "off",
@ -128,7 +137,13 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
this.rootNode.innerHTML = ""; this.rootNode.innerHTML = "";
const monaco = await loadMonaco(); const monaco = await loadMonaco();
createCallback(monaco?.editor?.create(this.rootNode, options)); try {
createCallback(monaco?.editor?.create(this.rootNode, options));
} catch (error) {
// This could happen if the parent node suddenly disappears during create()
console.error("Unable to create EditorReact", error);
return;
}
if (this.rootNode.innerHTML) { if (this.rootNode.innerHTML) {
this.setState({ this.setState({

View File

@ -7,14 +7,14 @@
} }
.settingsV2ToolTip { .settingsV2ToolTip {
padding: 10px; padding: 10px;
font: 12px @DataExplorerFont; font: 12px @DataExplorerFont;
max-width: 300px; max-width: 300px;
} }
.autoPilotSelector span { .autoPilotSelector span {
height: 25px; height: 25px;
font: 14px @DataExplorerFont; font: 14px @DataExplorerFont;
} }
.settingsV2TabsContainer { .settingsV2TabsContainer {
@ -25,7 +25,14 @@
font-family: @DataExplorerFont; font-family: @DataExplorerFont;
} }
.settingsV2IndexingPolicyEditor { .settingsV2Editor {
width: 100%; width: 100%;
height: 60vh; height: 60vh;
} }
.settingsV2EditorSpinner {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}

View File

@ -3,7 +3,12 @@ import {
ComputedPropertiesComponent, ComputedPropertiesComponent,
ComputedPropertiesComponentProps, ComputedPropertiesComponentProps,
} from "Explorer/Controls/Settings/SettingsSubComponents/ComputedPropertiesComponent"; } from "Explorer/Controls/Settings/SettingsSubComponents/ComputedPropertiesComponent";
import {
ContainerVectorPolicyComponent,
ContainerVectorPolicyComponentProps,
} from "Explorer/Controls/Settings/SettingsSubComponents/ContainerVectorPolicyComponent";
import { useDatabases } from "Explorer/useDatabases"; import { useDatabases } from "Explorer/useDatabases";
import { isVectorSearchEnabled } from "Utils/CapabilityUtils";
import { isRunningOnPublicCloud } from "Utils/CloudUtils"; import { isRunningOnPublicCloud } from "Utils/CloudUtils";
import * as React from "react"; import * as React from "react";
import DiscardIcon from "../../../../images/discard.svg"; import DiscardIcon from "../../../../images/discard.svg";
@ -144,6 +149,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
private shouldShowComputedPropertiesEditor: boolean; private shouldShowComputedPropertiesEditor: boolean;
private shouldShowIndexingPolicyEditor: boolean; private shouldShowIndexingPolicyEditor: boolean;
private shouldShowPartitionKeyEditor: boolean; private shouldShowPartitionKeyEditor: boolean;
private isVectorSearchEnabled: boolean;
private totalThroughputUsed: number; private totalThroughputUsed: number;
public mongoDBCollectionResource: MongoDBCollectionResource; public mongoDBCollectionResource: MongoDBCollectionResource;
@ -158,6 +164,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
this.shouldShowComputedPropertiesEditor = userContext.apiType === "SQL"; this.shouldShowComputedPropertiesEditor = userContext.apiType === "SQL";
this.shouldShowIndexingPolicyEditor = userContext.apiType !== "Cassandra" && userContext.apiType !== "Mongo"; this.shouldShowIndexingPolicyEditor = userContext.apiType !== "Cassandra" && userContext.apiType !== "Mongo";
this.shouldShowPartitionKeyEditor = userContext.apiType === "SQL" && isRunningOnPublicCloud(); this.shouldShowPartitionKeyEditor = userContext.apiType === "SQL" && isRunningOnPublicCloud();
this.isVectorSearchEnabled = isVectorSearchEnabled() && !hasDatabaseSharedThroughput(this.collection);
this.changeFeedPolicyVisible = userContext.features.enableChangeFeedPolicy; this.changeFeedPolicyVisible = userContext.features.enableChangeFeedPolicy;
@ -1097,6 +1104,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
indexTransformationProgress: this.state.indexTransformationProgress, indexTransformationProgress: this.state.indexTransformationProgress,
refreshIndexTransformationProgress: this.refreshIndexTransformationProgress, refreshIndexTransformationProgress: this.refreshIndexTransformationProgress,
onIndexingPolicyDirtyChange: this.onIndexingPolicyDirtyChange, onIndexingPolicyDirtyChange: this.onIndexingPolicyDirtyChange,
isVectorSearchEnabled: this.isVectorSearchEnabled,
}; };
const mongoIndexingPolicyComponentProps: MongoIndexingPolicyComponentProps = { const mongoIndexingPolicyComponentProps: MongoIndexingPolicyComponentProps = {
@ -1143,6 +1151,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
explorer: this.props.settingsTab.getContainer(), explorer: this.props.settingsTab.getContainer(),
}; };
const containerVectorPolicyProps: ContainerVectorPolicyComponentProps = {
vectorEmbeddingPolicy: this.collection.rawDataModel?.vectorEmbeddingPolicy,
};
const tabs: SettingsV2TabInfo[] = []; const tabs: SettingsV2TabInfo[] = [];
if (!hasDatabaseSharedThroughput(this.collection) && this.offer) { if (!hasDatabaseSharedThroughput(this.collection) && this.offer) {
tabs.push({ tabs.push({
@ -1156,6 +1168,13 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
content: <SubSettingsComponent {...subSettingsComponentProps} />, content: <SubSettingsComponent {...subSettingsComponentProps} />,
}); });
if (this.isVectorSearchEnabled) {
tabs.push({
tab: SettingsV2TabTypes.ContainerVectorPolicyTab,
content: <ContainerVectorPolicyComponent {...containerVectorPolicyProps} />,
});
}
if (this.shouldShowIndexingPolicyEditor) { if (this.shouldShowIndexingPolicyEditor) {
tabs.push({ tabs.push({
tab: SettingsV2TabTypes.IndexingPolicyTab, tab: SettingsV2TabTypes.IndexingPolicyTab,

View File

@ -121,7 +121,7 @@ export class ComputedPropertiesComponent extends React.Component<
</Link> </Link>
&#160; about how to define computed properties and how to use them. &#160; about how to define computed properties and how to use them.
</Text> </Text>
<div className="settingsV2IndexingPolicyEditor" tabIndex={0} ref={this.computedPropertiesDiv}></div> <div className="settingsV2Editor" tabIndex={0} ref={this.computedPropertiesDiv}></div>
</Stack> </Stack>
); );
} }

View File

@ -0,0 +1,30 @@
import { Stack } from "@fluentui/react";
import { VectorEmbeddingPolicy } from "Contracts/DataModels";
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
import { titleAndInputStackProps } from "Explorer/Controls/Settings/SettingsRenderUtils";
import React from "react";
export interface ContainerVectorPolicyComponentProps {
vectorEmbeddingPolicy: VectorEmbeddingPolicy;
}
export const ContainerVectorPolicyComponent: React.FC<ContainerVectorPolicyComponentProps> = ({
vectorEmbeddingPolicy,
}) => {
return (
<Stack {...titleAndInputStackProps} styles={{ root: { position: "relative" } }}>
<EditorReact
language={"json"}
content={JSON.stringify(vectorEmbeddingPolicy || {}, null, 4)}
isReadOnly={true}
wordWrap={"on"}
ariaLabel={"Container vector policy"}
lineNumbers={"on"}
scrollBeyondLastLine={false}
className={"settingsV2Editor"}
spinnerClassName={"settingsV2EditorSpinner"}
fontSize={14}
/>
</Stack>
);
};

View File

@ -16,6 +16,7 @@ export interface IndexingPolicyComponentProps {
logIndexingPolicySuccessMessage: () => void; logIndexingPolicySuccessMessage: () => void;
indexTransformationProgress: number; indexTransformationProgress: number;
refreshIndexTransformationProgress: () => Promise<void>; refreshIndexTransformationProgress: () => Promise<void>;
isVectorSearchEnabled?: boolean;
onIndexingPolicyDirtyChange: (isIndexingPolicyDirty: boolean) => void; onIndexingPolicyDirtyChange: (isIndexingPolicyDirty: boolean) => void;
} }
@ -119,10 +120,15 @@ export class IndexingPolicyComponent extends React.Component<
indexTransformationProgress={this.props.indexTransformationProgress} indexTransformationProgress={this.props.indexTransformationProgress}
refreshIndexTransformationProgress={this.props.refreshIndexTransformationProgress} refreshIndexTransformationProgress={this.props.refreshIndexTransformationProgress}
/> />
{this.props.isVectorSearchEnabled && (
<MessageBar messageBarType={MessageBarType.severeWarning}>
Container vector policies and vector indexes are not modifiable after container creation
</MessageBar>
)}
{isDirty(this.props.indexingPolicyContent, this.props.indexingPolicyContentBaseline) && ( {isDirty(this.props.indexingPolicyContent, this.props.indexingPolicyContentBaseline) && (
<MessageBar messageBarType={MessageBarType.warning}>{unsavedEditorWarningMessage("indexPolicy")}</MessageBar> <MessageBar messageBarType={MessageBarType.warning}>{unsavedEditorWarningMessage("indexPolicy")}</MessageBar>
)} )}
<div className="settingsV2IndexingPolicyEditor" tabIndex={0} ref={this.indexingPolicyDiv}></div> <div className="settingsV2Editor" tabIndex={0} ref={this.indexingPolicyDiv}></div>
</Stack> </Stack>
); );
} }

View File

@ -29,7 +29,7 @@ exports[`ComputedPropertiesComponent renders 1`] = `
  about how to define computed properties and how to use them.   about how to define computed properties and how to use them.
</Text> </Text>
<div <div
className="settingsV2IndexingPolicyEditor" className="settingsV2Editor"
tabIndex={0} tabIndex={0}
/> />
</Stack> </Stack>

View File

@ -12,7 +12,7 @@ exports[`IndexingPolicyComponent renders 1`] = `
refreshIndexTransformationProgress={[Function]} refreshIndexTransformationProgress={[Function]}
/> />
<div <div
className="settingsV2IndexingPolicyEditor" className="settingsV2Editor"
tabIndex={0} tabIndex={0}
/> />
</Stack> </Stack>

View File

@ -47,6 +47,7 @@ export enum SettingsV2TabTypes {
IndexingPolicyTab, IndexingPolicyTab,
PartitionKeyTab, PartitionKeyTab,
ComputedPropertiesTab, ComputedPropertiesTab,
ContainerVectorPolicyTab,
} }
export interface IsComponentDirtyResult { export interface IsComponentDirtyResult {
@ -151,7 +152,9 @@ export const getTabTitle = (tab: SettingsV2TabTypes): string => {
case SettingsV2TabTypes.PartitionKeyTab: case SettingsV2TabTypes.PartitionKeyTab:
return "Partition Keys (preview)"; return "Partition Keys (preview)";
case SettingsV2TabTypes.ComputedPropertiesTab: case SettingsV2TabTypes.ComputedPropertiesTab:
return "Computed Properties (preview)"; return "Computed Properties";
case SettingsV2TabTypes.ContainerVectorPolicyTab:
return "Container Vector Policy (preview)";
default: default:
throw new Error(`Unknown tab ${tab}`); throw new Error(`Unknown tab ${tab}`);
} }

View File

@ -30,7 +30,6 @@ exports[`SettingsComponent renders 1`] = `
"conflictResolutionPolicy": [Function], "conflictResolutionPolicy": [Function],
"container": Explorer { "container": Explorer {
"_isInitializingNotebooks": false, "_isInitializingNotebooks": false,
"_resetNotebookWorkspace": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function], "isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function], "onRefreshDatabasesKeyPress": [Function],
@ -108,7 +107,6 @@ exports[`SettingsComponent renders 1`] = `
"conflictResolutionPolicy": [Function], "conflictResolutionPolicy": [Function],
"container": Explorer { "container": Explorer {
"_isInitializingNotebooks": false, "_isInitializingNotebooks": false,
"_resetNotebookWorkspace": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function], "isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function], "onRefreshDatabasesKeyPress": [Function],
@ -198,6 +196,7 @@ exports[`SettingsComponent renders 1`] = `
"indexingMode": "consistent", "indexingMode": "consistent",
} }
} }
isVectorSearchEnabled={false}
logIndexingPolicySuccessMessage={[Function]} logIndexingPolicySuccessMessage={[Function]}
onIndexingPolicyContentChange={[Function]} onIndexingPolicyContentChange={[Function]}
onIndexingPolicyDirtyChange={[Function]} onIndexingPolicyDirtyChange={[Function]}
@ -225,7 +224,6 @@ exports[`SettingsComponent renders 1`] = `
"conflictResolutionPolicy": [Function], "conflictResolutionPolicy": [Function],
"container": Explorer { "container": Explorer {
"_isInitializingNotebooks": false, "_isInitializingNotebooks": false,
"_resetNotebookWorkspace": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function], "isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function], "onRefreshDatabasesKeyPress": [Function],
@ -272,7 +270,6 @@ exports[`SettingsComponent renders 1`] = `
explorer={ explorer={
Explorer { Explorer {
"_isInitializingNotebooks": false, "_isInitializingNotebooks": false,
"_resetNotebookWorkspace": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function], "isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function], "onRefreshDatabasesKeyPress": [Function],
@ -300,7 +297,7 @@ exports[`SettingsComponent renders 1`] = `
/> />
</PivotItem> </PivotItem>
<PivotItem <PivotItem
headerText="Computed Properties (preview)" headerText="Computed Properties"
itemKey="ComputedPropertiesTab" itemKey="ComputedPropertiesTab"
key="ComputedPropertiesTab" key="ComputedPropertiesTab"
style={ style={

View File

@ -20,6 +20,10 @@
outline-offset: 2px; outline-offset: 2px;
} }
.outlineNone{
outline: none !important;
}
.copyQuery:focus::after, .copyQuery:focus::after,
.deleteQuery:focus::after { .deleteQuery:focus::after {
outline: none !important; outline: none !important;

View File

@ -223,6 +223,7 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
<Text variant="small" aria-label="capacity calculator of azure cosmos db"> <Text variant="small" aria-label="capacity calculator of azure cosmos db">
Estimate your required RU/s with{" "} Estimate your required RU/s with{" "}
<Link <Link
className="underlinedLink outlineNone"
target="_blank" target="_blank"
href="https://cosmos.azure.com/capacitycalculator/" href="https://cosmos.azure.com/capacitycalculator/"
aria-label="capacity calculator of azure cosmos db" aria-label="capacity calculator of azure cosmos db"
@ -271,7 +272,12 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
<Stack className="throughputInputSpacing"> <Stack className="throughputInputSpacing">
<Text variant="small" aria-label="ruDescription"> <Text variant="small" aria-label="ruDescription">
Estimate your required RU/s with&nbsp; Estimate your required RU/s with&nbsp;
<Link target="_blank" href="https://cosmos.azure.com/capacitycalculator/" aria-label="Capacity calculator"> <Link
className="underlinedLink"
target="_blank"
href="https://cosmos.azure.com/capacitycalculator/"
aria-label="Capacity calculator"
>
capacity calculator capacity calculator
</Link> </Link>
. .

View File

@ -733,11 +733,13 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
<StyledLinkBase <StyledLinkBase
aria-label="capacity calculator of azure cosmos db" aria-label="capacity calculator of azure cosmos db"
className="underlinedLink outlineNone"
href="https://cosmos.azure.com/capacitycalculator/" href="https://cosmos.azure.com/capacitycalculator/"
target="_blank" target="_blank"
> >
<LinkBase <LinkBase
aria-label="capacity calculator of azure cosmos db" aria-label="capacity calculator of azure cosmos db"
className="underlinedLink outlineNone"
href="https://cosmos.azure.com/capacitycalculator/" href="https://cosmos.azure.com/capacitycalculator/"
styles={[Function]} styles={[Function]}
target="_blank" target="_blank"
@ -1017,7 +1019,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
> >
<a <a
aria-label="capacity calculator of azure cosmos db" aria-label="capacity calculator of azure cosmos db"
className="ms-Link root-117" className="ms-Link underlinedLink outlineNone root-117"
href="https://cosmos.azure.com/capacitycalculator/" href="https://cosmos.azure.com/capacitycalculator/"
onClick={[Function]} onClick={[Function]}
target="_blank" target="_blank"

View File

@ -1,48 +1,48 @@
import React from "react";
import { shallow } from "enzyme"; import { shallow } from "enzyme";
import { TreeComponent, TreeNode, TreeNodeComponent } from "./TreeComponent"; import React from "react";
import { LegacyTreeComponent, LegacyTreeNode, LegacyTreeNodeComponent } from "./LegacyTreeComponent";
const buildChildren = (): TreeNode[] => { const buildChildren = (): LegacyTreeNode[] => {
const grandChild11: TreeNode = { const grandChild11: LegacyTreeNode = {
label: "ZgrandChild11", label: "ZgrandChild11",
}; };
const grandChild12: TreeNode = { const grandChild12: LegacyTreeNode = {
label: "AgrandChild12", label: "AgrandChild12",
}; };
const child1: TreeNode = { const child1: LegacyTreeNode = {
label: "Bchild1", label: "Bchild1",
children: [grandChild11, grandChild12], children: [grandChild11, grandChild12],
}; };
const child2: TreeNode = { const child2: LegacyTreeNode = {
label: "2child2", label: "2child2",
}; };
return [child1, child2]; return [child1, child2];
}; };
const buildChildren2 = (): TreeNode[] => { const buildChildren2 = (): LegacyTreeNode[] => {
const grandChild11: TreeNode = { const grandChild11: LegacyTreeNode = {
label: "ZgrandChild11", label: "ZgrandChild11",
}; };
const grandChild12: TreeNode = { const grandChild12: LegacyTreeNode = {
label: "AgrandChild12", label: "AgrandChild12",
}; };
const child1: TreeNode = { const child1: LegacyTreeNode = {
label: "aChild", label: "aChild",
}; };
const child2: TreeNode = { const child2: LegacyTreeNode = {
label: "bchild", label: "bchild",
children: [grandChild11, grandChild12], children: [grandChild11, grandChild12],
}; };
const child3: TreeNode = { const child3: LegacyTreeNode = {
label: "cchild", label: "cchild",
}; };
const child4: TreeNode = { const child4: LegacyTreeNode = {
label: "dchild", label: "dchild",
children: [grandChild11, grandChild12], children: [grandChild11, grandChild12],
}; };
@ -50,7 +50,7 @@ const buildChildren2 = (): TreeNode[] => {
return [child1, child2, child3, child4]; return [child1, child2, child3, child4];
}; };
describe("TreeComponent", () => { describe("LegacyTreeComponent", () => {
it("renders a simple tree", () => { it("renders a simple tree", () => {
const root = { const root = {
label: "root", label: "root",
@ -62,14 +62,14 @@ describe("TreeComponent", () => {
className: "tree", className: "tree",
}; };
const wrapper = shallow(<TreeComponent {...props} />); const wrapper = shallow(<LegacyTreeComponent {...props} />);
expect(wrapper).toMatchSnapshot(); expect(wrapper).toMatchSnapshot();
}); });
}); });
describe("TreeNodeComponent", () => { describe("LegacyTreeNodeComponent", () => {
it("renders a simple node (sorted children, expanded)", () => { it("renders a simple node (sorted children, expanded)", () => {
const node: TreeNode = { const node: LegacyTreeNode = {
label: "label", label: "label",
id: "id", id: "id",
children: buildChildren(), children: buildChildren(),
@ -98,12 +98,12 @@ describe("TreeNodeComponent", () => {
generation: 12, generation: 12,
paddingLeft: 23, paddingLeft: 23,
}; };
const wrapper = shallow(<TreeNodeComponent {...props} />); const wrapper = shallow(<LegacyTreeNodeComponent {...props} />);
expect(wrapper).toMatchSnapshot(); expect(wrapper).toMatchSnapshot();
}); });
it("renders unsorted children by default", () => { it("renders unsorted children by default", () => {
const node: TreeNode = { const node: LegacyTreeNode = {
label: "label", label: "label",
children: buildChildren(), children: buildChildren(),
isExpanded: true, isExpanded: true,
@ -113,12 +113,12 @@ describe("TreeNodeComponent", () => {
generation: 2, generation: 2,
paddingLeft: 9, paddingLeft: 9,
}; };
const wrapper = shallow(<TreeNodeComponent {...props} />); const wrapper = shallow(<LegacyTreeNodeComponent {...props} />);
expect(wrapper).toMatchSnapshot(); expect(wrapper).toMatchSnapshot();
}); });
it("does not render children by default", () => { it("does not render children by default", () => {
const node: TreeNode = { const node: LegacyTreeNode = {
label: "label", label: "label",
children: buildChildren(), children: buildChildren(),
isAlphaSorted: false, isAlphaSorted: false,
@ -128,12 +128,12 @@ describe("TreeNodeComponent", () => {
generation: 2, generation: 2,
paddingLeft: 9, paddingLeft: 9,
}; };
const wrapper = shallow(<TreeNodeComponent {...props} />); const wrapper = shallow(<LegacyTreeNodeComponent {...props} />);
expect(wrapper).toMatchSnapshot(); expect(wrapper).toMatchSnapshot();
}); });
it("renders sorted children, expanded, leaves and parents separated", () => { it("renders sorted children, expanded, leaves and parents separated", () => {
const node: TreeNode = { const node: LegacyTreeNode = {
label: "label", label: "label",
id: "id", id: "id",
children: buildChildren2(), children: buildChildren2(),
@ -156,12 +156,12 @@ describe("TreeNodeComponent", () => {
generation: 12, generation: 12,
paddingLeft: 23, paddingLeft: 23,
}; };
const wrapper = shallow(<TreeNodeComponent {...props} />); const wrapper = shallow(<LegacyTreeNodeComponent {...props} />);
expect(wrapper).toMatchSnapshot(); expect(wrapper).toMatchSnapshot();
}); });
it("renders loading icon", () => { it("renders loading icon", () => {
const node: TreeNode = { const node: LegacyTreeNode = {
label: "label", label: "label",
children: [], children: [],
isExpanded: true, isExpanded: true,
@ -172,7 +172,7 @@ describe("TreeNodeComponent", () => {
generation: 2, generation: 2,
paddingLeft: 9, paddingLeft: 9,
}; };
const wrapper = shallow(<TreeNodeComponent {...props} />); const wrapper = shallow(<LegacyTreeNodeComponent {...props} />);
expect(wrapper).toMatchSnapshot(); expect(wrapper).toMatchSnapshot();
}); });
}); });

View File

@ -12,6 +12,7 @@ import {
IContextualMenuItemProps, IContextualMenuItemProps,
IContextualMenuProps, IContextualMenuProps,
} from "@fluentui/react"; } from "@fluentui/react";
import { TreeNodeMenuItem } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
import * as React from "react"; import * as React from "react";
import AnimateHeight from "react-animate-height"; import AnimateHeight from "react-animate-height";
import LoadingIndicator_3Squares from "../../../../images/LoadingIndicator_3Squares.gif"; import LoadingIndicator_3Squares from "../../../../images/LoadingIndicator_3Squares.gif";
@ -22,18 +23,10 @@ import { StyleConstants } from "../../../Common/StyleConstants";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
export interface TreeNodeMenuItem { export interface LegacyTreeNode {
label: string;
onClick: () => void;
iconSrc?: string;
isDisabled?: boolean;
styleClass?: string;
}
export interface TreeNode {
label: string; label: string;
id?: string; id?: string;
children?: TreeNode[]; children?: LegacyTreeNode[];
contextMenu?: TreeNodeMenuItem[]; contextMenu?: TreeNodeMenuItem[];
iconSrc?: string; iconSrc?: string;
isExpanded?: boolean; isExpanded?: boolean;
@ -50,34 +43,37 @@ export interface TreeNode {
onContextMenuOpen?: () => void; onContextMenuOpen?: () => void;
} }
export interface TreeComponentProps { export interface LegacyTreeComponentProps {
rootNode: TreeNode; rootNode: LegacyTreeNode;
style?: any; style?: any;
className?: string; className?: string;
} }
export class TreeComponent extends React.Component<TreeComponentProps> { export class LegacyTreeComponent extends React.Component<LegacyTreeComponentProps> {
public render(): JSX.Element { public render(): JSX.Element {
return ( return (
<div style={this.props.style} className={`treeComponent ${this.props.className}`} role="tree"> <div style={this.props.style} className={`treeComponent ${this.props.className}`} role="tree">
<TreeNodeComponent paddingLeft={0} node={this.props.rootNode} generation={0} /> <LegacyTreeNodeComponent paddingLeft={0} node={this.props.rootNode} generation={0} />
</div> </div>
); );
} }
} }
/* Tree node is a react component */ /* Tree node is a react component */
interface TreeNodeComponentProps { interface LegacyTreeNodeComponentProps {
node: TreeNode; node: LegacyTreeNode;
generation: number; generation: number;
paddingLeft: number; paddingLeft: number;
} }
interface TreeNodeComponentState { interface LegacyTreeNodeComponentState {
isExpanded: boolean; isExpanded: boolean;
isMenuShowing: boolean; isMenuShowing: boolean;
} }
export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, TreeNodeComponentState> { export class LegacyTreeNodeComponent extends React.Component<
LegacyTreeNodeComponentProps,
LegacyTreeNodeComponentState
> {
private static readonly paddingPerGenerationPx = 16; private static readonly paddingPerGenerationPx = 16;
private static readonly iconOffset = 22; private static readonly iconOffset = 22;
private static readonly transitionDurationMS = 200; private static readonly transitionDurationMS = 200;
@ -85,7 +81,7 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
private contextMenuRef = React.createRef<HTMLDivElement>(); private contextMenuRef = React.createRef<HTMLDivElement>();
private isExpanded: boolean; private isExpanded: boolean;
constructor(props: TreeNodeComponentProps) { constructor(props: LegacyTreeNodeComponentProps) {
super(props); super(props);
this.isExpanded = props.node.isExpanded; this.isExpanded = props.node.isExpanded;
this.state = { this.state = {
@ -94,13 +90,13 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
}; };
} }
componentDidUpdate(prevProps: TreeNodeComponentProps, prevState: TreeNodeComponentState) { componentDidUpdate(prevProps: LegacyTreeNodeComponentProps, prevState: LegacyTreeNodeComponentState) {
// Only call when expand has actually changed // Only call when expand has actually changed
if (this.state.isExpanded !== prevState.isExpanded) { if (this.state.isExpanded !== prevState.isExpanded) {
if (this.state.isExpanded) { if (this.state.isExpanded) {
this.props.node.onExpanded && setTimeout(this.props.node.onExpanded, TreeNodeComponent.callbackDelayMS); this.props.node.onExpanded && setTimeout(this.props.node.onExpanded, LegacyTreeNodeComponent.callbackDelayMS);
} else { } else {
this.props.node.onCollapsed && setTimeout(this.props.node.onCollapsed, TreeNodeComponent.callbackDelayMS); this.props.node.onCollapsed && setTimeout(this.props.node.onCollapsed, LegacyTreeNodeComponent.callbackDelayMS);
} }
} }
if (this.props.node.isExpanded !== this.isExpanded) { if (this.props.node.isExpanded !== this.isExpanded) {
@ -115,18 +111,18 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
return this.renderNode(this.props.node, this.props.generation); return this.renderNode(this.props.node, this.props.generation);
} }
private static getSortedChildren(treeNode: TreeNode): TreeNode[] { private static getSortedChildren(treeNode: LegacyTreeNode): LegacyTreeNode[] {
if (!treeNode || !treeNode.children) { if (!treeNode || !treeNode.children) {
return undefined; return undefined;
} }
const compareFct = (a: TreeNode, b: TreeNode) => a.label.localeCompare(b.label); const compareFct = (a: LegacyTreeNode, b: LegacyTreeNode) => a.label.localeCompare(b.label);
let unsortedChildren; let unsortedChildren;
if (treeNode.isLeavesParentsSeparate) { if (treeNode.isLeavesParentsSeparate) {
// Separate parents and leave // Separate parents and leave
const parents: TreeNode[] = treeNode.children.filter((node) => node.children); const parents: LegacyTreeNode[] = treeNode.children.filter((node) => node.children);
const leaves: TreeNode[] = treeNode.children.filter((node) => !node.children); const leaves: LegacyTreeNode[] = treeNode.children.filter((node) => !node.children);
if (treeNode.isAlphaSorted) { if (treeNode.isAlphaSorted) {
parents.sort(compareFct); parents.sort(compareFct);
@ -141,18 +137,18 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
return unsortedChildren; return unsortedChildren;
} }
private static isNodeHeaderBlank(node: TreeNode): boolean { private static isNodeHeaderBlank(node: LegacyTreeNode): boolean {
return (node.label === undefined || node.label === null) && !node.contextMenu; return (node.label === undefined || node.label === null) && !node.contextMenu;
} }
private renderNode(node: TreeNode, generation: number): JSX.Element { private renderNode(node: LegacyTreeNode, generation: number): JSX.Element {
let paddingLeft = generation * TreeNodeComponent.paddingPerGenerationPx; const paddingLeft = generation * LegacyTreeNodeComponent.paddingPerGenerationPx;
let additionalOffsetPx = 15; let additionalOffsetPx = 15;
if (node.children) { if (node.children) {
const childrenWithSubChildren = node.children.filter((child: TreeNode) => !!child.children); const childrenWithSubChildren = node.children.filter((child: LegacyTreeNode) => !!child.children);
if (childrenWithSubChildren.length > 0) { if (childrenWithSubChildren.length > 0) {
additionalOffsetPx = TreeNodeComponent.iconOffset; additionalOffsetPx = LegacyTreeNodeComponent.iconOffset;
} }
} }
@ -160,16 +156,17 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
const showSelected = const showSelected =
this.props.node.isSelected && this.props.node.isSelected &&
this.props.node.isSelected() && this.props.node.isSelected() &&
!TreeNodeComponent.isAnyDescendantSelected(this.props.node); !LegacyTreeNodeComponent.isAnyDescendantSelected(this.props.node);
const headerStyle: React.CSSProperties = { paddingLeft: this.props.paddingLeft }; const headerStyle: React.CSSProperties = { paddingLeft: this.props.paddingLeft };
if (TreeNodeComponent.isNodeHeaderBlank(node)) { if (LegacyTreeNodeComponent.isNodeHeaderBlank(node)) {
headerStyle.height = 0; headerStyle.height = 0;
headerStyle.padding = 0; headerStyle.padding = 0;
} }
return ( return (
<div <div
data-test={`Tree/TreeNode:${node.label}`}
className={`${this.props.node.className || ""} main${generation} nodeItem ${showSelected ? "selected" : ""}`} className={`${this.props.node.className || ""} main${generation} nodeItem ${showSelected ? "selected" : ""}`}
onClick={(event: React.MouseEvent<HTMLDivElement>) => this.onNodeClick(event, node)} onClick={(event: React.MouseEvent<HTMLDivElement>) => this.onNodeClick(event, node)}
onKeyPress={(event: React.KeyboardEvent<HTMLDivElement>) => this.onNodeKeyPress(event, node)} onKeyPress={(event: React.KeyboardEvent<HTMLDivElement>) => this.onNodeKeyPress(event, node)}
@ -178,9 +175,9 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
> >
<div <div
className={`treeNodeHeader ${this.state.isMenuShowing ? "showingMenu" : ""}`} className={`treeNodeHeader ${this.state.isMenuShowing ? "showingMenu" : ""}`}
data-test={`Tree/TreeNode/Header:${node.label}`}
style={headerStyle} style={headerStyle}
tabIndex={node.children ? -1 : 0} tabIndex={node.children ? -1 : 0}
data-test={node.label}
> >
{this.renderCollapseExpandIcon(node)} {this.renderCollapseExpandIcon(node)}
{node.iconSrc && <img className="nodeIcon" src={node.iconSrc} alt="" />} {node.iconSrc && <img className="nodeIcon" src={node.iconSrc} alt="" />}
@ -195,10 +192,13 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
<img className="loadingIcon" src={LoadingIndicator_3Squares} hidden={!this.props.node.isLoading} /> <img className="loadingIcon" src={LoadingIndicator_3Squares} hidden={!this.props.node.isLoading} />
</div> </div>
{node.children && ( {node.children && (
<AnimateHeight duration={TreeNodeComponent.transitionDurationMS} height={this.state.isExpanded ? "auto" : 0}> <AnimateHeight
duration={LegacyTreeNodeComponent.transitionDurationMS}
height={this.state.isExpanded ? "auto" : 0}
>
<div className="nodeChildren" data-test={node.label} role="group"> <div className="nodeChildren" data-test={node.label} role="group">
{TreeNodeComponent.getSortedChildren(node).map((childNode: TreeNode) => ( {LegacyTreeNodeComponent.getSortedChildren(node).map((childNode: LegacyTreeNode) => (
<TreeNodeComponent <LegacyTreeNodeComponent
key={`${childNode.label}-${generation + 1}-${childNode.timestamp}`} key={`${childNode.label}-${generation + 1}-${childNode.timestamp}`}
node={childNode} node={childNode}
generation={generation + 1} generation={generation + 1}
@ -216,12 +216,14 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
* Recursive: is the node or any descendant selected * Recursive: is the node or any descendant selected
* @param node * @param node
*/ */
private static isAnyDescendantSelected(node: TreeNode): boolean { private static isAnyDescendantSelected(node: LegacyTreeNode): boolean {
return ( return (
node.children && node.children &&
node.children.reduce( node.children.reduce(
(previous: boolean, child: TreeNode) => (previous: boolean, child: LegacyTreeNode) =>
previous || (child.isSelected && child.isSelected()) || TreeNodeComponent.isAnyDescendantSelected(child), previous ||
(child.isSelected && child.isSelected()) ||
LegacyTreeNodeComponent.isAnyDescendantSelected(child),
false, false,
) )
); );
@ -232,10 +234,10 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
} }
private onRightClick = (): void => { private onRightClick = (): void => {
this.contextMenuRef.current.firstChild.dispatchEvent(TreeNodeComponent.createClickEvent()); this.contextMenuRef.current.firstChild.dispatchEvent(LegacyTreeNodeComponent.createClickEvent());
}; };
private renderContextMenuButton(node: TreeNode): JSX.Element { private renderContextMenuButton(node: LegacyTreeNode): JSX.Element {
const menuItemLabel = "More"; const menuItemLabel = "More";
const buttonStyles: Partial<IButtonStyles> = { const buttonStyles: Partial<IButtonStyles> = {
rootFocused: { outline: `1px dashed ${StyleConstants.FocusColor}` }, rootFocused: { outline: `1px dashed ${StyleConstants.FocusColor}` },
@ -263,9 +265,9 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
onMenuDismissed: (contextualMenu?: IContextualMenuProps) => this.setState({ isMenuShowing: false }), onMenuDismissed: (contextualMenu?: IContextualMenuProps) => this.setState({ isMenuShowing: false }),
contextualMenuItemAs: (props: IContextualMenuItemProps) => ( contextualMenuItemAs: (props: IContextualMenuItemProps) => (
<div <div
data-test={`treeComponentMenuItemContainer`} data-test={`Tree/TreeNode/MenuItem:${props.item.text}`}
className="treeComponentMenuItemContainer" className="treeComponentMenuItemContainer"
onContextMenu={(e) => e.target.dispatchEvent(TreeNodeComponent.createClickEvent())} onContextMenu={(e) => e.target.dispatchEvent(LegacyTreeNodeComponent.createClickEvent())}
> >
{props.item.onRenderIcon()} {props.item.onRenderIcon()}
<span <span
@ -297,7 +299,7 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
); );
} }
private renderCollapseExpandIcon(node: TreeNode): JSX.Element { private renderCollapseExpandIcon(node: LegacyTreeNode): JSX.Element {
if (!node.children || !node.label) { if (!node.children || !node.label) {
return <></>; return <></>;
} }
@ -314,12 +316,12 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
); );
} }
private onNodeClick = (event: React.MouseEvent<HTMLDivElement>, node: TreeNode): void => { private onNodeClick = (event: React.MouseEvent<HTMLDivElement>, node: LegacyTreeNode): void => {
event.stopPropagation(); event.stopPropagation();
if (node.children) { if (node.children) {
const isExpanded = !this.state.isExpanded; const isExpanded = !this.state.isExpanded;
// Prevent collapsing if node header is blank // Prevent collapsing if node header is blank
if (!(TreeNodeComponent.isNodeHeaderBlank(node) && !isExpanded)) { if (!(LegacyTreeNodeComponent.isNodeHeaderBlank(node) && !isExpanded)) {
this.setState({ isExpanded }); this.setState({ isExpanded });
} }
} }
@ -327,14 +329,14 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
this.props.node.onClick && this.props.node.onClick(this.state.isExpanded); this.props.node.onClick && this.props.node.onClick(this.state.isExpanded);
}; };
private onNodeKeyPress = (event: React.KeyboardEvent<HTMLDivElement>, node: TreeNode): void => { private onNodeKeyPress = (event: React.KeyboardEvent<HTMLDivElement>, node: LegacyTreeNode): void => {
if (event.charCode === Constants.KeyCodes.Space || event.charCode === Constants.KeyCodes.Enter) { if (event.charCode === Constants.KeyCodes.Space || event.charCode === Constants.KeyCodes.Enter) {
event.stopPropagation(); event.stopPropagation();
this.props.node.onClick && this.props.node.onClick(this.state.isExpanded); this.props.node.onClick && this.props.node.onClick(this.state.isExpanded);
} }
}; };
private onCollapseExpandIconKeyPress = (event: React.KeyboardEvent<HTMLDivElement>, node: TreeNode): void => { private onCollapseExpandIconKeyPress = (event: React.KeyboardEvent<HTMLDivElement>, node: LegacyTreeNode): void => {
if (event.charCode === Constants.KeyCodes.Space || event.charCode === Constants.KeyCodes.Enter) { if (event.charCode === Constants.KeyCodes.Space || event.charCode === Constants.KeyCodes.Enter) {
event.stopPropagation(); event.stopPropagation();
if (node.children) { if (node.children) {

View File

@ -0,0 +1,194 @@
import { TreeItem, TreeItemLayout, tokens } from "@fluentui/react-components";
import PromiseSource from "Utils/PromiseSource";
import { mount, shallow } from "enzyme";
import React from "react";
import { act } from "react-dom/test-utils";
import { TreeNode, TreeNodeComponent } from "./TreeNodeComponent";
function generateTestNode(id: string, additionalProps?: Partial<TreeNode>): TreeNode {
const node: TreeNode = {
id,
label: `${id}Label`,
className: `${id}Class`,
iconSrc: `${id}Icon`,
onClick: jest.fn().mockName(`${id}Click`),
...additionalProps,
};
return node;
}
describe("TreeNodeComponent", () => {
it("renders a single node", () => {
const node = generateTestNode("root");
const component = shallow(<TreeNodeComponent node={node} treeNodeId={node.id} />);
expect(component).toMatchSnapshot();
// The "click" handler is actually attached to onOpenChange, with a type of "Click".
component
.find(TreeItem)
.props()
.onOpenChange(null!, { open: true, value: "borp", target: null!, event: null!, type: "Click" });
expect(node.onClick).toHaveBeenCalled();
});
it("renders a node with a menu", () => {
const node = generateTestNode("root", {
contextMenu: [
{
label: "enabledItem",
onClick: jest.fn().mockName("enabledItemClick"),
},
{
label: "disabledItem",
onClick: jest.fn().mockName("disabledItemClick"),
isDisabled: true,
},
],
});
const component = shallow(<TreeNodeComponent node={node} treeNodeId={node.id} />);
expect(component).toMatchSnapshot();
});
it("renders a loading spinner if the node is loading", async () => {
const loading = new PromiseSource();
const node = generateTestNode("root", {
onExpanded: () => loading.promise,
});
const component = shallow(<TreeNodeComponent node={node} treeNodeId={node.id} />);
act(() => {
component
.find(TreeItem)
.props()
.onOpenChange(null!, { open: true, value: "borp", target: null!, event: null!, type: "ExpandIconClick" });
});
expect(component).toMatchSnapshot("loading");
await loading.resolveAndWait();
expect(component).toMatchSnapshot("loaded");
});
it("renders single selected leaf node as selected", () => {
const node = generateTestNode("root", {
isSelected: () => true,
});
const component = shallow(<TreeNodeComponent node={node} treeNodeId={node.id} />);
expect(component.find(TreeItemLayout).props().style?.backgroundColor).toStrictEqual(
tokens.colorNeutralBackground1Selected,
);
expect(component).toMatchSnapshot();
});
it("renders selected parent node as selected if no descendant nodes are selected", () => {
const node = generateTestNode("root", {
isSelected: () => true,
children: [
generateTestNode("child1", {
children: [generateTestNode("grandchild1"), generateTestNode("grandchild2")],
}),
generateTestNode("child2"),
],
});
const component = shallow(<TreeNodeComponent node={node} treeNodeId={node.id} />);
expect(component.find(TreeItemLayout).props().style?.backgroundColor).toStrictEqual(
tokens.colorNeutralBackground1Selected,
);
expect(component).toMatchSnapshot();
});
it("renders selected parent node as unselected if any descendant node is selected", () => {
const node = generateTestNode("root", {
isSelected: () => true,
children: [
generateTestNode("child1", {
children: [
generateTestNode("grandchild1", {
isSelected: () => true,
}),
generateTestNode("grandchild2"),
],
}),
generateTestNode("child2"),
],
});
const component = shallow(<TreeNodeComponent node={node} treeNodeId={node.id} />);
expect(component.find(TreeItemLayout).props().style?.backgroundColor).toBeUndefined();
expect(component).toMatchSnapshot();
});
it("renders an icon if the node has one", () => {
const node = generateTestNode("root", {
iconSrc: "the-icon.svg",
});
const component = shallow(<TreeNodeComponent node={node} treeNodeId={node.id} />);
expect(component).toMatchSnapshot();
});
it("renders a node as expandable if it has empty, but defined, children array", () => {
const node = generateTestNode("root", {
isLoading: true,
children: [
generateTestNode("child1", {
children: [],
}),
generateTestNode("child2"),
],
});
const component = shallow(<TreeNodeComponent node={node} treeNodeId={node.id} />);
expect(component).toMatchSnapshot();
});
it("does not render children if the node is loading", () => {
const node = generateTestNode("root", {
isLoading: true,
children: [
generateTestNode("child1", {
children: [generateTestNode("grandchild1"), generateTestNode("grandchild2")],
}),
generateTestNode("child2"),
],
});
const component = shallow(<TreeNodeComponent node={node} treeNodeId={node.id} />);
expect(component).toMatchSnapshot();
});
it("fully renders a tree", () => {
const child3Loading = new PromiseSource();
const node = generateTestNode("root", {
isSelected: () => true,
children: [
generateTestNode("child1", {
children: [
generateTestNode("grandchild1", {
iconSrc: "grandchild1Icon.svg",
isSelected: () => true,
}),
generateTestNode("grandchild2"),
],
}),
generateTestNode("child2Loading", {
isLoading: true,
children: [generateTestNode("grandchild3NotRendered")],
}),
generateTestNode("child3Expanding", {
onExpanded: () => child3Loading.promise,
}),
],
});
const component = mount(<TreeNodeComponent node={node} treeNodeId={node.id} />);
// Find and expand the child3Expanding node
const expandingChild = component.find(TreeItem).filterWhere((n) => n.props().value === "root/child3ExpandingLabel");
act(() => {
expandingChild.props().onOpenChange(null!, {
open: true,
value: "root/child3ExpandingLabel",
target: null!,
event: null!,
type: "Click",
});
});
expect(component).toMatchSnapshot();
});
});

View File

@ -0,0 +1,207 @@
import {
Button,
Menu,
MenuItem,
MenuList,
MenuOpenChangeData,
MenuOpenEvent,
MenuPopover,
MenuTrigger,
Spinner,
Tree,
TreeItem,
TreeItemLayout,
TreeOpenChangeData,
TreeOpenChangeEvent,
} from "@fluentui/react-components";
import { MoreHorizontal20Regular } from "@fluentui/react-icons";
import { tokens } from "@fluentui/react-theme";
import * as React from "react";
import { useCallback } from "react";
export interface TreeNodeMenuItem {
label: string;
onClick: () => void;
iconSrc?: string;
isDisabled?: boolean;
styleClass?: string;
}
export interface TreeNode {
label: string;
id?: string;
children?: TreeNode[];
contextMenu?: TreeNodeMenuItem[];
iconSrc?: string;
isExpanded?: boolean;
className?: string;
isAlphaSorted?: boolean;
// data?: any; // Piece of data corresponding to this node
timestamp?: number;
isLeavesParentsSeparate?: boolean; // Display parents together first, then leaves
isLoading?: boolean;
isScrollable?: boolean;
isSelected?: () => boolean;
onClick?: () => void; // Only if a leaf, other click will expand/collapse
onExpanded?: () => Promise<void>;
onCollapsed?: () => void;
onContextMenuOpen?: () => void;
}
export interface TreeNodeComponentProps {
node: TreeNode;
className?: string;
treeNodeId: string;
}
/** Function that returns true if any descendant (at any depth) of this node is selected. */
function isAnyDescendantSelected(node: TreeNode): boolean {
return (
node.children &&
node.children.reduce(
(previous: boolean, child: TreeNode) =>
previous || (child.isSelected && child.isSelected()) || isAnyDescendantSelected(child),
false,
)
);
}
const getTreeIcon = (iconSrc: string): JSX.Element => <img src={iconSrc} alt="" style={{ width: 16, height: 16 }} />;
export const TreeNodeComponent: React.FC<TreeNodeComponentProps> = ({
node,
treeNodeId,
}: TreeNodeComponentProps): JSX.Element => {
const [isLoading, setIsLoading] = React.useState<boolean>(false);
const getSortedChildren = (treeNode: TreeNode): TreeNode[] => {
if (!treeNode || !treeNode.children) {
return undefined;
}
const compareFct = (a: TreeNode, b: TreeNode) => a.label.localeCompare(b.label);
let unsortedChildren;
if (treeNode.isLeavesParentsSeparate) {
// Separate parents and leave
const parents: TreeNode[] = treeNode.children.filter((node) => node.children);
const leaves: TreeNode[] = treeNode.children.filter((node) => !node.children);
if (treeNode.isAlphaSorted) {
parents.sort(compareFct);
leaves.sort(compareFct);
}
unsortedChildren = parents.concat(leaves);
} else {
unsortedChildren = treeNode.isAlphaSorted ? treeNode.children.sort(compareFct) : treeNode.children;
}
return unsortedChildren;
};
// A branch node is any node with a defined children array, even if the array is empty.
const isBranch = !!node.children;
const onOpenChange = useCallback(
(_: TreeOpenChangeEvent, data: TreeOpenChangeData) => {
if (data.type === "Click" && !isBranch && node.onClick) {
node.onClick();
}
if (!node.isExpanded && data.open && node.onExpanded) {
// Catch the transition non-expanded to expanded
setIsLoading(true);
node.onExpanded?.().then(() => setIsLoading(false));
} else if (node.isExpanded && !data.open && node.onCollapsed) {
// Catch the transition expanded to non-expanded
node.onCollapsed?.();
}
},
[isBranch, node, setIsLoading],
);
const onMenuOpenChange = useCallback(
(e: MenuOpenEvent, data: MenuOpenChangeData) => {
if (data.open) {
node.onContextMenuOpen?.();
}
},
[node],
);
// We show a node as selected if it is selected AND no descendant is selected.
// We want to show only the deepest selected node as selected.
const isCurrentNodeSelected = node.isSelected && node.isSelected();
const shouldShowAsSelected = isCurrentNodeSelected && !isAnyDescendantSelected(node);
const contextMenuItems = (node.contextMenu ?? []).map((menuItem) => (
<MenuItem
data-test={`TreeNode/ContextMenuItem:${menuItem.label}`}
disabled={menuItem.isDisabled}
key={menuItem.label}
onClick={menuItem.onClick}
>
{menuItem.label}
</MenuItem>
));
const treeItem = (
<TreeItem
data-test={`TreeNodeContainer:${treeNodeId}`}
value={treeNodeId}
itemType={isBranch ? "branch" : "leaf"}
onOpenChange={onOpenChange}
>
<TreeItemLayout
className={node.className}
data-test={`TreeNode:${treeNodeId}`}
actions={
contextMenuItems.length > 0 && (
<Menu onOpenChange={onMenuOpenChange}>
<MenuTrigger disableButtonEnhancement>
<Button
aria-label="More options"
data-test="TreeNode/ContextMenuTrigger"
appearance="subtle"
icon={<MoreHorizontal20Regular />}
/>
</MenuTrigger>
<MenuPopover data-test={`TreeNode/ContextMenu:${treeNodeId}`}>
<MenuList>{contextMenuItems}</MenuList>
</MenuPopover>
</Menu>
)
}
expandIcon={isLoading ? <Spinner size="extra-tiny" /> : undefined}
iconBefore={node.iconSrc && getTreeIcon(node.iconSrc)}
style={{
backgroundColor: shouldShowAsSelected ? tokens.colorNeutralBackground1Selected : undefined,
}}
>
{node.label}
</TreeItemLayout>
{!node.isLoading && node.children?.length > 0 && (
<Tree style={{ overflow: node.isScrollable ? "auto" : undefined }}>
{getSortedChildren(node).map((childNode: TreeNode) => (
<TreeNodeComponent key={childNode.label} node={childNode} treeNodeId={`${treeNodeId}/${childNode.label}`} />
))}
</Tree>
)}
</TreeItem>
);
if (contextMenuItems.length === 0) {
return treeItem;
}
// For accessibility, it's highly recommended that any 'actions' also be available in the context menu.
// See https://react.fluentui.dev/?path=/docs/components-tree--default#actions
return (
<Menu positioning="below-end" openOnContext onOpenChange={onMenuOpenChange}>
<MenuTrigger disableButtonEnhancement>{treeItem}</MenuTrigger>
<MenuPopover>
<MenuList>{contextMenuItems}</MenuList>
</MenuPopover>
</Menu>
);
};

View File

@ -1,11 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`TreeComponent renders a simple tree 1`] = ` exports[`LegacyTreeComponent renders a simple tree 1`] = `
<div <div
className="treeComponent tree" className="treeComponent tree"
role="tree" role="tree"
> >
<TreeNodeComponent <LegacyTreeNodeComponent
generation={0} generation={0}
node={ node={
Object { Object {
@ -33,16 +33,17 @@ exports[`TreeComponent renders a simple tree 1`] = `
</div> </div>
`; `;
exports[`TreeNodeComponent does not render children by default 1`] = ` exports[`LegacyTreeNodeComponent does not render children by default 1`] = `
<div <div
className=" main2 nodeItem " className=" main2 nodeItem "
data-test="Tree/TreeNode:label"
onClick={[Function]} onClick={[Function]}
onKeyPress={[Function]} onKeyPress={[Function]}
role="treeitem" role="treeitem"
> >
<div <div
className="treeNodeHeader " className="treeNodeHeader "
data-test="label" data-test="Tree/TreeNode/Header:label"
style={ style={
Object { Object {
"paddingLeft": 9, "paddingLeft": 9,
@ -102,7 +103,7 @@ exports[`TreeNodeComponent does not render children by default 1`] = `
data-test="label" data-test="label"
role="group" role="group"
> >
<TreeNodeComponent <LegacyTreeNodeComponent
generation={3} generation={3}
key="Bchild1-3-undefined" key="Bchild1-3-undefined"
node={ node={
@ -120,7 +121,7 @@ exports[`TreeNodeComponent does not render children by default 1`] = `
} }
paddingLeft={32} paddingLeft={32}
/> />
<TreeNodeComponent <LegacyTreeNodeComponent
generation={3} generation={3}
key="2child2-3-undefined" key="2child2-3-undefined"
node={ node={
@ -135,9 +136,10 @@ exports[`TreeNodeComponent does not render children by default 1`] = `
</div> </div>
`; `;
exports[`TreeNodeComponent renders a simple node (sorted children, expanded) 1`] = ` exports[`LegacyTreeNodeComponent renders a simple node (sorted children, expanded) 1`] = `
<div <div
className="nodeClassname main12 nodeItem " className="nodeClassname main12 nodeItem "
data-test="Tree/TreeNode:label"
id="id" id="id"
onClick={[Function]} onClick={[Function]}
onKeyPress={[Function]} onKeyPress={[Function]}
@ -145,7 +147,7 @@ exports[`TreeNodeComponent renders a simple node (sorted children, expanded) 1`]
> >
<div <div
className="treeNodeHeader " className="treeNodeHeader "
data-test="label" data-test="Tree/TreeNode/Header:label"
style={ style={
Object { Object {
"paddingLeft": 23, "paddingLeft": 23,
@ -254,7 +256,7 @@ exports[`TreeNodeComponent renders a simple node (sorted children, expanded) 1`]
data-test="label" data-test="label"
role="group" role="group"
> >
<TreeNodeComponent <LegacyTreeNodeComponent
generation={13} generation={13}
key="2child2-13-undefined" key="2child2-13-undefined"
node={ node={
@ -264,7 +266,7 @@ exports[`TreeNodeComponent renders a simple node (sorted children, expanded) 1`]
} }
paddingLeft={214} paddingLeft={214}
/> />
<TreeNodeComponent <LegacyTreeNodeComponent
generation={13} generation={13}
key="Bchild1-13-undefined" key="Bchild1-13-undefined"
node={ node={
@ -287,16 +289,17 @@ exports[`TreeNodeComponent renders a simple node (sorted children, expanded) 1`]
</div> </div>
`; `;
exports[`TreeNodeComponent renders loading icon 1`] = ` exports[`LegacyTreeNodeComponent renders loading icon 1`] = `
<div <div
className=" main2 nodeItem " className=" main2 nodeItem "
data-test="Tree/TreeNode:label"
onClick={[Function]} onClick={[Function]}
onKeyPress={[Function]} onKeyPress={[Function]}
role="treeitem" role="treeitem"
> >
<div <div
className="treeNodeHeader " className="treeNodeHeader "
data-test="label" data-test="Tree/TreeNode/Header:label"
style={ style={
Object { Object {
"paddingLeft": 9, "paddingLeft": 9,
@ -360,9 +363,10 @@ exports[`TreeNodeComponent renders loading icon 1`] = `
</div> </div>
`; `;
exports[`TreeNodeComponent renders sorted children, expanded, leaves and parents separated 1`] = ` exports[`LegacyTreeNodeComponent renders sorted children, expanded, leaves and parents separated 1`] = `
<div <div
className="nodeClassname main12 nodeItem " className="nodeClassname main12 nodeItem "
data-test="Tree/TreeNode:label"
id="id" id="id"
onClick={[Function]} onClick={[Function]}
onKeyPress={[Function]} onKeyPress={[Function]}
@ -370,7 +374,7 @@ exports[`TreeNodeComponent renders sorted children, expanded, leaves and parents
> >
<div <div
className="treeNodeHeader " className="treeNodeHeader "
data-test="label" data-test="Tree/TreeNode/Header:label"
style={ style={
Object { Object {
"paddingLeft": 23, "paddingLeft": 23,
@ -470,7 +474,7 @@ exports[`TreeNodeComponent renders sorted children, expanded, leaves and parents
data-test="label" data-test="label"
role="group" role="group"
> >
<TreeNodeComponent <LegacyTreeNodeComponent
generation={13} generation={13}
key="bchild-13-undefined" key="bchild-13-undefined"
node={ node={
@ -488,7 +492,7 @@ exports[`TreeNodeComponent renders sorted children, expanded, leaves and parents
} }
paddingLeft={192} paddingLeft={192}
/> />
<TreeNodeComponent <LegacyTreeNodeComponent
generation={13} generation={13}
key="dchild-13-undefined" key="dchild-13-undefined"
node={ node={
@ -506,7 +510,7 @@ exports[`TreeNodeComponent renders sorted children, expanded, leaves and parents
} }
paddingLeft={192} paddingLeft={192}
/> />
<TreeNodeComponent <LegacyTreeNodeComponent
generation={13} generation={13}
key="aChild-13-undefined" key="aChild-13-undefined"
node={ node={
@ -516,7 +520,7 @@ exports[`TreeNodeComponent renders sorted children, expanded, leaves and parents
} }
paddingLeft={214} paddingLeft={214}
/> />
<TreeNodeComponent <LegacyTreeNodeComponent
generation={13} generation={13}
key="cchild-13-undefined" key="cchild-13-undefined"
node={ node={
@ -531,16 +535,17 @@ exports[`TreeNodeComponent renders sorted children, expanded, leaves and parents
</div> </div>
`; `;
exports[`TreeNodeComponent renders unsorted children by default 1`] = ` exports[`LegacyTreeNodeComponent renders unsorted children by default 1`] = `
<div <div
className=" main2 nodeItem " className=" main2 nodeItem "
data-test="Tree/TreeNode:label"
onClick={[Function]} onClick={[Function]}
onKeyPress={[Function]} onKeyPress={[Function]}
role="treeitem" role="treeitem"
> >
<div <div
className="treeNodeHeader " className="treeNodeHeader "
data-test="label" data-test="Tree/TreeNode/Header:label"
style={ style={
Object { Object {
"paddingLeft": 9, "paddingLeft": 9,
@ -600,7 +605,7 @@ exports[`TreeNodeComponent renders unsorted children by default 1`] = `
data-test="label" data-test="label"
role="group" role="group"
> >
<TreeNodeComponent <LegacyTreeNodeComponent
generation={3} generation={3}
key="Bchild1-3-undefined" key="Bchild1-3-undefined"
node={ node={
@ -618,7 +623,7 @@ exports[`TreeNodeComponent renders unsorted children by default 1`] = `
} }
paddingLeft={32} paddingLeft={32}
/> />
<TreeNodeComponent <LegacyTreeNodeComponent
generation={3} generation={3}
key="2child2-3-undefined" key="2child2-3-undefined"
node={ node={

File diff suppressed because it is too large Load Diff

View File

@ -1,147 +0,0 @@
import {
Button,
Menu,
MenuItem,
MenuList,
MenuPopover,
MenuTrigger,
Spinner,
Tree,
TreeItem,
TreeItemLayout,
TreeOpenChangeData,
TreeOpenChangeEvent,
} from "@fluentui/react-components";
import { MoreHorizontal20Regular } from "@fluentui/react-icons";
import { tokens } from "@fluentui/react-theme";
import * as React from "react";
export interface TreeNode2MenuItem {
label: string;
onClick: () => void;
iconSrc?: string;
isDisabled?: boolean;
styleClass?: string;
}
export interface TreeNode2 {
label: string;
id?: string;
children?: TreeNode2[];
contextMenu?: TreeNode2MenuItem[];
iconSrc?: string;
isExpanded?: boolean;
className?: string;
isAlphaSorted?: boolean;
// data?: any; // Piece of data corresponding to this node
timestamp?: number;
isLeavesParentsSeparate?: boolean; // Display parents together first, then leaves
isLoading?: boolean;
isScrollable?: boolean;
isSelected?: () => boolean;
onClick?: () => void; // Only if a leaf, other click will expand/collapse
onExpanded?: () => Promise<void>;
onCollapsed?: () => void;
onContextMenuOpen?: () => void;
}
export interface TreeNode2ComponentProps {
node: TreeNode2;
className?: string;
treeNodeId: string;
}
const getTreeIcon = (iconSrc: string): JSX.Element => <img src={iconSrc} alt="" style={{ width: 20, height: 20 }} />;
export const TreeNode2Component: React.FC<TreeNode2ComponentProps> = ({
node,
treeNodeId,
}: TreeNode2ComponentProps): JSX.Element => {
const [isLoading, setIsLoading] = React.useState<boolean>(false);
const getSortedChildren = (treeNode: TreeNode2): TreeNode2[] => {
if (!treeNode || !treeNode.children) {
return undefined;
}
const compareFct = (a: TreeNode2, b: TreeNode2) => a.label.localeCompare(b.label);
let unsortedChildren;
if (treeNode.isLeavesParentsSeparate) {
// Separate parents and leave
const parents: TreeNode2[] = treeNode.children.filter((node) => node.children);
const leaves: TreeNode2[] = treeNode.children.filter((node) => !node.children);
if (treeNode.isAlphaSorted) {
parents.sort(compareFct);
leaves.sort(compareFct);
}
unsortedChildren = parents.concat(leaves);
} else {
unsortedChildren = treeNode.isAlphaSorted ? treeNode.children.sort(compareFct) : treeNode.children;
}
return unsortedChildren;
};
const onOpenChange = (_: TreeOpenChangeEvent, data: TreeOpenChangeData) => {
if (!node.isExpanded && data.open && node.onExpanded) {
// Catch the transition non-expanded to expanded
setIsLoading(true);
node.onExpanded?.().then(() => setIsLoading(false));
} else if (node.isExpanded && !data.open && node.onCollapsed) {
// Catch the transition expanded to non-expanded
node.onCollapsed?.();
}
};
return (
<TreeItem
value={treeNodeId}
itemType={node.children !== undefined ? "branch" : "leaf"}
style={{ height: "100%" }}
onOpenChange={onOpenChange}
>
<TreeItemLayout
className={node.className}
actions={
node.contextMenu && (
<Menu>
<MenuTrigger disableButtonEnhancement>
<Button aria-label="More options" appearance="subtle" icon={<MoreHorizontal20Regular />} />
</MenuTrigger>
<MenuPopover>
<MenuList>
{node.contextMenu.map((menuItem) => (
<MenuItem disabled={menuItem.isDisabled} key={menuItem.label} onClick={menuItem.onClick}>
{menuItem.label}
</MenuItem>
))}
</MenuList>
</MenuPopover>
</Menu>
)
}
expandIcon={isLoading ? <Spinner size="extra-tiny" /> : undefined}
iconBefore={node.iconSrc && getTreeIcon(node.iconSrc)}
style={{
backgroundColor: node.isSelected && node.isSelected() ? tokens.colorNeutralBackground1Selected : undefined,
}}
>
<span onClick={() => node.onClick?.()}>{node.label}</span>
</TreeItemLayout>
{!node.isLoading && node.children?.length > 0 && (
<Tree style={{ overflow: node.isScrollable ? "auto" : undefined }}>
{getSortedChildren(node).map((childNode: TreeNode2) => (
<TreeNode2Component
key={childNode.label}
node={childNode}
treeNodeId={`${treeNodeId}/${childNode.label}`}
/>
))}
</Tree>
)}
</TreeItem>
);
};

View File

@ -38,7 +38,6 @@ import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils";
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils"; import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils";
import { update } from "../Utils/arm/generatedClients/cosmos/databaseAccounts"; import { update } from "../Utils/arm/generatedClients/cosmos/databaseAccounts";
import { listByDatabaseAccount } from "../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces";
import { useSidePanel } from "../hooks/useSidePanel"; import { useSidePanel } from "../hooks/useSidePanel";
import { useTabs } from "../hooks/useTabs"; import { useTabs } from "../hooks/useTabs";
import "./ComponentRegisterer"; import "./ComponentRegisterer";
@ -56,7 +55,6 @@ import { AddCollectionPanel } from "./Panes/AddCollectionPanel";
import { CassandraAddCollectionPane } from "./Panes/CassandraAddCollectionPane/CassandraAddCollectionPane"; import { CassandraAddCollectionPane } from "./Panes/CassandraAddCollectionPane/CassandraAddCollectionPane";
import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane/ExecuteSprocParamsPane"; import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane/ExecuteSprocParamsPane";
import { StringInputPane } from "./Panes/StringInputPane/StringInputPane"; import { StringInputPane } from "./Panes/StringInputPane/StringInputPane";
import { UploadFilePane } from "./Panes/UploadFilePane/UploadFilePane";
import { UploadItemsPane } from "./Panes/UploadItemsPane/UploadItemsPane"; import { UploadItemsPane } from "./Panes/UploadItemsPane/UploadItemsPane";
import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient"; import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient";
import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab"; import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab";
@ -138,14 +136,6 @@ export default class Explorer {
this.isTabsContentExpanded = ko.observable(false); this.isTabsContentExpanded = ko.observable(false);
document.addEventListener(
"contextmenu",
(e) => {
e.preventDefault();
},
false,
);
$(() => { $(() => {
$(document.body).click(() => $(".commandDropdownContainer").hide()); $(document.body).click(() => $(".commandDropdownContainer").hide());
}); });
@ -510,104 +500,6 @@ export default class Explorer {
.then((memoryUsageInfo) => useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo)); .then((memoryUsageInfo) => useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo));
} }
public resetNotebookWorkspace(): void {
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookClient) {
handleError(
"Attempt to reset notebook workspace, but notebook is not enabled",
"Explorer/resetNotebookWorkspace",
);
return;
}
const dialogContent = useNotebook.getState().isPhoenixNotebooks
? "Notebooks saved in the temporary workspace will be deleted. Do you want to proceed?"
: "This lets you keep your notebook files and the workspace will be restored to default. Proceed anyway?";
const resetConfirmationDialogProps: DialogProps = {
isModal: true,
title: "Reset Workspace",
subText: dialogContent,
primaryButtonText: "OK",
secondaryButtonText: "Cancel",
onPrimaryButtonClick: this._resetNotebookWorkspace,
onSecondaryButtonClick: () => useDialog.getState().closeDialog(),
};
useDialog.getState().openDialog(resetConfirmationDialogProps);
}
private async _containsDefaultNotebookWorkspace(databaseAccount: DataModels.DatabaseAccount): Promise<boolean> {
if (!databaseAccount) {
return false;
}
try {
const { value: workspaces } = await listByDatabaseAccount(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
);
return workspaces && workspaces.length > 0 && workspaces.some((workspace) => workspace.name === "default");
} catch (error) {
Logger.logError(getErrorMessage(error), "Explorer/_containsDefaultNotebookWorkspace");
return false;
}
}
private _resetNotebookWorkspace = async () => {
useDialog.getState().closeDialog();
const clearInProgressMessage = logConsoleProgress("Resetting notebook workspace");
let connectionStatus: ContainerConnectionInfo;
try {
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
if (!notebookServerInfo || !notebookServerInfo.notebookServerEndpoint) {
const error = "No server endpoint detected";
Logger.logError(error, "NotebookContainerClient/resetWorkspace");
logConsoleError(error);
return;
}
TelemetryProcessor.traceStart(Action.PhoenixResetWorkspace, {
dataExplorerArea: Areas.Notebook,
});
if (useNotebook.getState().isPhoenixNotebooks) {
useTabs.getState().closeAllNotebookTabs(true);
connectionStatus = {
status: ConnectionStatusType.Connecting,
};
useNotebook.getState().setConnectionInfo(connectionStatus);
}
const connectionInfo = await this.notebookManager?.notebookClient.resetWorkspace();
if (connectionInfo?.status !== HttpStatusCodes.OK) {
throw new Error(`Reset Workspace: Received status code- ${connectionInfo?.status}`);
}
if (!connectionInfo?.data?.phoenixServiceUrl) {
throw new Error(`Reset Workspace: PhoenixServiceUrl is invalid!`);
}
if (useNotebook.getState().isPhoenixNotebooks) {
await this.setNotebookInfo(true, connectionInfo, connectionStatus);
useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed);
}
logConsoleInfo("Successfully reset notebook workspace");
TelemetryProcessor.traceSuccess(Action.PhoenixResetWorkspace, {
dataExplorerArea: Areas.Notebook,
});
} catch (error) {
logConsoleError(`Failed to reset notebook workspace: ${error}`);
TelemetryProcessor.traceFailure(Action.PhoenixResetWorkspace, {
dataExplorerArea: Areas.Notebook,
error: getErrorMessage(error),
errorStack: getErrorStack(error),
});
if (useNotebook.getState().isPhoenixNotebooks) {
connectionStatus = {
status: ConnectionStatusType.Failed,
};
useNotebook.getState().resetContainerConnection(connectionStatus);
useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed);
}
throw error;
} finally {
clearInProgressMessage();
}
};
private getDeltaDatabases( private getDeltaDatabases(
updatedDatabaseList: DataModels.Database[], updatedDatabaseList: DataModels.Database[],
databases: ViewModels.Database[], databases: ViewModels.Database[],
@ -1010,92 +902,6 @@ export default class Explorer {
); );
} }
/**
* This creates a new notebook file, then opens the notebook
*/
public async onNewNotebookClicked(parent?: NotebookContentItem, isGithubTree?: boolean): Promise<void> {
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
const error = "Attempt to create new notebook, but notebook is not enabled";
handleError(error, "Explorer/onNewNotebookClicked");
throw new Error(error);
}
if (useNotebook.getState().isPhoenixNotebooks) {
if (isGithubTree) {
await this.allocateContainer(PoolIdType.DefaultPoolId);
parent = parent || this.resourceTree.myNotebooksContentRoot;
this.createNewNoteBook(parent, isGithubTree);
} else {
useDialog.getState().showOkCancelModalDialog(
Notebook.newNotebookModalTitle,
undefined,
"Create",
async () => {
await this.allocateContainer(PoolIdType.DefaultPoolId);
parent = parent || this.resourceTree.myNotebooksContentRoot;
this.createNewNoteBook(parent, isGithubTree);
},
"Cancel",
undefined,
this.getNewNoteWarningText(),
);
}
} else {
parent = parent || this.resourceTree.myNotebooksContentRoot;
this.createNewNoteBook(parent, isGithubTree);
}
}
private getNewNoteWarningText(): JSX.Element {
return (
<>
<p>{Notebook.newNotebookModalContent1}</p>
<br />
<p>
{Notebook.newNotebookModalContent2}
<Link href={Notebook.cosmosNotebookHomePageUrl} target="_blank">
{Notebook.learnMore}
</Link>
</p>
</>
);
}
private createNewNoteBook(parent?: NotebookContentItem, isGithubTree?: boolean): void {
const clearInProgressMessage = logConsoleProgress(`Creating new notebook in ${parent.path}`);
const startKey: number = TelemetryProcessor.traceStart(Action.CreateNewNotebook, {
dataExplorerArea: Constants.Areas.Notebook,
});
this.notebookManager?.notebookContentClient
.createNewNotebookFile(parent, isGithubTree)
.then((newFile: NotebookContentItem) => {
logConsoleInfo(`Successfully created: ${newFile.name}`);
TelemetryProcessor.traceSuccess(
Action.CreateNewNotebook,
{
dataExplorerArea: Constants.Areas.Notebook,
},
startKey,
);
return this.openNotebook(newFile);
})
.then(() => this.resourceTree.triggerRender())
.catch((error) => {
const errorMessage = `Failed to create a new notebook: ${getErrorMessage(error)}`;
logConsoleError(errorMessage);
TelemetryProcessor.traceFailure(
Action.CreateNewNotebook,
{
dataExplorerArea: Constants.Areas.Notebook,
error: errorMessage,
errorStack: getErrorStack(error),
},
startKey,
);
})
.finally(clearInProgressMessage);
}
// TODO: Delete this function when ResourceTreeAdapter is removed. // TODO: Delete this function when ResourceTreeAdapter is removed.
public async refreshContentItem(item: NotebookContentItem): Promise<void> { public async refreshContentItem(item: NotebookContentItem): Promise<void> {
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
@ -1130,10 +936,6 @@ export default class Explorer {
let title: string; let title: string;
switch (kind) { switch (kind) {
case ViewModels.TerminalKind.Default:
title = "Terminal";
break;
case ViewModels.TerminalKind.Mongo: case ViewModels.TerminalKind.Mongo:
title = "Mongo Shell"; title = "Mongo Shell";
break; break;
@ -1287,36 +1089,6 @@ export default class Explorer {
.openSidePanel("Input parameters", <ExecuteSprocParamsPane storedProcedure={storedProcedure} />); .openSidePanel("Input parameters", <ExecuteSprocParamsPane storedProcedure={storedProcedure} />);
} }
public openUploadFilePanel(parent?: NotebookContentItem): void {
if (useNotebook.getState().isPhoenixNotebooks) {
useDialog.getState().showOkCancelModalDialog(
Notebook.newNotebookUploadModalTitle,
undefined,
"Upload",
async () => {
await this.allocateContainer(PoolIdType.DefaultPoolId);
parent = parent || this.resourceTree.myNotebooksContentRoot;
this.uploadFilePanel(parent);
},
"Cancel",
undefined,
this.getNewNoteWarningText(),
);
} else {
parent = parent || this.resourceTree.myNotebooksContentRoot;
this.uploadFilePanel(parent);
}
}
private uploadFilePanel(parent?: NotebookContentItem): void {
useSidePanel
.getState()
.openSidePanel(
"Upload file to notebook server",
<UploadFilePane uploadFile={(name: string, content: string) => this.uploadFile(name, content, parent)} />,
);
}
public getDownloadModalConent(fileName: string): JSX.Element { public getDownloadModalConent(fileName: string): JSX.Element {
if (useNotebook.getState().isPhoenixNotebooks) { if (useNotebook.getState().isPhoenixNotebooks) {
return ( return (
@ -1385,21 +1157,25 @@ export default class Explorer {
} }
public async refreshSampleData(): Promise<void> { public async refreshSampleData(): Promise<void> {
if (!userContext.sampleDataConnectionInfo) { try {
if (!userContext.sampleDataConnectionInfo) {
return;
}
const collection: DataModels.Collection = await readSampleCollection();
if (!collection) {
return;
}
const databaseId = userContext.sampleDataConnectionInfo?.databaseId;
if (!databaseId) {
return;
}
const sampleDataResourceTokenCollection = new ResourceTokenCollection(this, databaseId, collection, true);
useDatabases.setState({ sampleDataResourceTokenCollection });
} catch (error) {
Logger.logError(getErrorMessage(error), "Explorer");
return; return;
} }
const collection: DataModels.Collection = await readSampleCollection();
if (!collection) {
return;
}
const databaseId = userContext.sampleDataConnectionInfo?.databaseId;
if (!databaseId) {
return;
}
const sampleDataResourceTokenCollection = new ResourceTokenCollection(this, databaseId, collection, true);
useDatabases.setState({ sampleDataResourceTokenCollection });
} }
} }

View File

@ -349,7 +349,7 @@ export class NodePropertiesComponent extends React.Component<
onActivated={this.setIsDeleteConfirm.bind(this, true)} onActivated={this.setIsDeleteConfirm.bind(this, true)}
aria-label="Delete this vertex" aria-label="Delete this vertex"
> >
<img src={DeleteIcon} alt="Delete" /> <img src={DeleteIcon} alt="Delete" role="button" />
</AccessibleElement> </AccessibleElement>
); );
} else { } else {
@ -406,7 +406,7 @@ export class NodePropertiesComponent extends React.Component<
aria-label="Edit properties" aria-label="Edit properties"
onActivated={expandClickHandler} onActivated={expandClickHandler}
> >
<img src={EditIcon} alt="Edit" /> <img src={EditIcon} alt="Edit" role="button" />
</AccessibleElement> </AccessibleElement>
)} )}

View File

@ -184,12 +184,18 @@ export const NewVertexComponent: FunctionComponent<INewVertexComponentProps> = (
className="rightPaneTrashIcon rightPaneBtns" className="rightPaneTrashIcon rightPaneBtns"
tabIndex={0} tabIndex={0}
role="button" role="button"
aria-label={`Delete ${data.key}`}
onClick={(event: React.MouseEvent<HTMLDivElement>) => removeNewVertexProperty(event, index)} onClick={(event: React.MouseEvent<HTMLDivElement>) => removeNewVertexProperty(event, index)}
onKeyPress={(event: React.KeyboardEvent<HTMLDivElement>) => onKeyPress={(event: React.KeyboardEvent<HTMLDivElement>) =>
removeNewVertexPropertyKeyPress(event, index) removeNewVertexPropertyKeyPress(event, index)
} }
> >
<img className="refreshcol rightPaneTrashIconImg" src={DeleteIcon} alt="Remove property" /> <img
aria-label="hidden"
className="refreshcol rightPaneTrashIconImg"
src={DeleteIcon}
alt="Remove property"
/>
</div> </div>
</div> </div>
</div> </div>

View File

@ -5,6 +5,7 @@
*/ */
import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react"; import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react";
import { useNotebook } from "Explorer/Notebook/useNotebook"; import { useNotebook } from "Explorer/Notebook/useNotebook";
import { KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
import { userContext } from "UserContext"; import { userContext } from "UserContext";
import * as React from "react"; import * as React from "react";
import create, { UseStore } from "zustand"; import create, { UseStore } from "zustand";
@ -40,6 +41,7 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
const buttons = useCommandBar((state) => state.contextButtons); const buttons = useCommandBar((state) => state.contextButtons);
const isHidden = useCommandBar((state) => state.isHidden); const isHidden = useCommandBar((state) => state.isHidden);
const backgroundColor = StyleConstants.BaseLight; const backgroundColor = StyleConstants.BaseLight;
const setKeyboardHandlers = useKeyboardActionGroup(KeyboardActionGroup.COMMAND_BAR);
if (userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo") { if (userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo") {
const buttons = const buttons =
@ -105,6 +107,10 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
}, },
}; };
const allButtons = staticButtons.concat(contextButtons).concat(controlButtons);
const keyboardHandlers = CommandBarUtil.createKeyboardHandlers(allButtons);
setKeyboardHandlers(keyboardHandlers);
return ( return (
<div className="commandBarContainer" style={{ display: isHidden ? "none" : "initial" }}> <div className="commandBarContainer" style={{ display: isHidden ? "none" : "initial" }}>
<FluentCommandBar <FluentCommandBar

View File

@ -1,3 +1,4 @@
import { KeyboardAction } from "KeyboardShortcuts";
import { ReactTabKind, useTabs } from "hooks/useTabs"; import { ReactTabKind, useTabs } from "hooks/useTabs";
import * as React from "react"; import * as React from "react";
import AddCollectionIcon from "../../../../images/AddCollection.svg"; import AddCollectionIcon from "../../../../images/AddCollection.svg";
@ -57,6 +58,7 @@ export function createStaticCommandBarButtons(
buttons.push(homeBtn); buttons.push(homeBtn);
const newCollectionBtn = createNewCollectionGroup(container); const newCollectionBtn = createNewCollectionGroup(container);
newCollectionBtn.keyboardAction = KeyboardAction.NEW_COLLECTION; // Just for the root button, not the child version we create below.
buttons.push(newCollectionBtn); buttons.push(newCollectionBtn);
if (userContext.apiType !== "Tables" && userContext.apiType !== "Cassandra") { if (userContext.apiType !== "Tables" && userContext.apiType !== "Cassandra") {
const addSynapseLink = createOpenSynapseLinkDialogButton(container); const addSynapseLink = createOpenSynapseLinkDialogButton(container);
@ -94,6 +96,7 @@ export function createStaticCommandBarButtons(
const newStoredProcedureBtn: CommandButtonComponentProps = { const newStoredProcedureBtn: CommandButtonComponentProps = {
iconSrc: AddStoredProcedureIcon, iconSrc: AddStoredProcedureIcon,
iconAlt: label, iconAlt: label,
keyboardAction: KeyboardAction.NEW_SPROC,
onCommandClick: () => { onCommandClick: () => {
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection(); const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection); selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection);
@ -277,6 +280,7 @@ function createNewDatabase(container: Explorer): CommandButtonComponentProps {
return { return {
iconSrc: AddDatabaseIcon, iconSrc: AddDatabaseIcon,
iconAlt: label, iconAlt: label,
keyboardAction: KeyboardAction.NEW_DATABASE,
onCommandClick: async () => { onCommandClick: async () => {
const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit; const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit;
if (throughputCap && throughputCap !== -1) { if (throughputCap && throughputCap !== -1) {
@ -297,6 +301,7 @@ function createNewSQLQueryButton(selectedNodeState: SelectedNodeState): CommandB
id: "newQueryBtn", id: "newQueryBtn",
iconSrc: AddSqlQueryIcon, iconSrc: AddSqlQueryIcon,
iconAlt: label, iconAlt: label,
keyboardAction: KeyboardAction.NEW_QUERY,
onCommandClick: () => { onCommandClick: () => {
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection(); const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
selectedCollection && selectedCollection.onNewQueryClick(selectedCollection); selectedCollection && selectedCollection.onNewQueryClick(selectedCollection);
@ -312,6 +317,7 @@ function createNewSQLQueryButton(selectedNodeState: SelectedNodeState): CommandB
id: "newQueryBtn", id: "newQueryBtn",
iconSrc: AddSqlQueryIcon, iconSrc: AddSqlQueryIcon,
iconAlt: label, iconAlt: label,
keyboardAction: KeyboardAction.NEW_QUERY,
onCommandClick: () => { onCommandClick: () => {
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection(); const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection); selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection);
@ -337,6 +343,7 @@ export function createScriptCommandButtons(selectedNodeState: SelectedNodeState)
const newStoredProcedureBtn: CommandButtonComponentProps = { const newStoredProcedureBtn: CommandButtonComponentProps = {
iconSrc: AddStoredProcedureIcon, iconSrc: AddStoredProcedureIcon,
iconAlt: label, iconAlt: label,
keyboardAction: KeyboardAction.NEW_SPROC,
onCommandClick: () => { onCommandClick: () => {
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection(); const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection); selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection);
@ -356,6 +363,7 @@ export function createScriptCommandButtons(selectedNodeState: SelectedNodeState)
const newUserDefinedFunctionBtn: CommandButtonComponentProps = { const newUserDefinedFunctionBtn: CommandButtonComponentProps = {
iconSrc: AddUdfIcon, iconSrc: AddUdfIcon,
iconAlt: label, iconAlt: label,
keyboardAction: KeyboardAction.NEW_UDF,
onCommandClick: () => { onCommandClick: () => {
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection(); const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
selectedCollection && selectedCollection.onNewUserDefinedFunctionClick(selectedCollection); selectedCollection && selectedCollection.onNewUserDefinedFunctionClick(selectedCollection);
@ -375,6 +383,7 @@ export function createScriptCommandButtons(selectedNodeState: SelectedNodeState)
const newTriggerBtn: CommandButtonComponentProps = { const newTriggerBtn: CommandButtonComponentProps = {
iconSrc: AddTriggerIcon, iconSrc: AddTriggerIcon,
iconAlt: label, iconAlt: label,
keyboardAction: KeyboardAction.NEW_TRIGGER,
onCommandClick: () => { onCommandClick: () => {
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection(); const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
selectedCollection && selectedCollection.onNewTriggerClick(selectedCollection); selectedCollection && selectedCollection.onNewTriggerClick(selectedCollection);
@ -397,6 +406,7 @@ function createOpenQueryButton(container: Explorer): CommandButtonComponentProps
return { return {
iconSrc: BrowseQueriesIcon, iconSrc: BrowseQueriesIcon,
iconAlt: label, iconAlt: label,
keyboardAction: KeyboardAction.OPEN_QUERY,
onCommandClick: () => onCommandClick: () =>
useSidePanel.getState().openSidePanel("Open Saved Queries", <BrowseQueriesPane explorer={container} />), useSidePanel.getState().openSidePanel("Open Saved Queries", <BrowseQueriesPane explorer={container} />),
commandButtonLabel: label, commandButtonLabel: label,
@ -411,6 +421,7 @@ function createOpenQueryFromDiskButton(): CommandButtonComponentProps {
return { return {
iconSrc: OpenQueryFromDiskIcon, iconSrc: OpenQueryFromDiskIcon,
iconAlt: label, iconAlt: label,
keyboardAction: KeyboardAction.OPEN_QUERY_FROM_DISK,
onCommandClick: () => useSidePanel.getState().openSidePanel("Load Query", <LoadQueryPane />), onCommandClick: () => useSidePanel.getState().openSidePanel("Load Query", <LoadQueryPane />),
commandButtonLabel: label, commandButtonLabel: label,
ariaLabel: label, ariaLabel: label,

View File

@ -7,6 +7,7 @@ import {
IDropdownStyles, IDropdownStyles,
} from "@fluentui/react"; } from "@fluentui/react";
import { useQueryCopilot } from "hooks/useQueryCopilot"; import { useQueryCopilot } from "hooks/useQueryCopilot";
import { KeyboardHandlerMap } from "KeyboardShortcuts";
import * as React from "react"; import * as React from "react";
import _ from "underscore"; import _ from "underscore";
import ChevronDownIcon from "../../../../images/Chevron_down.svg"; import ChevronDownIcon from "../../../../images/Chevron_down.svg";
@ -59,21 +60,23 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol
imageProps: btn.iconSrc ? { src: btn.iconSrc, alt: btn.iconAlt } : undefined, imageProps: btn.iconSrc ? { src: btn.iconSrc, alt: btn.iconAlt } : undefined,
iconName: btn.iconName, iconName: btn.iconName,
}, },
onClick: (ev?: React.MouseEvent<HTMLElement, MouseEvent> | React.KeyboardEvent<HTMLElement>) => { onClick: btn.onCommandClick
btn.onCommandClick(ev); ? (ev?: React.MouseEvent<HTMLElement, MouseEvent> | React.KeyboardEvent<HTMLElement>) => {
let copilotEnabled = false; btn.onCommandClick(ev);
if (useQueryCopilot.getState().copilotEnabled && useQueryCopilot.getState().copilotUserDBEnabled) { let copilotEnabled = false;
copilotEnabled = useQueryCopilot.getState().copilotEnabledforExecution; if (useQueryCopilot.getState().copilotEnabled && useQueryCopilot.getState().copilotUserDBEnabled) {
} copilotEnabled = useQueryCopilot.getState().copilotEnabledforExecution;
TelemetryProcessor.trace(Action.ClickCommandBarButton, ActionModifiers.Mark, { label, copilotEnabled }); }
}, TelemetryProcessor.trace(Action.ClickCommandBarButton, ActionModifiers.Mark, { label, copilotEnabled });
}
: undefined,
key: `${btn.commandButtonLabel}${index}`, key: `${btn.commandButtonLabel}${index}`,
text: label, text: label,
"data-test": label,
title: btn.tooltipText, title: btn.tooltipText,
name: label, name: label,
disabled: btn.disabled, disabled: btn.disabled,
ariaLabel: btn.ariaLabel, ariaLabel: btn.ariaLabel,
"data-test": `CommandBar/Button:${label}`,
buttonStyles: { buttonStyles: {
root: { root: {
backgroundColor: backgroundColor, backgroundColor: backgroundColor,
@ -233,3 +236,28 @@ export const createConnectionStatus = (container: Explorer, poolId: PoolIdType,
onRender: () => <ConnectionStatus container={container} poolId={poolId} />, onRender: () => <ConnectionStatus container={container} poolId={poolId} />,
}; };
}; };
export function createKeyboardHandlers(allButtons: CommandButtonComponentProps[]): KeyboardHandlerMap {
const handlers: KeyboardHandlerMap = {};
function createHandlers(buttons: CommandButtonComponentProps[]) {
buttons.forEach((button) => {
if (!button.disabled && button.keyboardAction) {
handlers[button.keyboardAction] = (e) => {
button.onCommandClick(e);
// If the handler is bound, it means the button is visible and enabled, so we should prevent the default action
return true;
};
}
if (button.children && button.children.length > 0) {
createHandlers(button.children);
}
});
}
createHandlers(allButtons);
return handlers;
}

View File

@ -21,6 +21,7 @@ import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
import { configContext, Platform } from "ConfigContext"; import { configContext, Platform } from "ConfigContext";
import * as DataModels from "Contracts/DataModels"; import * as DataModels from "Contracts/DataModels";
import { SubscriptionType } from "Contracts/SubscriptionType"; import { SubscriptionType } from "Contracts/SubscriptionType";
import { AddVectorEmbeddingPolicyForm } from "Explorer/Panes/VectorSearchPanel/AddVectorEmbeddingPolicyForm";
import { useSidePanel } from "hooks/useSidePanel"; import { useSidePanel } from "hooks/useSidePanel";
import { useTeachingBubble } from "hooks/useTeachingBubble"; import { useTeachingBubble } from "hooks/useTeachingBubble";
import React from "react"; import React from "react";
@ -29,7 +30,7 @@ import { Action } from "Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
import { userContext } from "UserContext"; import { userContext } from "UserContext";
import { getCollectionName } from "Utils/APITypeUtils"; import { getCollectionName } from "Utils/APITypeUtils";
import { isCapabilityEnabled, isServerlessAccount } from "Utils/CapabilityUtils"; import { isCapabilityEnabled, isServerlessAccount, isVectorSearchEnabled } from "Utils/CapabilityUtils";
import { getUpsellMessage } from "Utils/PricingUtils"; import { getUpsellMessage } from "Utils/PricingUtils";
import { CollapsibleSectionComponent } from "../Controls/CollapsiblePanel/CollapsibleSectionComponent"; import { CollapsibleSectionComponent } from "../Controls/CollapsiblePanel/CollapsibleSectionComponent";
import { ThroughputInput } from "../Controls/ThroughputInput/ThroughputInput"; import { ThroughputInput } from "../Controls/ThroughputInput/ThroughputInput";
@ -81,6 +82,10 @@ export const AllPropertiesIndexed: DataModels.IndexingPolicy = {
excludedPaths: [], excludedPaths: [],
}; };
export const DefaultVectorEmbeddingPolicy: DataModels.VectorEmbeddingPolicy = {
vectorEmbeddings: [],
};
export interface AddCollectionPanelState { export interface AddCollectionPanelState {
createNewDatabase: boolean; createNewDatabase: boolean;
newDatabaseId: string; newDatabaseId: string;
@ -101,6 +106,9 @@ export interface AddCollectionPanelState {
isExecuting: boolean; isExecuting: boolean;
isThroughputCapExceeded: boolean; isThroughputCapExceeded: boolean;
teachingBubbleStep: number; teachingBubbleStep: number;
vectorIndexingPolicy: DataModels.VectorIndex[];
vectorEmbeddingPolicy: DataModels.VectorEmbedding[];
vectorPolicyValidated: boolean;
} }
export class AddCollectionPanel extends React.Component<AddCollectionPanelProps, AddCollectionPanelState> { export class AddCollectionPanel extends React.Component<AddCollectionPanelProps, AddCollectionPanelState> {
@ -136,6 +144,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
isExecuting: false, isExecuting: false,
isThroughputCapExceeded: false, isThroughputCapExceeded: false,
teachingBubbleStep: 0, teachingBubbleStep: 0,
vectorEmbeddingPolicy: [],
vectorIndexingPolicy: [],
vectorPolicyValidated: true,
}; };
} }
@ -145,11 +156,17 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
} }
} }
componentDidUpdate(_prevProps: AddCollectionPanelProps, prevState: AddCollectionPanelState): void {
if (this.state.errorMessage && this.state.errorMessage !== prevState.errorMessage) {
this.scrollToSection("panelContainer");
}
}
render(): JSX.Element { render(): JSX.Element {
const isFirstResourceCreated = useDatabases.getState().isFirstResourceCreated(); const isFirstResourceCreated = useDatabases.getState().isFirstResourceCreated();
return ( return (
<form className="panelFormWrapper" onSubmit={this.submit.bind(this)}> <form className="panelFormWrapper" onSubmit={this.submit.bind(this)} id="panelContainer">
{this.state.errorMessage && ( {this.state.errorMessage && (
<PanelInfoErrorComponent <PanelInfoErrorComponent
message={this.state.errorMessage} message={this.state.errorMessage}
@ -382,6 +399,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
)} )}
{!this.state.createNewDatabase && ( {!this.state.createNewDatabase && (
<Dropdown <Dropdown
ariaLabel="Choose an existing database"
styles={{ title: { height: 27, lineHeight: 27 }, dropdownItem: { fontSize: 12 } }} styles={{ title: { height: 27, lineHeight: 27 }, dropdownItem: { fontSize: 12 } }}
style={{ width: 300, fontSize: 12 }} style={{ width: 300, fontSize: 12 }}
placeholder="Choose an existing database" placeholder="Choose an existing database"
@ -800,8 +818,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
iconName="Info" iconName="Info"
className="panelInfoIcon" className="panelInfoIcon"
tabIndex={0} tabIndex={0}
ariaLabel="Enable analytical store capability to perform near real-time analytics on your operational data, without ariaLabel="Enable analytical store capability to perform near real-time analytics on your operational data, without impacting the performance of transactional workloads."
impacting the performance of transactional workloads."
/> />
</TooltipHost> </TooltipHost>
</Stack> </Stack>
@ -863,17 +880,44 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
)} )}
</Stack> </Stack>
)} )}
{this.shouldShowVectorSearchParameters() && (
<Stack>
<CollapsibleSectionComponent
title="Container Vector Policy"
isExpandedByDefault={false}
onExpand={() => {
this.scrollToSection("collapsibleVectorPolicySectionContent");
}}
tooltipContent={this.getContainerVectorPolicyTooltipContent()}
>
<Stack id="collapsibleVectorPolicySectionContent" styles={{ root: { position: "relative" } }}>
<Stack styles={{ root: { paddingLeft: 40 } }}>
<AddVectorEmbeddingPolicyForm
vectorEmbedding={this.state.vectorEmbeddingPolicy}
vectorIndex={this.state.vectorIndexingPolicy}
onVectorEmbeddingChange={(
vectorEmbeddingPolicy: DataModels.VectorEmbedding[],
vectorIndexingPolicy: DataModels.VectorIndex[],
vectorPolicyValidated: boolean,
) => {
this.setState({ vectorEmbeddingPolicy, vectorIndexingPolicy, vectorPolicyValidated });
}}
/>
</Stack>
</Stack>
</CollapsibleSectionComponent>
</Stack>
)}
{userContext.apiType !== "Tables" && ( {userContext.apiType !== "Tables" && (
<CollapsibleSectionComponent <CollapsibleSectionComponent
title="Advanced" title="Advanced"
isExpandedByDefault={false} isExpandedByDefault={false}
onExpand={() => { onExpand={() => {
TelemetryProcessor.traceOpen(Action.ExpandAddCollectionPaneAdvancedSection); TelemetryProcessor.traceOpen(Action.ExpandAddCollectionPaneAdvancedSection);
this.scrollToAdvancedSection(); this.scrollToSection("collapsibleAdvancedSectionContent");
}} }}
> >
<Stack className="panelGroupSpacing" id="collapsibleSectionContent"> <Stack className="panelGroupSpacing" id="collapsibleAdvancedSectionContent">
{isCapabilityEnabled("EnableMongo") && !isCapabilityEnabled("EnableMongo16MBDocumentSupport") && ( {isCapabilityEnabled("EnableMongo") && !isCapabilityEnabled("EnableMongo16MBDocumentSupport") && (
<Stack className="panelGroupSpacing"> <Stack className="panelGroupSpacing">
<Stack horizontal> <Stack horizontal>
@ -924,10 +968,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
} }
/> />
<Text variant="small"> <Text variant="small">
<Icon iconName="InfoSolid" className="removeIcon" tabIndex={0} /> To ensure compatibility with <Icon iconName="InfoSolid" className="removeIcon" /> To ensure compatibility with older SDKs, the
older SDKs, the created container will use a legacy partitioning scheme that supports partition created container will use a legacy partitioning scheme that supports partition key values of size
key values of size only up to 101 bytes. If this is enabled, you will not be able to use only up to 101 bytes. If this is enabled, you will not be able to use hierarchical partition keys.{" "}
hierarchical partition keys.{" "}
<Link href="https://aka.ms/cosmos-large-pk" target="_blank"> <Link href="https://aka.ms/cosmos-large-pk" target="_blank">
Learn more Learn more
</Link> </Link>
@ -1070,6 +1113,18 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
} }
} }
private setVectorEmbeddingPolicy(vectorEmbeddingPolicy: DataModels.VectorEmbedding[]): void {
this.setState({
vectorEmbeddingPolicy,
});
}
private setVectorIndexingPolicy(vectorIndexingPolicy: DataModels.VectorIndex[]): void {
this.setState({
vectorIndexingPolicy,
});
}
private isSelectedDatabaseSharedThroughput(): boolean { private isSelectedDatabaseSharedThroughput(): boolean {
if (!this.state.selectedDatabaseId) { if (!this.state.selectedDatabaseId) {
return false; return false;
@ -1150,6 +1205,18 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
); );
} }
private getContainerVectorPolicyTooltipContent(): JSX.Element {
return (
<Text variant="small">
Describe any properties in your data that contain vectors, so that they can be made available for similarity
queries.{" "}
<Link target="_blank" href="https://aka.ms/CosmosDBVectorSetup">
Learn more
</Link>
</Text>
);
}
private shouldShowCollectionThroughputInput(): boolean { private shouldShowCollectionThroughputInput(): boolean {
if (isServerlessAccount()) { if (isServerlessAccount()) {
return false; return false;
@ -1209,6 +1276,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
); );
} }
private shouldShowVectorSearchParameters() {
return isVectorSearchEnabled() && (isServerlessAccount() || this.shouldShowCollectionThroughputInput());
}
private parseUniqueKeys(): DataModels.UniqueKeyPolicy { private parseUniqueKeys(): DataModels.UniqueKeyPolicy {
if (this.state.uniqueKeys?.length === 0) { if (this.state.uniqueKeys?.length === 0) {
return undefined; return undefined;
@ -1265,6 +1336,11 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
return false; return false;
} }
if (this.shouldShowVectorSearchParameters() && !this.state.vectorPolicyValidated) {
this.setState({ errorMessage: "Please fix errors in container vector policy" });
return false;
}
return true; return true;
} }
@ -1287,8 +1363,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
return Constants.AnalyticalStorageTtl.Disabled; return Constants.AnalyticalStorageTtl.Disabled;
} }
private scrollToAdvancedSection(): void { private scrollToSection(id: string): void {
document.getElementById("collapsibleSectionContent")?.scrollIntoView(); document.getElementById(id)?.scrollIntoView();
} }
private getSampleDBName(): string { private getSampleDBName(): string {
@ -1344,6 +1420,15 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
? AllPropertiesIndexed ? AllPropertiesIndexed
: SharedDatabaseDefault; : SharedDatabaseDefault;
let vectorEmbeddingPolicy: DataModels.VectorEmbeddingPolicy;
if (this.shouldShowVectorSearchParameters()) {
indexingPolicy.vectorIndexes = this.state.vectorIndexingPolicy;
vectorEmbeddingPolicy = {
vectorEmbeddings: this.state.vectorEmbeddingPolicy,
};
}
const telemetryData = { const telemetryData = {
database: { database: {
id: databaseId, id: databaseId,
@ -1402,6 +1487,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
partitionKey, partitionKey,
uniqueKeyPolicy, uniqueKeyPolicy,
createMongoWildcardIndex: this.state.createMongoWildCardIndex, createMongoWildcardIndex: this.state.createMongoWildCardIndex,
vectorEmbeddingPolicy,
}; };
this.setState({ isExecuting: true }); this.setState({ isExecuting: true });

View File

@ -202,8 +202,8 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
required={true} required={true}
autoComplete="off" autoComplete="off"
styles={getTextFieldStyles()} styles={getTextFieldStyles()}
pattern="[^/?#\\]*[^/?# \\]" pattern="[^/?#\\-]*[^/?#- \\]"
title="May not end with space nor contain characters '\' '/' '#' '?'" title="May not end with space nor contain characters '\' '/' '#' '?' '-'"
placeholder="Type a new keyspace id" placeholder="Type a new keyspace id"
size={40} size={40}
value={newKeyspaceId} value={newKeyspaceId}
@ -275,7 +275,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
<span className="mandatoryStar">*&nbsp;</span> <span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small"> <Text className="panelTextBold" variant="small">
Enter CQL command to create the table.{" "} Enter CQL command to create the table.{" "}
<Link href="https://aka.ms/cassandra-create-table" target="_blank"> <Link className="underlinedLink" href="https://aka.ms/cassandra-create-table" target="_blank">
Learn More Learn More
</Link> </Link>
</Text> </Text>
@ -292,8 +292,8 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
required={true} required={true}
ariaLabel="addCollection-table Id Create table" ariaLabel="addCollection-table Id Create table"
autoComplete="off" autoComplete="off"
pattern="[^/?#\\]*[^/?# \\]" pattern="[^/?#\\-]*[^/?#- \\]"
title="May not end with space nor contain characters '\' '/' '#' '?'" title="May not end with space nor contain characters '\' '/' '#' '?' '-'"
placeholder="Enter table Id" placeholder="Enter table Id"
size={20} size={20}
value={tableId} value={tableId}

View File

@ -379,6 +379,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
> >
<CustomizedPrimaryButton <CustomizedPrimaryButton
ariaLabel="OK" ariaLabel="OK"
data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
text="OK" text="OK"
@ -386,6 +387,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
> >
<PrimaryButton <PrimaryButton
ariaLabel="OK" ariaLabel="OK"
data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
text="OK" text="OK"
@ -666,6 +668,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
> >
<CustomizedDefaultButton <CustomizedDefaultButton
ariaLabel="OK" ariaLabel="OK"
data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
onRenderDescription={[Function]} onRenderDescription={[Function]}
@ -948,6 +951,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
> >
<DefaultButton <DefaultButton
ariaLabel="OK" ariaLabel="OK"
data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
onRenderDescription={[Function]} onRenderDescription={[Function]}
@ -1231,6 +1235,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
<BaseButton <BaseButton
ariaLabel="OK" ariaLabel="OK"
baseClassName="ms-Button" baseClassName="ms-Button"
data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
onRenderDescription={[Function]} onRenderDescription={[Function]}
@ -2105,6 +2110,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
aria-label="OK" aria-label="OK"
className="ms-Button ms-Button--primary root-122" className="ms-Button ms-Button--primary root-122"
data-is-focusable={true} data-is-focusable={true}
data-test="Panel/OkButton"
id="sidePanelOkButton" id="sidePanelOkButton"
onClick={[Function]} onClick={[Function]}
onKeyDown={[Function]} onKeyDown={[Function]}
@ -2140,6 +2146,11 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
</div> </div>
</PanelFooterComponent> </PanelFooterComponent>
</form> </form>
<span
className="screenReaderOnly"
id="screenReaderStatus"
role="status"
/>
</RightPaneForm> </RightPaneForm>
</DeleteCollectionConfirmationPane> </DeleteCollectionConfirmationPane>
`; `;

View File

@ -9,6 +9,7 @@ import { DefaultExperienceUtility } from "Shared/DefaultExperienceUtility";
import { Action, ActionModifiers } from "Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
import { userContext } from "UserContext"; import { userContext } from "UserContext";
import { getDatabaseName } from "Utils/APITypeUtils";
import { logConsoleError } from "Utils/NotificationConsoleUtils"; import { logConsoleError } from "Utils/NotificationConsoleUtils";
import { useSidePanel } from "hooks/useSidePanel"; import { useSidePanel } from "hooks/useSidePanel";
import { useTabs } from "hooks/useTabs"; import { useTabs } from "hooks/useTabs";
@ -37,11 +38,11 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
const submit = async (): Promise<void> => { const submit = async (): Promise<void> => {
if (selectedDatabase?.id() && databaseInput !== selectedDatabase.id()) { if (selectedDatabase?.id() && databaseInput !== selectedDatabase.id()) {
setFormError( setFormError(
`Input database name "${databaseInput}" does not match the selected database "${selectedDatabase.id()}"`, `Input ${getDatabaseName()} name "${databaseInput}" does not match the selected ${getDatabaseName()} "${selectedDatabase.id()}"`,
); );
logConsoleError(`Error while deleting collection ${selectedDatabase && selectedDatabase.id()}`); logConsoleError(`Error while deleting ${getDatabaseName()} ${selectedDatabase && selectedDatabase.id()}`);
logConsoleError( logConsoleError(
`Input database name "${databaseInput}" does not match the selected database "${selectedDatabase.id()}"`, `Input ${getDatabaseName()} name "${databaseInput}" does not match the selected ${getDatabaseName()} "${selectedDatabase.id()}"`,
); );
return; return;
} }
@ -123,17 +124,18 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
message: message:
"Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources.", "Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources.",
}; };
const confirmDatabase = "Confirm by typing the database id"; const confirmDatabase = `Confirm by typing the ${getDatabaseName()} id`;
const reasonInfo = "Help us improve Azure Cosmos DB! What is the reason why you are deleting this database?"; const reasonInfo = `Help us improve Azure Cosmos DB! What is the reason why you are deleting this ${getDatabaseName()}?`;
return ( return (
<RightPaneForm {...props}> <RightPaneForm {...props}>
{!formError && <PanelInfoErrorComponent {...errorProps} />} {!formError && <PanelInfoErrorComponent {...errorProps} />}
<div className="panelMainContent"> <div className="panelMainContent">
<div className="confirmDeleteInput"> <div className="confirmDeleteInput">
<span className="mandatoryStar">* </span> <span className="mandatoryStar">* </span>
<Text variant="small">Confirm by typing the database id</Text> <Text variant="small">Confirm by typing the {getDatabaseName()} id</Text>
<TextField <TextField
id="confirmDatabaseId" id="confirmDatabaseId"
data-test="Input:confirmDatabaseId"
autoFocus autoFocus
styles={{ fieldGroup: { width: 300 } }} styles={{ fieldGroup: { width: 300 } }}
onChange={(event, newInput?: string) => { onChange={(event, newInput?: string) => {
@ -149,7 +151,7 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
Help us improve Azure Cosmos DB! Help us improve Azure Cosmos DB!
</Text> </Text>
<Text variant="small" block> <Text variant="small" block>
What is the reason why you are deleting this database? What is the reason why you are deleting this {getDatabaseName()}?
</Text> </Text>
<TextField <TextField
id="deleteDatabaseFeedbackInput" id="deleteDatabaseFeedbackInput"

View File

@ -5312,6 +5312,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
> >
<CustomizedPrimaryButton <CustomizedPrimaryButton
ariaLabel="Execute" ariaLabel="Execute"
data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
text="Execute" text="Execute"
@ -5319,6 +5320,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
> >
<PrimaryButton <PrimaryButton
ariaLabel="Execute" ariaLabel="Execute"
data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
text="Execute" text="Execute"
@ -5599,6 +5601,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
> >
<CustomizedDefaultButton <CustomizedDefaultButton
ariaLabel="Execute" ariaLabel="Execute"
data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
onRenderDescription={[Function]} onRenderDescription={[Function]}
@ -5881,6 +5884,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
> >
<DefaultButton <DefaultButton
ariaLabel="Execute" ariaLabel="Execute"
data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
onRenderDescription={[Function]} onRenderDescription={[Function]}
@ -6164,6 +6168,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
<BaseButton <BaseButton
ariaLabel="Execute" ariaLabel="Execute"
baseClassName="ms-Button" baseClassName="ms-Button"
data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
onRenderDescription={[Function]} onRenderDescription={[Function]}
@ -7038,6 +7043,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
aria-label="Execute" aria-label="Execute"
className="ms-Button ms-Button--primary root-148" className="ms-Button ms-Button--primary root-148"
data-is-focusable={true} data-is-focusable={true}
data-test="Panel/OkButton"
id="sidePanelOkButton" id="sidePanelOkButton"
onClick={[Function]} onClick={[Function]}
onKeyDown={[Function]} onKeyDown={[Function]}
@ -7073,6 +7079,11 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
</div> </div>
</PanelFooterComponent> </PanelFooterComponent>
</form> </form>
<span
className="screenReaderOnly"
id="screenReaderStatus"
role="status"
/>
</RightPaneForm> </RightPaneForm>
</ExecuteSprocParamsPane> </ExecuteSprocParamsPane>
`; `;

View File

@ -18,7 +18,6 @@ exports[`GitHub Repos Panel should render Default properly 1`] = `
Object { Object {
"container": Explorer { "container": Explorer {
"_isInitializingNotebooks": false, "_isInitializingNotebooks": false,
"_resetNotebookWorkspace": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function], "isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function], "onRefreshDatabasesKeyPress": [Function],

View File

@ -48,6 +48,13 @@
font-size: @mediumFontSize; font-size: @mediumFontSize;
padding: 0 @LargeSpace 0 @SmallSpace; padding: 0 @LargeSpace 0 @SmallSpace;
} }
.panelSectionSpinner {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
} }
} }

View File

@ -53,6 +53,7 @@ export class PanelContainerComponent extends React.Component<PanelContainerProps
return ( return (
<Panel <Panel
data-test={`Panel:${this.props.headerText}`}
headerText={this.props.headerText} headerText={this.props.headerText}
isOpen={this.props.isOpen} isOpen={this.props.isOpen}
onDismiss={this.onDissmiss} onDismiss={this.onDissmiss}

View File

@ -16,6 +16,7 @@ export const PanelFooterComponent: React.FunctionComponent<PanelFooterProps> = (
<PrimaryButton <PrimaryButton
type="submit" type="submit"
id="sidePanelOkButton" id="sidePanelOkButton"
data-test="Panel/OkButton"
text={buttonLabel} text={buttonLabel}
ariaLabel={buttonLabel} ariaLabel={buttonLabel}
disabled={!!isButtonDisabled} disabled={!!isButtonDisabled}

View File

@ -2,6 +2,7 @@ import React, { CSSProperties, FunctionComponent, ReactNode } from "react";
import { PanelFooterComponent } from "../PanelFooterComponent"; import { PanelFooterComponent } from "../PanelFooterComponent";
import { PanelInfoErrorComponent } from "../PanelInfoErrorComponent"; import { PanelInfoErrorComponent } from "../PanelInfoErrorComponent";
import { PanelLoadingScreen } from "../PanelLoadingScreen"; import { PanelLoadingScreen } from "../PanelLoadingScreen";
import { labelToLoadingItemName } from "Explorer/Tables/Constants";
export interface RightPaneFormProps { export interface RightPaneFormProps {
formError: string; formError: string;
@ -27,6 +28,10 @@ export const RightPaneForm: FunctionComponent<RightPaneFormProps> = ({
const handleOnSubmit = (event: React.FormEvent<HTMLFormElement>) => { const handleOnSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault(); event.preventDefault();
onSubmit(); onSubmit();
const screenReaderStatusElement = document.getElementById("screenReaderStatus");
if (screenReaderStatusElement) {
screenReaderStatusElement.innerHTML = labelToLoadingItemName[submitButtonText] || "Loading";
}
}; };
return ( return (
@ -42,6 +47,7 @@ export const RightPaneForm: FunctionComponent<RightPaneFormProps> = ({
/> />
)} )}
</form> </form>
<span role="status" className="screenReaderOnly" id="screenReaderStatus"></span>
{isExecuting && <PanelLoadingScreen />} {isExecuting && <PanelLoadingScreen />}
</> </>
); );

View File

@ -21,6 +21,7 @@ exports[`Right Pane Form should render Default properly 1`] = `
> >
<CustomizedPrimaryButton <CustomizedPrimaryButton
ariaLabel="Load" ariaLabel="Load"
data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
text="Load" text="Load"
@ -28,6 +29,7 @@ exports[`Right Pane Form should render Default properly 1`] = `
> >
<PrimaryButton <PrimaryButton
ariaLabel="Load" ariaLabel="Load"
data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
text="Load" text="Load"
@ -308,6 +310,7 @@ exports[`Right Pane Form should render Default properly 1`] = `
> >
<CustomizedDefaultButton <CustomizedDefaultButton
ariaLabel="Load" ariaLabel="Load"
data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
onRenderDescription={[Function]} onRenderDescription={[Function]}
@ -590,6 +593,7 @@ exports[`Right Pane Form should render Default properly 1`] = `
> >
<DefaultButton <DefaultButton
ariaLabel="Load" ariaLabel="Load"
data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
onRenderDescription={[Function]} onRenderDescription={[Function]}
@ -873,6 +877,7 @@ exports[`Right Pane Form should render Default properly 1`] = `
<BaseButton <BaseButton
ariaLabel="Load" ariaLabel="Load"
baseClassName="ms-Button" baseClassName="ms-Button"
data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
onRenderDescription={[Function]} onRenderDescription={[Function]}
@ -1747,6 +1752,7 @@ exports[`Right Pane Form should render Default properly 1`] = `
aria-label="Load" aria-label="Load"
className="ms-Button ms-Button--primary root-109" className="ms-Button ms-Button--primary root-109"
data-is-focusable={true} data-is-focusable={true}
data-test="Panel/OkButton"
id="sidePanelOkButton" id="sidePanelOkButton"
onClick={[Function]} onClick={[Function]}
onKeyDown={[Function]} onKeyDown={[Function]}
@ -1782,5 +1788,10 @@ exports[`Right Pane Form should render Default properly 1`] = `
</div> </div>
</PanelFooterComponent> </PanelFooterComponent>
</form> </form>
<span
className="screenReaderOnly"
id="screenReaderStatus"
role="status"
/>
</RightPaneForm> </RightPaneForm>
`; `;

View File

@ -9,12 +9,15 @@ import {
Toggle, Toggle,
} from "@fluentui/react"; } from "@fluentui/react";
import * as Constants from "Common/Constants"; import * as Constants from "Common/Constants";
import { SplitterDirection } from "Common/Splitter";
import { InfoTooltip } from "Common/Tooltip/InfoTooltip"; import { InfoTooltip } from "Common/Tooltip/InfoTooltip";
import { configContext } from "ConfigContext"; import { configContext } from "ConfigContext";
import { useDatabases } from "Explorer/useDatabases";
import { import {
DefaultRUThreshold, DefaultRUThreshold,
LocalStorageUtility, LocalStorageUtility,
StorageKey, StorageKey,
getDefaultQueryResultsView,
getRUThreshold, getRUThreshold,
ruThresholdEnabled as isRUThresholdEnabled, ruThresholdEnabled as isRUThresholdEnabled,
} from "Shared/StorageUtility"; } from "Shared/StorageUtility";
@ -47,6 +50,9 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
LocalStorageUtility.getEntryBoolean(StorageKey.QueryTimeoutEnabled), LocalStorageUtility.getEntryBoolean(StorageKey.QueryTimeoutEnabled),
); );
const [queryTimeout, setQueryTimeout] = useState<number>(LocalStorageUtility.getEntryNumber(StorageKey.QueryTimeout)); const [queryTimeout, setQueryTimeout] = useState<number>(LocalStorageUtility.getEntryNumber(StorageKey.QueryTimeout));
const [defaultQueryResultsView, setDefaultQueryResultsView] = useState<SplitterDirection>(
getDefaultQueryResultsView(),
);
const [automaticallyCancelQueryAfterTimeout, setAutomaticallyCancelQueryAfterTimeout] = useState<boolean>( const [automaticallyCancelQueryAfterTimeout, setAutomaticallyCancelQueryAfterTimeout] = useState<boolean>(
LocalStorageUtility.getEntryBoolean(StorageKey.AutomaticallyCancelQueryAfterTimeout), LocalStorageUtility.getEntryBoolean(StorageKey.AutomaticallyCancelQueryAfterTimeout),
); );
@ -102,7 +108,10 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
const shouldShowCrossPartitionOption = userContext.apiType !== "Gremlin"; const shouldShowCrossPartitionOption = userContext.apiType !== "Gremlin";
const shouldShowParallelismOption = userContext.apiType !== "Gremlin"; const shouldShowParallelismOption = userContext.apiType !== "Gremlin";
const shouldShowPriorityLevelOption = PriorityBasedExecutionUtils.isFeatureEnabled(); const shouldShowPriorityLevelOption = PriorityBasedExecutionUtils.isFeatureEnabled();
const shouldShowCopilotSampleDBOption = userContext.apiType === "SQL" && useQueryCopilot.getState().copilotEnabled; const shouldShowCopilotSampleDBOption =
userContext.apiType === "SQL" &&
useQueryCopilot.getState().copilotEnabled &&
useDatabases.getState().sampleDataResourceTokenCollection;
const handlerOnSubmit = async () => { const handlerOnSubmit = async () => {
setIsExecuting(true); setIsExecuting(true);
@ -121,6 +130,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
LocalStorageUtility.setEntryNumber(StorageKey.MaxDegreeOfParellism, maxDegreeOfParallelism); LocalStorageUtility.setEntryNumber(StorageKey.MaxDegreeOfParellism, maxDegreeOfParallelism);
LocalStorageUtility.setEntryString(StorageKey.PriorityLevel, priorityLevel.toString()); LocalStorageUtility.setEntryString(StorageKey.PriorityLevel, priorityLevel.toString());
LocalStorageUtility.setEntryString(StorageKey.CopilotSampleDBEnabled, copilotSampleDBEnabled.toString()); LocalStorageUtility.setEntryString(StorageKey.CopilotSampleDBEnabled, copilotSampleDBEnabled.toString());
LocalStorageUtility.setEntryString(StorageKey.DefaultQueryResultsView, defaultQueryResultsView);
if (shouldShowGraphAutoVizOption) { if (shouldShowGraphAutoVizOption) {
LocalStorageUtility.setEntryBoolean( LocalStorageUtility.setEntryBoolean(
@ -197,6 +207,11 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
{ key: Constants.PriorityLevel.High, text: "High" }, { key: Constants.PriorityLevel.High, text: "High" },
]; ];
const defaultQueryResultsViewOptionList: IChoiceGroupOption[] = [
{ key: SplitterDirection.Vertical, text: "Vertical" },
{ key: SplitterDirection.Horizontal, text: "Horizontal" },
];
const handleOnPriorityLevelOptionChange = ( const handleOnPriorityLevelOptionChange = (
ev: React.FormEvent<HTMLInputElement>, ev: React.FormEvent<HTMLInputElement>,
option: IChoiceGroupOption, option: IChoiceGroupOption,
@ -234,6 +249,13 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
} }
}; };
const handleOnDefaultQueryResultsViewChange = (
ev: React.MouseEvent<HTMLElement>,
option: IChoiceGroupOption,
): void => {
setDefaultQueryResultsView(option.key as SplitterDirection);
};
const handleOnQueryRetryAttemptsSpinButtonChange = (ev: React.MouseEvent<HTMLElement>, newValue?: string): void => { const handleOnQueryRetryAttemptsSpinButtonChange = (ev: React.MouseEvent<HTMLElement>, newValue?: string): void => {
const retryAttempts = Number(newValue); const retryAttempts = Number(newValue);
if (!isNaN(retryAttempts)) { if (!isNaN(retryAttempts)) {
@ -438,6 +460,25 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
)} )}
</div> </div>
</div> </div>
<div className="settingsSection">
<div className="settingsSectionPart">
<div>
<legend id="defaultQueryResultsView" className="settingsSectionLabel legendLabel">
Default Query Results View
</legend>
<InfoTooltip>Select the default view to use when displaying query results.</InfoTooltip>
</div>
<div>
<ChoiceGroup
ariaLabelledBy="defaultQueryResultsView"
selectedKey={defaultQueryResultsView}
options={defaultQueryResultsViewOptionList}
styles={choiceButtonStyles}
onChange={handleOnDefaultQueryResultsViewChange}
/>
</div>
</div>
</div>
</> </>
)} )}
<div className="settingsSection"> <div className="settingsSection">
@ -630,7 +671,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
Enable sample database Enable sample database
<InfoTooltip> <InfoTooltip>
This is a sample database and collection with synthetic product data you can use to explore using This is a sample database and collection with synthetic product data you can use to explore using
NoSQL queries and Copilot. This will appear as another database in the Data Explorer UI, and is NoSQL queries and Query Advisor. This will appear as another database in the Data Explorer UI, and is
created by, and maintained by Microsoft at no cost to you. created by, and maintained by Microsoft at no cost to you.
</InfoTooltip> </InfoTooltip>
</div> </div>
@ -640,7 +681,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
label: { padding: 0 }, label: { padding: 0 },
}} }}
className="padding" className="padding"
ariaLabel="Enable sample db for Copilot" ariaLabel="Enable sample db for Query Advisor"
checked={copilotSampleDBEnabled} checked={copilotSampleDBEnabled}
onChange={handleSampleDatabaseChange} onChange={handleSampleDatabaseChange}
/> />

View File

@ -205,6 +205,67 @@ exports[`Settings Pane should render Default properly 1`] = `
</div> </div>
</div> </div>
</div> </div>
<div
className="settingsSection"
>
<div
className="settingsSectionPart"
>
<div>
<legend
className="settingsSectionLabel legendLabel"
id="defaultQueryResultsView"
>
Default Query Results View
</legend>
<InfoTooltip>
Select the default view to use when displaying query results.
</InfoTooltip>
</div>
<div>
<StyledChoiceGroup
ariaLabelledBy="defaultQueryResultsView"
onChange={[Function]}
options={
Array [
Object {
"key": "vertical",
"text": "Vertical",
},
Object {
"key": "horizontal",
"text": "Horizontal",
},
]
}
selectedKey="vertical"
styles={
Object {
"flexContainer": Array [
Object {
"selectors": Object {
".ms-ChoiceField": Object {
"marginTop": 0,
},
".ms-ChoiceField-wrapper label": Object {
"fontSize": 12,
"paddingTop": 0,
},
".ms-ChoiceFieldGroup root-133": Object {
"clear": "both",
},
},
},
],
"root": Object {
"clear": "both",
},
}
}
/>
</div>
</div>
</div>
<div <div
className="settingsSection" className="settingsSection"
> >

View File

@ -8,7 +8,6 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
explorer={ explorer={
Explorer { Explorer {
"_isInitializingNotebooks": false, "_isInitializingNotebooks": false,
"_resetNotebookWorkspace": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function], "isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function], "onRefreshDatabasesKeyPress": [Function],
@ -689,6 +688,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
> >
<CustomizedPrimaryButton <CustomizedPrimaryButton
ariaLabel="Create" ariaLabel="Create"
data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
text="Create" text="Create"
@ -696,6 +696,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
> >
<PrimaryButton <PrimaryButton
ariaLabel="Create" ariaLabel="Create"
data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
text="Create" text="Create"
@ -976,6 +977,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
> >
<CustomizedDefaultButton <CustomizedDefaultButton
ariaLabel="Create" ariaLabel="Create"
data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
onRenderDescription={[Function]} onRenderDescription={[Function]}
@ -1258,6 +1260,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
> >
<DefaultButton <DefaultButton
ariaLabel="Create" ariaLabel="Create"
data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
onRenderDescription={[Function]} onRenderDescription={[Function]}
@ -1541,6 +1544,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
<BaseButton <BaseButton
ariaLabel="Create" ariaLabel="Create"
baseClassName="ms-Button" baseClassName="ms-Button"
data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
onRenderDescription={[Function]} onRenderDescription={[Function]}
@ -2415,6 +2419,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
aria-label="Create" aria-label="Create"
className="ms-Button ms-Button--primary root-122" className="ms-Button ms-Button--primary root-122"
data-is-focusable={true} data-is-focusable={true}
data-test="Panel/OkButton"
id="sidePanelOkButton" id="sidePanelOkButton"
onClick={[Function]} onClick={[Function]}
onKeyDown={[Function]} onKeyDown={[Function]}
@ -2450,6 +2455,11 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
</div> </div>
</PanelFooterComponent> </PanelFooterComponent>
</form> </form>
<span
className="screenReaderOnly"
id="screenReaderStatus"
role="status"
/>
</RightPaneForm> </RightPaneForm>
</StringInputPane> </StringInputPane>
`; `;

View File

@ -124,8 +124,8 @@ export const AddTableEntityPanel: FunctionComponent<AddTableEntityPanelProps> =
setIsExecuting(true); setIsExecuting(true);
const entity: Entities.ITableEntity = entityFromAttributes(entities); const entity: Entities.ITableEntity = entityFromAttributes(entities);
const newEntity: Entities.ITableEntity = await tableDataClient.createDocument(queryTablesTab.collection, entity);
try { try {
const newEntity: Entities.ITableEntity = await tableDataClient.createDocument(queryTablesTab.collection, entity);
await tableEntityListViewModel.addEntityToCache(newEntity); await tableEntityListViewModel.addEntityToCache(newEntity);
if (!tryInsertNewHeaders(tableEntityListViewModel, newEntity)) { if (!tryInsertNewHeaders(tableEntityListViewModel, newEntity)) {
tableEntityListViewModel.redrawTableThrottled(); tableEntityListViewModel.redrawTableThrottled();
@ -261,6 +261,7 @@ export const AddTableEntityPanel: FunctionComponent<AddTableEntityPanelProps> =
<TextField <TextField
multiline multiline
rows={5} rows={5}
ariaLabel={entityAttributeProperty}
value={entityAttributeValue} value={entityAttributeValue}
onChange={(event, newInput?: string) => { onChange={(event, newInput?: string) => {
entityChange(newInput, selectedRow, "value"); entityChange(newInput, selectedRow, "value");

View File

@ -1258,6 +1258,7 @@ exports[`Table query select Panel should render Default properly 1`] = `
> >
<CustomizedPrimaryButton <CustomizedPrimaryButton
ariaLabel="OK" ariaLabel="OK"
data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
text="OK" text="OK"
@ -1265,6 +1266,7 @@ exports[`Table query select Panel should render Default properly 1`] = `
> >
<PrimaryButton <PrimaryButton
ariaLabel="OK" ariaLabel="OK"
data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
text="OK" text="OK"
@ -1545,6 +1547,7 @@ exports[`Table query select Panel should render Default properly 1`] = `
> >
<CustomizedDefaultButton <CustomizedDefaultButton
ariaLabel="OK" ariaLabel="OK"
data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
onRenderDescription={[Function]} onRenderDescription={[Function]}
@ -1827,6 +1830,7 @@ exports[`Table query select Panel should render Default properly 1`] = `
> >
<DefaultButton <DefaultButton
ariaLabel="OK" ariaLabel="OK"
data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
onRenderDescription={[Function]} onRenderDescription={[Function]}
@ -2110,6 +2114,7 @@ exports[`Table query select Panel should render Default properly 1`] = `
<BaseButton <BaseButton
ariaLabel="OK" ariaLabel="OK"
baseClassName="ms-Button" baseClassName="ms-Button"
data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
onRenderDescription={[Function]} onRenderDescription={[Function]}
@ -2984,6 +2989,7 @@ exports[`Table query select Panel should render Default properly 1`] = `
aria-label="OK" aria-label="OK"
className="ms-Button ms-Button--primary root-125" className="ms-Button ms-Button--primary root-125"
data-is-focusable={true} data-is-focusable={true}
data-test="Panel/OkButton"
id="sidePanelOkButton" id="sidePanelOkButton"
onClick={[Function]} onClick={[Function]}
onKeyDown={[Function]} onKeyDown={[Function]}
@ -3019,6 +3025,11 @@ exports[`Table query select Panel should render Default properly 1`] = `
</div> </div>
</PanelFooterComponent> </PanelFooterComponent>
</form> </form>
<span
className="screenReaderOnly"
id="screenReaderStatus"
role="status"
/>
</RightPaneForm> </RightPaneForm>
</TableQuerySelectPanel> </TableQuerySelectPanel>
`; `;

View File

@ -369,6 +369,7 @@ exports[`Excute Add Table Entity Pane should render Default properly 1`] = `
> >
<CustomizedPrimaryButton <CustomizedPrimaryButton
ariaLabel="Add Entity" ariaLabel="Add Entity"
data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
text="Add Entity" text="Add Entity"
@ -376,6 +377,7 @@ exports[`Excute Add Table Entity Pane should render Default properly 1`] = `
> >
<PrimaryButton <PrimaryButton
ariaLabel="Add Entity" ariaLabel="Add Entity"
data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
text="Add Entity" text="Add Entity"
@ -656,6 +658,7 @@ exports[`Excute Add Table Entity Pane should render Default properly 1`] = `
> >
<CustomizedDefaultButton <CustomizedDefaultButton
ariaLabel="Add Entity" ariaLabel="Add Entity"
data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
onRenderDescription={[Function]} onRenderDescription={[Function]}
@ -938,6 +941,7 @@ exports[`Excute Add Table Entity Pane should render Default properly 1`] = `
> >
<DefaultButton <DefaultButton
ariaLabel="Add Entity" ariaLabel="Add Entity"
data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
onRenderDescription={[Function]} onRenderDescription={[Function]}
@ -1221,6 +1225,7 @@ exports[`Excute Add Table Entity Pane should render Default properly 1`] = `
<BaseButton <BaseButton
ariaLabel="Add Entity" ariaLabel="Add Entity"
baseClassName="ms-Button" baseClassName="ms-Button"
data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
onRenderDescription={[Function]} onRenderDescription={[Function]}
@ -2095,6 +2100,7 @@ exports[`Excute Add Table Entity Pane should render Default properly 1`] = `
aria-label="Add Entity" aria-label="Add Entity"
className="ms-Button ms-Button--primary root-113" className="ms-Button ms-Button--primary root-113"
data-is-focusable={true} data-is-focusable={true}
data-test="Panel/OkButton"
id="sidePanelOkButton" id="sidePanelOkButton"
onClick={[Function]} onClick={[Function]}
onKeyDown={[Function]} onKeyDown={[Function]}
@ -2130,6 +2136,11 @@ exports[`Excute Add Table Entity Pane should render Default properly 1`] = `
</div> </div>
</PanelFooterComponent> </PanelFooterComponent>
</form> </form>
<span
className="screenReaderOnly"
id="screenReaderStatus"
role="status"
/>
</RightPaneForm> </RightPaneForm>
</AddTableEntityPanel> </AddTableEntityPanel>
`; `;

View File

@ -375,6 +375,7 @@ exports[`Excute Edit Table Entity Pane should render Default properly 1`] = `
> >
<CustomizedPrimaryButton <CustomizedPrimaryButton
ariaLabel="Update" ariaLabel="Update"
data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
text="Update" text="Update"
@ -382,6 +383,7 @@ exports[`Excute Edit Table Entity Pane should render Default properly 1`] = `
> >
<PrimaryButton <PrimaryButton
ariaLabel="Update" ariaLabel="Update"
data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
text="Update" text="Update"
@ -662,6 +664,7 @@ exports[`Excute Edit Table Entity Pane should render Default properly 1`] = `
> >
<CustomizedDefaultButton <CustomizedDefaultButton
ariaLabel="Update" ariaLabel="Update"
data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
onRenderDescription={[Function]} onRenderDescription={[Function]}
@ -944,6 +947,7 @@ exports[`Excute Edit Table Entity Pane should render Default properly 1`] = `
> >
<DefaultButton <DefaultButton
ariaLabel="Update" ariaLabel="Update"
data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
onRenderDescription={[Function]} onRenderDescription={[Function]}
@ -1227,6 +1231,7 @@ exports[`Excute Edit Table Entity Pane should render Default properly 1`] = `
<BaseButton <BaseButton
ariaLabel="Update" ariaLabel="Update"
baseClassName="ms-Button" baseClassName="ms-Button"
data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
onRenderDescription={[Function]} onRenderDescription={[Function]}
@ -2101,6 +2106,7 @@ exports[`Excute Edit Table Entity Pane should render Default properly 1`] = `
aria-label="Update" aria-label="Update"
className="ms-Button ms-Button--primary root-113" className="ms-Button ms-Button--primary root-113"
data-is-focusable={true} data-is-focusable={true}
data-test="Panel/OkButton"
id="sidePanelOkButton" id="sidePanelOkButton"
onClick={[Function]} onClick={[Function]}
onKeyDown={[Function]} onKeyDown={[Function]}
@ -2136,6 +2142,11 @@ exports[`Excute Edit Table Entity Pane should render Default properly 1`] = `
</div> </div>
</PanelFooterComponent> </PanelFooterComponent>
</form> </form>
<span
className="screenReaderOnly"
id="screenReaderStatus"
role="status"
/>
</RightPaneForm> </RightPaneForm>
</EditTableEntityPanel> </EditTableEntityPanel>
`; `;

View File

@ -1,91 +0,0 @@
import { Upload } from "Common/Upload/Upload";
import { useSidePanel } from "hooks/useSidePanel";
import React, { ChangeEvent, FunctionComponent, useState } from "react";
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "Utils/NotificationConsoleUtils";
import { NotebookContentItem } from "../../Notebook/NotebookContentItem";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
export interface UploadFilePanelProps {
uploadFile: (name: string, content: string) => Promise<NotebookContentItem>;
}
export const UploadFilePane: FunctionComponent<UploadFilePanelProps> = ({ uploadFile }: UploadFilePanelProps) => {
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
const extensions: string = undefined; //ex. ".ipynb"
const errorMessage = "Could not upload file";
const inProgressMessage = "Uploading file to notebook server";
const successMessage = "Successfully uploaded file to notebook server";
const [files, setFiles] = useState<FileList>();
const [formErrors, setFormErrors] = useState<string>("");
const [isExecuting, setIsExecuting] = useState<boolean>(false);
const submit = () => {
setFormErrors("");
if (!files || files.length === 0) {
setFormErrors("No file specified. Please input a file.");
logConsoleError(`${errorMessage} -- No file specified. Please input a file.`);
return;
}
const file: File = files.item(0);
const clearMessage = logConsoleProgress(`${inProgressMessage}: ${file.name}`);
setIsExecuting(true);
onSubmit(files.item(0))
.then(
() => {
logConsoleInfo(`${successMessage} ${file.name}`);
closeSidePanel();
},
(error: string) => {
setFormErrors(errorMessage);
logConsoleError(`${errorMessage} ${file.name}: ${error}`);
},
)
.finally(() => {
setIsExecuting(false);
clearMessage();
});
};
const updateSelectedFiles = (event: ChangeEvent<HTMLInputElement>): void => {
setFiles(event.target.files);
};
const onSubmit = async (file: File): Promise<NotebookContentItem> => {
const readFileAsText = (inputFile: File): Promise<string> => {
const reader = new FileReader();
return new Promise((resolve, reject) => {
reader.onerror = () => {
reader.abort();
reject(`Problem parsing file: ${inputFile}`);
};
reader.onload = () => {
resolve(reader.result as string);
};
reader.readAsText(inputFile);
});
};
const fileContent = await readFileAsText(file);
return uploadFile(file.name, fileContent);
};
const props: RightPaneFormProps = {
formError: formErrors,
isExecuting: isExecuting,
submitButtonText: "Upload",
onSubmit: submit,
};
return (
<RightPaneForm {...props}>
<div className="paneMainContent">
<Upload label="Select file to upload" accept={extensions} onUpload={updateSelectedFiles} />
</div>
</RightPaneForm>
);
};

View File

@ -0,0 +1,84 @@
import "@testing-library/jest-dom/extend-expect";
import { RenderResult, fireEvent, render, screen, waitFor } from "@testing-library/react";
import { VectorEmbedding, VectorIndex } from "Contracts/DataModels";
import React from "react";
import { AddVectorEmbeddingPolicyForm } from "./AddVectorEmbeddingPolicyForm";
const mockVectorEmbedding: VectorEmbedding[] = [
{ path: "/vector1", dataType: "float32", distanceFunction: "euclidean", dimensions: 0 },
];
const mockVectorIndex: VectorIndex[] = [{ path: "/vector1", type: "flat" }];
const mockOnVectorEmbeddingChange = jest.fn();
describe("AddVectorEmbeddingPolicyForm", () => {
let component: RenderResult;
beforeEach(() => {
component = render(
<AddVectorEmbeddingPolicyForm
vectorEmbedding={mockVectorEmbedding}
vectorIndex={mockVectorIndex}
onVectorEmbeddingChange={mockOnVectorEmbeddingChange}
/>,
);
});
test("renders correctly", () => {
expect(screen.getByText("Vector embedding 1")).toBeInTheDocument();
expect(screen.getByPlaceholderText("/vector1")).toBeInTheDocument();
});
test("calls onVectorEmbeddingChange on adding a new vector embedding", () => {
fireEvent.click(screen.getByText("Add vector embedding"));
expect(mockOnVectorEmbeddingChange).toHaveBeenCalled();
});
test("calls onDelete when delete button is clicked", async () => {
const deleteButton = component.container.querySelector("#delete-vector-policy-1");
fireEvent.click(deleteButton);
expect(mockOnVectorEmbeddingChange).toHaveBeenCalled();
expect(screen.queryByText("Vector embedding 1")).toBeNull();
});
test("calls onVectorEmbeddingPathChange on input change", () => {
fireEvent.change(screen.getByPlaceholderText("/vector1"), { target: { value: "/newPath" } });
expect(mockOnVectorEmbeddingChange).toHaveBeenCalled();
});
test("validates input correctly", async () => {
fireEvent.change(screen.getByPlaceholderText("/vector1"), { target: { value: "" } });
await waitFor(() => expect(screen.getByText("Vector embedding path should not be empty")).toBeInTheDocument(), {
timeout: 1500,
});
await waitFor(
() =>
expect(
screen.getByText("Vector embedding dimension must be greater than 0 and less than or equal 4096"),
).toBeInTheDocument(),
{
timeout: 1500,
},
);
fireEvent.change(component.container.querySelector("#vector-policy-dimension-1"), { target: { value: "4096" } });
fireEvent.change(screen.getByPlaceholderText("/vector1"), { target: { value: "/vector1" } });
await waitFor(() => expect(screen.queryByText("Vector embedding path should not be empty")).toBeNull(), {
timeout: 1500,
});
await waitFor(
() => expect(screen.queryByText("Maximum allowed dimension for flat index is 505")).toBeInTheDocument(),
{
timeout: 1500,
},
);
});
test("duplicate vector path is not allowed", async () => {
fireEvent.click(screen.getByText("Add vector embedding"));
fireEvent.change(component.container.querySelector("#vector-policy-path-2"), { target: { value: "/vector1" } });
await waitFor(() => expect(screen.queryByText("Vector embedding path is already defined")).toBeNull(), {
timeout: 1500,
});
});
});

View File

@ -0,0 +1,300 @@
import {
DefaultButton,
Dropdown,
IDropdownOption,
IStyleFunctionOrObject,
ITextFieldStyleProps,
ITextFieldStyles,
IconButton,
Label,
Stack,
TextField,
} from "@fluentui/react";
import { VectorEmbedding, VectorIndex } from "Contracts/DataModels";
import { CollapsibleSectionComponent } from "Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent";
import {
getDataTypeOptions,
getDistanceFunctionOptions,
getIndexTypeOptions,
} from "Explorer/Panes/VectorSearchPanel/VectorSearchUtils";
import React, { FunctionComponent, useState } from "react";
export interface IAddVectorEmbeddingPolicyFormProps {
vectorEmbedding: VectorEmbedding[];
vectorIndex: VectorIndex[];
onVectorEmbeddingChange: (
vectorEmbeddings: VectorEmbedding[],
vectorIndexingPolicies: VectorIndex[],
validationPassed: boolean,
) => void;
}
export interface VectorEmbeddingPolicyData {
path: string;
dataType: VectorEmbedding["dataType"];
distanceFunction: VectorEmbedding["distanceFunction"];
dimensions: number;
indexType: VectorIndex["type"] | "none";
pathError: string;
dimensionsError: string;
}
type VectorEmbeddingPolicyProperty = "dataType" | "distanceFunction" | "indexType";
const textFieldStyles: IStyleFunctionOrObject<ITextFieldStyleProps, ITextFieldStyles> = {
fieldGroup: {
height: 27,
},
field: {
fontSize: 12,
padding: "0 8px",
},
};
const dropdownStyles = {
title: {
height: 27,
lineHeight: "24px",
fontSize: 12,
},
dropdown: {
height: 27,
lineHeight: "24px",
},
dropdownItem: {
fontSize: 12,
},
};
export const AddVectorEmbeddingPolicyForm: FunctionComponent<IAddVectorEmbeddingPolicyFormProps> = ({
vectorEmbedding,
vectorIndex,
onVectorEmbeddingChange,
}): JSX.Element => {
const onVectorEmbeddingPathError = (path: string, index?: number): string => {
let error = "";
if (!path) {
error = "Vector embedding path should not be empty";
}
if (
index >= 0 &&
vectorEmbeddingPolicyData?.find(
(vectorEmbedding: VectorEmbeddingPolicyData, dataIndex: number) =>
dataIndex !== index && vectorEmbedding.path === path,
)
) {
error = "Vector embedding path is already defined";
}
return error;
};
const onVectorEmbeddingDimensionError = (dimension: number, indexType: VectorIndex["type"] | "none"): string => {
let error = "";
if (dimension <= 0 || dimension > 4096) {
error = "Vector embedding dimension must be greater than 0 and less than or equal 4096";
}
if (indexType === "flat" && dimension > 505) {
error = "Maximum allowed dimension for flat index is 505";
}
return error;
};
const initializeData = (vectorEmbedding: VectorEmbedding[], vectorIndex: VectorIndex[]) => {
const mergedData: VectorEmbeddingPolicyData[] = [];
vectorEmbedding.forEach((embedding) => {
const matchingIndex = vectorIndex.find((index) => index.path === embedding.path);
mergedData.push({
...embedding,
indexType: matchingIndex?.type || "none",
pathError: onVectorEmbeddingPathError(embedding.path),
dimensionsError: onVectorEmbeddingDimensionError(embedding.dimensions, matchingIndex?.type || "none"),
});
});
return mergedData;
};
const [vectorEmbeddingPolicyData, setVectorEmbeddingPolicyData] = useState<VectorEmbeddingPolicyData[]>(
initializeData(vectorEmbedding, vectorIndex),
);
React.useEffect(() => {
propagateData();
}, [vectorEmbeddingPolicyData]);
const propagateData = () => {
const vectorEmbeddings: VectorEmbedding[] = vectorEmbeddingPolicyData.map((policy: VectorEmbeddingPolicyData) => ({
dataType: policy.dataType,
dimensions: policy.dimensions,
distanceFunction: policy.distanceFunction,
path: policy.path,
}));
const vectorIndexingPolicies: VectorIndex[] = vectorEmbeddingPolicyData
.filter((policy: VectorEmbeddingPolicyData) => policy.indexType !== "none")
.map(
(policy) =>
({
path: policy.path,
type: policy.indexType,
}) as VectorIndex,
);
const validationPassed = vectorEmbeddingPolicyData.every(
(policy: VectorEmbeddingPolicyData) => policy.pathError === "" && policy.dimensionsError === "",
);
onVectorEmbeddingChange(vectorEmbeddings, vectorIndexingPolicies, validationPassed);
};
const onVectorEmbeddingPathChange = (index: number, event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value.trim();
const vectorEmbeddings = [...vectorEmbeddingPolicyData];
if (!vectorEmbeddings[index]?.path && !value.startsWith("/")) {
vectorEmbeddings[index].path = "/" + value;
} else {
vectorEmbeddings[index].path = value;
}
const error = onVectorEmbeddingPathError(value, index);
vectorEmbeddings[index].pathError = error;
setVectorEmbeddingPolicyData(vectorEmbeddings);
};
const onVectorEmbeddingDimensionsChange = (index: number, event: React.ChangeEvent<HTMLInputElement>) => {
const value = parseInt(event.target.value.trim()) || 0;
const vectorEmbeddings = [...vectorEmbeddingPolicyData];
const vectorEmbedding = vectorEmbeddings[index];
vectorEmbeddings[index].dimensions = value;
const error = onVectorEmbeddingDimensionError(value, vectorEmbedding.indexType);
vectorEmbeddings[index].dimensionsError = error;
setVectorEmbeddingPolicyData(vectorEmbeddings);
};
const onVectorEmbeddingIndexTypeChange = (index: number, option: IDropdownOption): void => {
const vectorEmbeddings = [...vectorEmbeddingPolicyData];
const vectorEmbedding = vectorEmbeddings[index];
vectorEmbeddings[index].indexType = option.key as never;
const error = onVectorEmbeddingDimensionError(vectorEmbedding.dimensions, vectorEmbedding.indexType);
vectorEmbeddings[index].dimensionsError = error;
setVectorEmbeddingPolicyData(vectorEmbeddings);
};
const onVectorEmbeddingPolicyChange = (
index: number,
option: IDropdownOption,
property: VectorEmbeddingPolicyProperty,
): void => {
const vectorEmbeddings = [...vectorEmbeddingPolicyData];
vectorEmbeddings[index][property] = option.key as never;
setVectorEmbeddingPolicyData(vectorEmbeddings);
};
const onAdd = () => {
setVectorEmbeddingPolicyData([
...vectorEmbeddingPolicyData,
{
path: "",
dataType: "float32",
distanceFunction: "euclidean",
dimensions: 0,
indexType: "none",
pathError: onVectorEmbeddingPathError(""),
dimensionsError: onVectorEmbeddingDimensionError(0, "none"),
},
]);
};
const onDelete = (index: number) => {
const vectorEmbeddings = vectorEmbeddingPolicyData.filter((_uniqueKey, j) => index !== j);
setVectorEmbeddingPolicyData(vectorEmbeddings);
};
return (
<Stack tokens={{ childrenGap: 4 }}>
{vectorEmbeddingPolicyData.length > 0 &&
vectorEmbeddingPolicyData.map((vectorEmbeddingPolicy: VectorEmbeddingPolicyData, index: number) => (
<CollapsibleSectionComponent key={index} isExpandedByDefault={true} title={`Vector embedding ${index + 1}`}>
<Stack horizontal tokens={{ childrenGap: 4 }}>
<Stack
styles={{
root: {
margin: "0 0 6px 20px !important",
paddingLeft: 20,
width: "80%",
borderLeft: "1px solid",
},
}}
>
<Stack>
<Label styles={{ root: { fontSize: 12 } }}>Path</Label>
<TextField
id={`vector-policy-path-${index + 1}`}
required={true}
placeholder="/vector1"
styles={textFieldStyles}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => onVectorEmbeddingPathChange(index, event)}
value={vectorEmbeddingPolicy.path || ""}
errorMessage={vectorEmbeddingPolicy.pathError}
/>
</Stack>
<Stack>
<Label styles={{ root: { fontSize: 12 } }}>Data type</Label>
<Dropdown
required={true}
styles={dropdownStyles}
options={getDataTypeOptions()}
selectedKey={vectorEmbeddingPolicy.dataType}
onChange={(_event: React.FormEvent<HTMLDivElement>, option: IDropdownOption) =>
onVectorEmbeddingPolicyChange(index, option, "dataType")
}
></Dropdown>
</Stack>
<Stack>
<Label styles={{ root: { fontSize: 12 } }}>Distance function</Label>
<Dropdown
required={true}
styles={dropdownStyles}
options={getDistanceFunctionOptions()}
selectedKey={vectorEmbeddingPolicy.distanceFunction}
onChange={(_event: React.FormEvent<HTMLDivElement>, option: IDropdownOption) =>
onVectorEmbeddingPolicyChange(index, option, "distanceFunction")
}
></Dropdown>
</Stack>
<Stack>
<Label styles={{ root: { fontSize: 12 } }}>Dimensions</Label>
<TextField
id={`vector-policy-dimension-${index + 1}`}
required={true}
styles={textFieldStyles}
value={String(vectorEmbeddingPolicy.dimensions || 0)}
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
onVectorEmbeddingDimensionsChange(index, event)
}
errorMessage={vectorEmbeddingPolicy.dimensionsError}
/>
</Stack>
<Stack>
<Label styles={{ root: { fontSize: 12 } }}>Index type</Label>
<Dropdown
required={true}
styles={dropdownStyles}
options={getIndexTypeOptions()}
selectedKey={vectorEmbeddingPolicy.indexType}
onChange={(_event: React.FormEvent<HTMLDivElement>, option: IDropdownOption) =>
onVectorEmbeddingIndexTypeChange(index, option)
}
></Dropdown>
</Stack>
</Stack>
<IconButton
id={`delete-vector-policy-${index + 1}`}
iconProps={{ iconName: "Delete" }}
style={{ height: 27, margin: "auto" }}
onClick={() => onDelete(index)}
/>
</Stack>
</CollapsibleSectionComponent>
))}
<DefaultButton id={`add-vector-policy`} styles={{ root: { maxWidth: 170, fontSize: 12 } }} onClick={onAdd}>
Add vector embedding
</DefaultButton>
</Stack>
);
};

View File

@ -0,0 +1,16 @@
import { IDropdownOption } from "@fluentui/react";
const dataTypes = ["float32", "uint8", "int8"];
const distanceFunctions = ["euclidean", "cosine", "dotproduct"];
const indexTypes = ["none", "flat", "diskANN", "quantizedFlat"];
export const getDataTypeOptions = (): IDropdownOption[] => createDropdownOptionsFromLiterals(dataTypes);
export const getDistanceFunctionOptions = (): IDropdownOption[] => createDropdownOptionsFromLiterals(distanceFunctions);
export const getIndexTypeOptions = (): IDropdownOption[] => createDropdownOptionsFromLiterals(indexTypes);
function createDropdownOptionsFromLiterals<T extends string>(literals: T[]): IDropdownOption[] {
return literals.map((value) => ({
key: value,
text: value,
}));
}

View File

@ -3,6 +3,7 @@
exports[`AddCollectionPanel should render Default properly 1`] = ` exports[`AddCollectionPanel should render Default properly 1`] = `
<form <form
className="panelFormWrapper" className="panelFormWrapper"
id="panelContainer"
onSubmit={[Function]} onSubmit={[Function]}
> >
<div <div
@ -433,7 +434,7 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
> >
<Stack <Stack
className="panelGroupSpacing" className="panelGroupSpacing"
id="collapsibleSectionContent" id="collapsibleAdvancedSectionContent"
> >
<Stack <Stack
className="panelGroupSpacing" className="panelGroupSpacing"
@ -466,7 +467,6 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
<Icon <Icon
className="removeIcon" className="removeIcon"
iconName="InfoSolid" iconName="InfoSolid"
tabIndex={0}
/> />
To ensure compatibility with older SDKs, the created container will use a legacy partitioning scheme that supports partition key values of size only up to 101 bytes. If this is enabled, you will not be able to use hierarchical partition keys. To ensure compatibility with older SDKs, the created container will use a legacy partitioning scheme that supports partition key values of size only up to 101 bytes. If this is enabled, you will not be able to use hierarchical partition keys.
@ -486,4 +486,4 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
isButtonDisabled={false} isButtonDisabled={false}
/> />
</form> </form>
`; `;

View File

@ -361,12 +361,15 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
<span <span
className="css-113" className="css-113"
> >
Confirm by typing the database id Confirm by typing the
Database
id
</span> </span>
</Text> </Text>
<StyledTextFieldBase <StyledTextFieldBase
ariaLabel="Confirm by typing the database id" ariaLabel="Confirm by typing the Database id"
autoFocus={true} autoFocus={true}
data-test="Input:confirmDatabaseId"
id="confirmDatabaseId" id="confirmDatabaseId"
onChange={[Function]} onChange={[Function]}
required={true} required={true}
@ -379,8 +382,9 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
} }
> >
<TextFieldBase <TextFieldBase
ariaLabel="Confirm by typing the database id" ariaLabel="Confirm by typing the Database id"
autoFocus={true} autoFocus={true}
data-test="Input:confirmDatabaseId"
deferredValidationTime={200} deferredValidationTime={200}
id="confirmDatabaseId" id="confirmDatabaseId"
onChange={[Function]} onChange={[Function]}
@ -673,9 +677,10 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
> >
<input <input
aria-invalid={false} aria-invalid={false}
aria-label="Confirm by typing the database id" aria-label="Confirm by typing the Database id"
autoFocus={true} autoFocus={true}
className="ms-TextField-field field-117" className="ms-TextField-field field-117"
data-test="Input:confirmDatabaseId"
id="confirmDatabaseId" id="confirmDatabaseId"
onBlur={[Function]} onBlur={[Function]}
onChange={[Function]} onChange={[Function]}
@ -711,11 +716,13 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
<span <span
className="css-126" className="css-126"
> >
What is the reason why you are deleting this database? What is the reason why you are deleting this
Database
?
</span> </span>
</Text> </Text>
<StyledTextFieldBase <StyledTextFieldBase
ariaLabel="Help us improve Azure Cosmos DB! What is the reason why you are deleting this database?" ariaLabel="Help us improve Azure Cosmos DB! What is the reason why you are deleting this Database?"
id="deleteDatabaseFeedbackInput" id="deleteDatabaseFeedbackInput"
multiline={true} multiline={true}
onChange={[Function]} onChange={[Function]}
@ -729,7 +736,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
} }
> >
<TextFieldBase <TextFieldBase
ariaLabel="Help us improve Azure Cosmos DB! What is the reason why you are deleting this database?" ariaLabel="Help us improve Azure Cosmos DB! What is the reason why you are deleting this Database?"
deferredValidationTime={200} deferredValidationTime={200}
id="deleteDatabaseFeedbackInput" id="deleteDatabaseFeedbackInput"
multiline={true} multiline={true}
@ -1023,7 +1030,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
> >
<textarea <textarea
aria-invalid={false} aria-invalid={false}
aria-label="Help us improve Azure Cosmos DB! What is the reason why you are deleting this database?" aria-label="Help us improve Azure Cosmos DB! What is the reason why you are deleting this Database?"
className="ms-TextField-field field-128" className="ms-TextField-field field-128"
id="deleteDatabaseFeedbackInput" id="deleteDatabaseFeedbackInput"
onBlur={[Function]} onBlur={[Function]}
@ -1049,6 +1056,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
> >
<CustomizedPrimaryButton <CustomizedPrimaryButton
ariaLabel="OK" ariaLabel="OK"
data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
text="OK" text="OK"
@ -1056,6 +1064,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
> >
<PrimaryButton <PrimaryButton
ariaLabel="OK" ariaLabel="OK"
data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
text="OK" text="OK"
@ -1336,6 +1345,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
> >
<CustomizedDefaultButton <CustomizedDefaultButton
ariaLabel="OK" ariaLabel="OK"
data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
onRenderDescription={[Function]} onRenderDescription={[Function]}
@ -1618,6 +1628,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
> >
<DefaultButton <DefaultButton
ariaLabel="OK" ariaLabel="OK"
data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
onRenderDescription={[Function]} onRenderDescription={[Function]}
@ -1901,6 +1912,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
<BaseButton <BaseButton
ariaLabel="OK" ariaLabel="OK"
baseClassName="ms-Button" baseClassName="ms-Button"
data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
onRenderDescription={[Function]} onRenderDescription={[Function]}
@ -2775,6 +2787,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
aria-label="OK" aria-label="OK"
className="ms-Button ms-Button--primary root-130" className="ms-Button ms-Button--primary root-130"
data-is-focusable={true} data-is-focusable={true}
data-test="Panel/OkButton"
id="sidePanelOkButton" id="sidePanelOkButton"
onClick={[Function]} onClick={[Function]}
onKeyDown={[Function]} onKeyDown={[Function]}
@ -2810,6 +2823,11 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
</div> </div>
</PanelFooterComponent> </PanelFooterComponent>
</form> </form>
<span
className="screenReaderOnly"
id="screenReaderStatus"
role="status"
/>
</RightPaneForm> </RightPaneForm>
</DeleteDatabaseConfirmationPanel> </DeleteDatabaseConfirmationPanel>
`; `;

View File

@ -4,6 +4,7 @@ exports[`PaneContainerComponent test should be resize if notification console is
<StyledPanelBase <StyledPanelBase
closeButtonAriaLabel="Close test" closeButtonAriaLabel="Close test"
customWidth="440px" customWidth="440px"
data-test="Panel:test"
headerClassName="panelHeader" headerClassName="panelHeader"
headerText="test" headerText="test"
isFooterAtBottom={true} isFooterAtBottom={true}
@ -44,6 +45,7 @@ exports[`PaneContainerComponent test should render with panel content and header
<StyledPanelBase <StyledPanelBase
closeButtonAriaLabel="Close test" closeButtonAriaLabel="Close test"
customWidth="440px" customWidth="440px"
data-test="Panel:test"
headerClassName="panelHeader" headerClassName="panelHeader"
headerText="test" headerText="test"
isFooterAtBottom={true} isFooterAtBottom={true}

View File

@ -385,7 +385,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
hasSmallHeadline={true} hasSmallHeadline={true}
headline="Write a prompt" headline="Write a prompt"
> >
Write a prompt here and Copilot will generate the query for you. You can also choose from our{" "} Write a prompt here and Query Advisor will generate the query for you. You can also choose from our{" "}
<Link <Link
onClick={() => { onClick={() => {
setShowSamplePrompts(true); setShowSamplePrompts(true);
@ -504,7 +504,12 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
{!showFeedbackBar && ( {!showFeedbackBar && (
<Text style={{ fontSize: 12 }}> <Text style={{ fontSize: 12 }}>
AI-generated content can have mistakes. Make sure it&apos;s accurate and appropriate before using it.{" "} AI-generated content can have mistakes. Make sure it&apos;s accurate and appropriate before using it.{" "}
<Link href="https://aka.ms/cdb-copilot-preview-terms" target="_blank" style={{ color: "#0072D4" }}> <Link
href="https://aka.ms/cdb-copilot-preview-terms"
target="_blank"
style={{ color: "#0072D4" }}
className="underlinedLink"
>
Read preview terms Read preview terms
</Link> </Link>
{showErrorMessageBar && ( {showErrorMessageBar && (

View File

@ -57,12 +57,12 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
const toggleCopilotButton = { const toggleCopilotButton = {
iconSrc: QueryCommandIcon, iconSrc: QueryCommandIcon,
iconAlt: "Copilot", iconAlt: "Query Advisor",
onCommandClick: () => { onCommandClick: () => {
toggleCopilot(true); toggleCopilot(true);
}, },
commandButtonLabel: "Copilot", commandButtonLabel: "Query Advisor",
ariaLabel: "Copilot", ariaLabel: "Query Advisor",
hasPopup: false, hasPopup: false,
disabled: copilotActive, disabled: copilotActive,
}; };

View File

@ -23,7 +23,6 @@ exports[`Query copilot tab snapshot test should render with initial input 1`] =
explorer={ explorer={
Explorer { Explorer {
"_isInitializingNotebooks": false, "_isInitializingNotebooks": false,
"_resetNotebookWorkspace": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function], "isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function], "onRefreshDatabasesKeyPress": [Function],

View File

@ -25,7 +25,6 @@ import * as React from "react";
import ConnectIcon from "../../../images/Connect_color.svg"; import ConnectIcon from "../../../images/Connect_color.svg";
import ContainersIcon from "../../../images/Containers.svg"; import ContainersIcon from "../../../images/Containers.svg";
import LinkIcon from "../../../images/Link_blue.svg"; import LinkIcon from "../../../images/Link_blue.svg";
import NotebookColorIcon from "../../../images/Notebooks.svg";
import PowerShellIcon from "../../../images/PowerShell.svg"; import PowerShellIcon from "../../../images/PowerShell.svg";
import CopilotIcon from "../../../images/QueryCopilotNewLogo.svg"; import CopilotIcon from "../../../images/QueryCopilotNewLogo.svg";
import QuickStartIcon from "../../../images/Quickstart_Lightning.svg"; import QuickStartIcon from "../../../images/Quickstart_Lightning.svg";
@ -151,9 +150,9 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
{useQueryCopilot.getState().copilotEnabled && ( {useQueryCopilot.getState().copilotEnabled && (
<SplashScreenButton <SplashScreenButton
imgSrc={CopilotIcon} imgSrc={CopilotIcon}
title={"Query faster with Copilot"} title={"Query faster with Query Advisor"}
description={ description={
"Copilot is your AI buddy that helps you write Azure Cosmos DB queries like a pro. Try it using our sample data set now!" "Query Advisor is your AI buddy that helps you write Azure Cosmos DB queries like a pro. Try it using our sample data set now!"
} }
onClick={() => { onClick={() => {
const copilotVersion = userContext.features.copilotVersion; const copilotVersion = userContext.features.copilotVersion;
@ -356,15 +355,15 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
) : ( ) : (
<div className="moreStuffContainer"> <div className="moreStuffContainer">
<div className="moreStuffColumn commonTasks"> <div className="moreStuffColumn commonTasks">
<div className="title">Recents</div> <h2 className="title">Recents</h2>
{this.getRecentItems()} {this.getRecentItems()}
</div> </div>
<div className="moreStuffColumn"> <div className="moreStuffColumn">
<div className="title">Top 3 things you need to know</div> <h2 className="title">Top 3 things you need to know</h2>
{this.top3Items()} {this.top3Items()}
</div> </div>
<div className="moreStuffColumn tipsContainer"> <div className="moreStuffColumn tipsContainer">
<div className="title">Learning Resources</div> <h2 className="title">Learning Resources</h2>
{this.getLearningResourceItems()} {this.getLearningResourceItems()}
</div> </div>
</div> </div>
@ -410,14 +409,6 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
}, },
}; };
heroes.push(launchQuickstartBtn); heroes.push(launchQuickstartBtn);
} else if (useNotebook.getState().isPhoenixNotebooks) {
const newNotebookBtn = {
iconSrc: NotebookColorIcon,
title: "New notebook",
description: "Visualize your data stored in Azure Cosmos DB",
onClick: () => this.container.onNewNotebookClicked(),
};
heroes.push(newNotebookBtn);
} }
heroes.push(this.getShellCard()); heroes.push(this.getShellCard());
@ -689,11 +680,20 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
title: "Learn the Fundamentals", title: "Learn the Fundamentals",
description: "Watch Azure Cosmos DB Live TV show introductory and how to videos.", description: "Watch Azure Cosmos DB Live TV show introductory and how to videos.",
}; };
let items: item[];
const commonItems: item[] = [
{
link: "https://learn.microsoft.com/azure/cosmos-db/data-explorer-shortcuts",
title: "Data Explorer keyboard shortcuts",
description: "Learn keyboard shortcuts to navigate Data Explorer.",
},
];
let apiItems: item[];
switch (userContext.apiType) { switch (userContext.apiType) {
case "SQL": case "SQL":
case "Postgres": case "Postgres":
items = [ apiItems = [
{ {
link: "https://aka.ms/msl-sdk-connect", link: "https://aka.ms/msl-sdk-connect",
title: "Get Started using an SDK", title: "Get Started using an SDK",
@ -708,7 +708,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
]; ];
break; break;
case "Mongo": case "Mongo":
items = [ apiItems = [
{ {
link: "https://aka.ms/mongonodejs", link: "https://aka.ms/mongonodejs",
title: "Build an app with Node.js", title: "Build an app with Node.js",
@ -723,7 +723,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
]; ];
break; break;
case "Cassandra": case "Cassandra":
items = [ apiItems = [
{ {
link: "https://aka.ms/cassandracontainer", link: "https://aka.ms/cassandracontainer",
title: "Create a Container", title: "Create a Container",
@ -738,7 +738,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
]; ];
break; break;
case "Gremlin": case "Gremlin":
items = [ apiItems = [
{ {
link: "https://aka.ms/graphquickstart", link: "https://aka.ms/graphquickstart",
title: "Get Started ", title: "Get Started ",
@ -753,7 +753,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
]; ];
break; break;
case "Tables": case "Tables":
items = [ apiItems = [
{ {
link: "https://aka.ms/tabledotnet", link: "https://aka.ms/tabledotnet",
title: "Build a .NET App", title: "Build a .NET App",
@ -770,6 +770,9 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
default: default:
break; break;
} }
const items = [...commonItems, ...apiItems];
return ( return (
<Stack> <Stack>
{items.map((item, i) => ( {items.map((item, i) => (

View File

@ -225,3 +225,8 @@ export const InputType = {
DateTime: "datetime-local", DateTime: "datetime-local",
Number: "number", Number: "number",
}; };
export const labelToLoadingItemName: Record<string, string> = {
"Add Row": "Adding row to table",
"Add Entity": "Adding entity to table",
};

View File

@ -1,5 +1,6 @@
import ko from "knockout"; import ko from "knockout";
import { isEnvironmentAltPressed, isEnvironmentCtrlPressed, isEnvironmentShiftPressed } from "Utils/KeyboardUtils";
import * as Constants from "../Constants"; import * as Constants from "../Constants";
import * as Entities from "../Entities"; import * as Entities from "../Entities";
import * as Utilities from "../Utilities"; import * as Utilities from "../Utilities";
@ -28,7 +29,7 @@ export default class DataTableOperationManager {
var elem: JQuery<Element> = $(event.currentTarget); var elem: JQuery<Element> = $(event.currentTarget);
this.updateLastSelectedItem(elem, event.shiftKey); this.updateLastSelectedItem(elem, event.shiftKey);
if (Utilities.isEnvironmentCtrlPressed(event)) { if (isEnvironmentCtrlPressed(event)) {
this.applyCtrlSelection(elem); this.applyCtrlSelection(elem);
} else if (event.shiftKey) { } else if (event.shiftKey) {
this.applyShiftSelection(elem); this.applyShiftSelection(elem);
@ -74,9 +75,9 @@ export default class DataTableOperationManager {
DataTableOperations.scrollToRowIfNeeded(dataTableRows, safeIndex, isUpArrowKey); DataTableOperations.scrollToRowIfNeeded(dataTableRows, safeIndex, isUpArrowKey);
} }
} else if ( } else if (
Utilities.isEnvironmentCtrlPressed(event) && isEnvironmentCtrlPressed(event) &&
!Utilities.isEnvironmentShiftPressed(event) && !isEnvironmentShiftPressed(event) &&
!Utilities.isEnvironmentAltPressed(event) && !isEnvironmentAltPressed(event) &&
event.keyCode === Constants.keyCodes.A event.keyCode === Constants.keyCodes.A
) { ) {
this.applySelectAll(); this.applySelectAll();

View File

@ -3,6 +3,7 @@ import * as ko from "knockout";
import Q from "q"; import Q from "q";
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
import * as Constants from "../../Common/Constants"; import * as Constants from "../../Common/Constants";
import { CassandraProxyAPIs, CassandraProxyEndpoints } from "../../Common/Constants";
import { handleError } from "../../Common/ErrorHandlingUtils"; import { handleError } from "../../Common/ErrorHandlingUtils";
import * as HeadersUtility from "../../Common/HeadersUtility"; import * as HeadersUtility from "../../Common/HeadersUtility";
import { createDocument } from "../../Common/dataAccess/createDocument"; import { createDocument } from "../../Common/dataAccess/createDocument";
@ -19,7 +20,6 @@ import Explorer from "../Explorer";
import * as TableConstants from "./Constants"; import * as TableConstants from "./Constants";
import * as Entities from "./Entities"; import * as Entities from "./Entities";
import * as TableEntityProcessor from "./TableEntityProcessor"; import * as TableEntityProcessor from "./TableEntityProcessor";
import { CassandraProxyAPIs, CassandraProxyEndpoints } from "../../Common/Constants";
export interface CassandraTableKeys { export interface CassandraTableKeys {
partitionKeys: CassandraTableKey[]; partitionKeys: CassandraTableKey[];
@ -172,8 +172,9 @@ export class CassandraAPIDataClient extends TableDataClient {
deferred.resolve(entity); deferred.resolve(entity);
}, },
(error) => { (error) => {
handleError(error, "AddRowCassandra", `Error while adding new row to table ${collection.id()}`); const errorText = error.responseJSON?.message ?? JSON.stringify(error);
deferred.reject(error); handleError(errorText, "AddRowCassandra", `Error while adding new row to table ${collection.id()}`);
deferred.reject(errorText);
}, },
) )
.finally(clearInProgressMessage); .finally(clearInProgressMessage);
@ -406,12 +407,13 @@ export class CassandraAPIDataClient extends TableDataClient {
deferred.resolve(); deferred.resolve();
}, },
(error) => { (error) => {
const errorText = error.responseJSON?.message ?? JSON.stringify(error);
handleError( handleError(
error, errorText,
"CreateKeyspaceCassandra", "CreateKeyspaceCassandra",
`Error while creating a keyspace with query ${createKeyspaceQuery}`, `Error while creating a keyspace with query ${createKeyspaceQuery}`,
); );
deferred.reject(error); deferred.reject(errorText);
}, },
) )
.finally(clearInProgressMessage); .finally(clearInProgressMessage);
@ -444,8 +446,13 @@ export class CassandraAPIDataClient extends TableDataClient {
deferred.resolve(); deferred.resolve();
}, },
(error) => { (error) => {
handleError(error, "CreateTableCassandra", `Error while creating a table with query ${createTableQuery}`); const errorText = error.responseJSON?.message ?? JSON.stringify(error);
deferred.reject(error); handleError(
errorText,
"CreateTableCassandra",
`Error while creating a table with query ${createTableQuery}`,
);
deferred.reject(errorText);
}, },
) )
.finally(clearInProgressMessage); .finally(clearInProgressMessage);
@ -493,8 +500,9 @@ export class CassandraAPIDataClient extends TableDataClient {
deferred.resolve(data); deferred.resolve(data);
}, },
(error: any) => { (error: any) => {
handleError(error, "FetchKeysCassandra", `Error fetching keys for table ${collection.id()}`); const errorText = error.responseJSON?.message ?? JSON.stringify(error);
deferred.reject(error); handleError(errorText, "FetchKeysCassandra", `Error fetching keys for table ${collection.id()}`);
deferred.reject(errorText);
}, },
) )
.done(clearInProgressMessage); .done(clearInProgressMessage);
@ -533,8 +541,9 @@ export class CassandraAPIDataClient extends TableDataClient {
deferred.resolve(data); deferred.resolve(data);
}, },
(error: any) => { (error: any) => {
handleError(error, "FetchKeysCassandra", `Error fetching keys for table ${collection.id()}`); const errorText = error.responseJSON?.message ?? JSON.stringify(error);
deferred.reject(error); handleError(errorText, "FetchKeysCassandra", `Error fetching keys for table ${collection.id()}`);
deferred.reject(errorText);
}, },
) )
.done(clearInProgressMessage); .done(clearInProgressMessage);
@ -578,8 +587,9 @@ export class CassandraAPIDataClient extends TableDataClient {
deferred.resolve(data.columns); deferred.resolve(data.columns);
}, },
(error: any) => { (error: any) => {
handleError(error, "FetchSchemaCassandra", `Error fetching schema for table ${collection.id()}`); const errorText = error.responseJSON?.message ?? JSON.stringify(error);
deferred.reject(error); handleError(errorText, "FetchSchemaCassandra", `Error fetching schema for table ${collection.id()}`);
deferred.reject(errorText);
}, },
) )
.done(clearInProgressMessage); .done(clearInProgressMessage);
@ -618,8 +628,9 @@ export class CassandraAPIDataClient extends TableDataClient {
deferred.resolve(data.columns); deferred.resolve(data.columns);
}, },
(error: any) => { (error: any) => {
handleError(error, "FetchSchemaCassandra", `Error fetching schema for table ${collection.id()}`); const errorText = error.responseJSON?.message ?? JSON.stringify(error);
deferred.reject(error); handleError(errorText, "FetchSchemaCassandra", `Error fetching schema for table ${collection.id()}`);
deferred.reject(errorText);
}, },
) )
.done(clearInProgressMessage); .done(clearInProgressMessage);
@ -734,10 +745,14 @@ export class CassandraAPIDataClient extends TableDataClient {
private useCassandraProxyEndpoint(api: string): boolean { private useCassandraProxyEndpoint(api: string): boolean {
const activeCassandraProxyEndpoints: string[] = [ const activeCassandraProxyEndpoints: string[] = [
CassandraProxyEndpoints.Development, CassandraProxyEndpoints.Development,
CassandraProxyEndpoints.Mpac CassandraProxyEndpoints.Mpac,
CassandraProxyEndpoints.Prod,
]; ];
let canAccessCassandraProxy: boolean = userContext.databaseAccount.properties.publicNetworkAccess === "Enabled"; let canAccessCassandraProxy: boolean = userContext.databaseAccount.properties.publicNetworkAccess === "Enabled";
if (userContext.databaseAccount.properties.ipRules?.length > 0) { if (
configContext.CASSANDRA_PROXY_ENDPOINT !== CassandraProxyEndpoints.Development &&
userContext.databaseAccount.properties.ipRules?.length > 0
) {
canAccessCassandraProxy = canAccessCassandraProxy && configContext.CASSANDRA_PROXY_OUTBOUND_IPS_ALLOWLISTED; canAccessCassandraProxy = canAccessCassandraProxy && configContext.CASSANDRA_PROXY_OUTBOUND_IPS_ALLOWLISTED;
} }

View File

@ -1,8 +1,8 @@
import * as _ from "underscore";
import Q from "q"; import Q from "q";
import * as _ from "underscore";
import * as Constants from "./Constants";
import * as Entities from "./Entities"; import * as Entities from "./Entities";
import { CassandraTableKey } from "./TableDataClient"; import { CassandraTableKey } from "./TableDataClient";
import * as Constants from "./Constants";
/** /**
* Generates a pseudo-random GUID. * Generates a pseudo-random GUID.
@ -180,30 +180,6 @@ export function onEsc(
return onKey(event, Constants.keyCodes.Esc, action, metaKey, shiftKey, altKey); return onKey(event, Constants.keyCodes.Esc, action, metaKey, shiftKey, altKey);
} }
/**
* Is the environment 'ctrl' key press. This key is used for multi selection, like select one more item, select all.
* For Windows and Linux, it's ctrl. For Mac, it's command.
*/
export function isEnvironmentCtrlPressed(event: JQueryEventObject): boolean {
return isMac() ? event.metaKey : event.ctrlKey;
}
export function isEnvironmentShiftPressed(event: JQueryEventObject): boolean {
return event.shiftKey;
}
export function isEnvironmentAltPressed(event: JQueryEventObject): boolean {
return event.altKey;
}
/**
* Returns whether the current platform is MacOS.
*/
export function isMac(): boolean {
var platform = navigator.platform.toUpperCase();
return platform.indexOf("MAC") >= 0;
}
// MAX_SAFE_INTEGER and MIN_SAFE_INTEGER will be provided by ECMAScript 6's Number // MAX_SAFE_INTEGER and MIN_SAFE_INTEGER will be provided by ECMAScript 6's Number
export var MAX_SAFE_INTEGER = Math.pow(2, 53) - 1; export var MAX_SAFE_INTEGER = Math.pow(2, 53) - 1;
export var MIN_SAFE_INTEGER = -MAX_SAFE_INTEGER; export var MIN_SAFE_INTEGER = -MAX_SAFE_INTEGER;

View File

@ -6,16 +6,16 @@ import DiscardIcon from "../../../images/discard.svg";
import SaveIcon from "../../../images/save-cosmos.svg"; import SaveIcon from "../../../images/save-cosmos.svg";
import * as Constants from "../../Common/Constants"; import * as Constants from "../../Common/Constants";
import { DocumentsGridMetrics, KeyCodes } from "../../Common/Constants"; import { DocumentsGridMetrics, KeyCodes } from "../../Common/Constants";
import { createDocument } from "../../Common/dataAccess/createDocument";
import { deleteConflict } from "../../Common/dataAccess/deleteConflict";
import { deleteDocument } from "../../Common/dataAccess/deleteDocument";
import { queryConflicts } from "../../Common/dataAccess/queryConflicts";
import { updateDocument } from "../../Common/dataAccess/updateDocument";
import editable from "../../Common/EditableUtility"; import editable from "../../Common/EditableUtility";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import * as HeadersUtility from "../../Common/HeadersUtility"; import * as HeadersUtility from "../../Common/HeadersUtility";
import { MinimalQueryIterator } from "../../Common/IteratorUtilities"; import { MinimalQueryIterator } from "../../Common/IteratorUtilities";
import { Splitter, SplitterBounds, SplitterDirection } from "../../Common/Splitter"; import { Splitter, SplitterBounds, SplitterDirection } from "../../Common/Splitter";
import { createDocument } from "../../Common/dataAccess/createDocument";
import { deleteConflict } from "../../Common/dataAccess/deleteConflict";
import { deleteDocument } from "../../Common/dataAccess/deleteDocument";
import { queryConflicts } from "../../Common/dataAccess/queryConflicts";
import { updateDocument } from "../../Common/dataAccess/updateDocument";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { Action } from "../../Shared/Telemetry/TelemetryConstants"; import { Action } from "../../Shared/Telemetry/TelemetryConstants";
@ -117,15 +117,15 @@ export default class ConflictsTab extends TabsBase {
this.isEditorDirty = ko.computed<boolean>(() => { this.isEditorDirty = ko.computed<boolean>(() => {
switch (this.editorState()) { switch (this.editorState()) {
case ViewModels.DocumentExplorerState.noDocumentSelected: case ViewModels.DocumentExplorerState.noDocumentSelected:
case ViewModels.DocumentExplorerState.exisitingDocumentNoEdits: case ViewModels.DocumentExplorerState.existingDocumentNoEdits:
return false; return false;
case ViewModels.DocumentExplorerState.newDocumentValid: case ViewModels.DocumentExplorerState.newDocumentValid:
case ViewModels.DocumentExplorerState.newDocumentInvalid: case ViewModels.DocumentExplorerState.newDocumentInvalid:
case ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid: case ViewModels.DocumentExplorerState.existingDocumentDirtyInvalid:
return true; return true;
case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid: case ViewModels.DocumentExplorerState.existingDocumentDirtyValid:
return ( return (
this.selectedConflictCurrent.getEditableOriginalValue() !== this.selectedConflictCurrent.getEditableOriginalValue() !==
this.selectedConflictCurrent.getEditableCurrentValue() this.selectedConflictCurrent.getEditableCurrentValue()
@ -139,8 +139,8 @@ export default class ConflictsTab extends TabsBase {
this.acceptChangesButton = { this.acceptChangesButton = {
enabled: ko.computed<boolean>(() => { enabled: ko.computed<boolean>(() => {
switch (this.editorState()) { switch (this.editorState()) {
case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid: case ViewModels.DocumentExplorerState.existingDocumentDirtyValid:
case ViewModels.DocumentExplorerState.exisitingDocumentNoEdits: case ViewModels.DocumentExplorerState.existingDocumentNoEdits:
return true; return true;
} }
@ -155,8 +155,8 @@ export default class ConflictsTab extends TabsBase {
this.discardButton = { this.discardButton = {
enabled: ko.computed<boolean>(() => { enabled: ko.computed<boolean>(() => {
switch (this.editorState()) { switch (this.editorState()) {
case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid: case ViewModels.DocumentExplorerState.existingDocumentDirtyValid:
case ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid: case ViewModels.DocumentExplorerState.existingDocumentDirtyInvalid:
return true; return true;
} }
@ -171,8 +171,8 @@ export default class ConflictsTab extends TabsBase {
this.deleteButton = { this.deleteButton = {
enabled: ko.computed<boolean>(() => { enabled: ko.computed<boolean>(() => {
switch (this.editorState()) { switch (this.editorState()) {
case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid: case ViewModels.DocumentExplorerState.existingDocumentDirtyValid:
case ViewModels.DocumentExplorerState.exisitingDocumentNoEdits: case ViewModels.DocumentExplorerState.existingDocumentNoEdits:
return true; return true;
} }
@ -247,7 +247,7 @@ export default class ConflictsTab extends TabsBase {
return Q(); return Q();
} }
this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits); this.editorState(ViewModels.DocumentExplorerState.existingDocumentNoEdits);
return Q(); return Q();
} }
@ -407,22 +407,22 @@ export default class ConflictsTab extends TabsBase {
public onDiscardClick = (): Q.Promise<any> => { public onDiscardClick = (): Q.Promise<any> => {
this.selectedConflictContent(this.selectedConflictContent.getEditableOriginalValue()); this.selectedConflictContent(this.selectedConflictContent.getEditableOriginalValue());
this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits); this.editorState(ViewModels.DocumentExplorerState.existingDocumentNoEdits);
return Q(); return Q();
}; };
public onValidDocumentEdit(): Q.Promise<any> { public onValidDocumentEdit(): Q.Promise<any> {
this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid); this.editorState(ViewModels.DocumentExplorerState.existingDocumentDirtyValid);
return Q(); return Q();
} }
public onInvalidDocumentEdit(): Q.Promise<any> { public onInvalidDocumentEdit(): Q.Promise<any> {
if ( if (
this.editorState() === ViewModels.DocumentExplorerState.exisitingDocumentNoEdits || this.editorState() === ViewModels.DocumentExplorerState.existingDocumentNoEdits ||
this.editorState() === ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid this.editorState() === ViewModels.DocumentExplorerState.existingDocumentDirtyValid
) { ) {
this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid); this.editorState(ViewModels.DocumentExplorerState.existingDocumentDirtyInvalid);
return Q(); return Q();
} }
@ -555,7 +555,7 @@ export default class ConflictsTab extends TabsBase {
let parsedConflictContent: any = JSON.parse(documentToInsert); let parsedConflictContent: any = JSON.parse(documentToInsert);
const renderedConflictContent: string = this.renderObjectForEditor(parsedConflictContent, null, 4); const renderedConflictContent: string = this.renderObjectForEditor(parsedConflictContent, null, 4);
this.selectedConflictContent.setBaseline(renderedConflictContent); this.selectedConflictContent.setBaseline(renderedConflictContent);
this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits); this.editorState(ViewModels.DocumentExplorerState.existingDocumentNoEdits);
} }
return Q(); return Q();
@ -576,7 +576,7 @@ export default class ConflictsTab extends TabsBase {
const renderedConflictContent: string = this.renderObjectForEditor(parsedConflictContent, null, 4); const renderedConflictContent: string = this.renderObjectForEditor(parsedConflictContent, null, 4);
this.selectedConflictContent.setBaseline(renderedConflictContent); this.selectedConflictContent.setBaseline(renderedConflictContent);
this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits); this.editorState(ViewModels.DocumentExplorerState.existingDocumentNoEdits);
} }
return Q(); return Q();
@ -588,7 +588,7 @@ export default class ConflictsTab extends TabsBase {
parsedDocumentToDelete = ConflictsTab.removeSystemProperties(parsedDocumentToDelete); parsedDocumentToDelete = ConflictsTab.removeSystemProperties(parsedDocumentToDelete);
const renderedDocumentToDelete: string = this.renderObjectForEditor(parsedDocumentToDelete, null, 4); const renderedDocumentToDelete: string = this.renderObjectForEditor(parsedDocumentToDelete, null, 4);
this.selectedConflictContent.setBaseline(renderedDocumentToDelete); this.selectedConflictContent.setBaseline(renderedDocumentToDelete);
this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits); this.editorState(ViewModels.DocumentExplorerState.existingDocumentNoEdits);
} }
return Q(); return Q();

View File

@ -1,238 +0,0 @@
<div
class="tab-pane active tabdocuments flexContainer"
data-bind="
setTemplateReady: true,
attr:{
id: tabId
},
visible: isActive"
role="tabpanel"
>
<!-- ko if: false -->
<!-- Messagebox Ok Cancel- Start -->
<div class="messagebox-background">
<div class="messagebox">
<h2 class="messagebox-title">Title</h2>
<div class="messagebox-text" tabindex="0">Text</div>
<div class="messagebox-buttons">
<div class="messagebox-buttons-container">
<button value="ok" class="messagebox-button-primary">Ok</button>
<button value="cancel" class="messagebox-button-default">Cancel</button>
</div>
</div>
</div>
</div>
<!-- Messagebox OK Cancel - End -->
<!-- /ko -->
<!-- Filter - Start -->
<div class="filterdivs" data-bind="visible: isFilterCreated">
<!-- Read-only Filter - Start -->
<div class="filterDocCollapsed" data-bind="visible: !isFilterExpanded() && !isPreferredApiMongoDB">
<span class="selectQuery">SELECT * FROM c</span>
<span class="appliedQuery" data-bind="text: appliedFilter"></span>
<button class="filterbtnstyle queryButton" data-bind="click: onShowFilterClick">Edit Filter</button>
</div>
<div
class="filterDocCollapsed"
data-bind="
visible: !isFilterExpanded() && isPreferredApiMongoDB"
>
<span
class="selectQuery"
data-bind="
visible: appliedFilter().length > 0"
>Filter :
</span>
<span
class="noFilterApplied"
data-bind="
visible: !appliedFilter().length > 0"
>No filter applied</span
>
<span class="appliedQuery" data-bind="text: appliedFilter"></span>
<button
class="filterbtnstyle queryButton"
data-bind="
click: onShowFilterClick"
>
Edit Filter
</button>
</div>
<!-- Read-only Filter - End -->
<!-- Editable Filter - start -->
<div
class="filterDocExpanded"
data-bind="
visible: isFilterExpanded"
>
<div>
<div class="editFilterContainer">
<span class="filterspan" data-bind="visible: !isPreferredApiMongoDB"> SELECT * FROM c </span>
<input
type="text"
list="filtersList"
class="querydropdown"
title="Type a query predicate or choose one from the list."
data-bind="
attr:{
placeholder:isPreferredApiMongoDB?'Type a query predicate (e.g., {´a´:´foo´}), or choose one from the drop down list, or leave empty to query all documents.':'Type a query predicate (e.g., WHERE c.id=´1´), or choose one from the drop down list, or leave empty to query all documents.'
},
css: { placeholderVisible: filterContent().length === 0 },
textInput: filterContent"
/>
<datalist
id="filtersList"
data-bind="
foreach: lastFilterContents"
>
<option
data-bind="
value: $data"
></option>
</datalist>
<span class="filterbuttonpad">
<button
class="filterbtnstyle queryButton"
data-bind="
click: refreshDocumentsGrid.bind($data, true),
enable: applyFilterButton.enabled"
aria-label="Apply filter"
tabindex="0"
>
Apply Filter
</button>
</span>
<span class="filterbuttonpad">
<button
class="filterbtnstyle queryButton"
data-bind="
visible: !isPreferredApiMongoDB && isExecuting,
click: onAbortQueryClick"
aria-label="Cancel Query"
tabindex="0"
>
Cancel Query
</button>
</span>
<span
class="filterclose"
role="button"
aria-label="close filter"
tabindex="0"
data-bind="
click: onHideFilterClick, event: { keydown: onCloseButtonKeyDown }"
>
<img src="/close-black.svg" style="height: 14px; width: 14px" alt="Hide filter" />
</span>
</div>
</div>
</div>
<!-- Editable Filter - End -->
</div>
<!-- Filter - End -->
<!-- Ids and Editor - Start -->
<div class="documentsTabGridAndEditor">
<div class="documentsContainerWithSplitter" , data-bind="attr: { id: documentContentsContainerId }">
<div class="flexContainer">
<!-- Document Ids - Start -->
<div
class="documentsGridHeaderContainer tabdocuments scrollable"
data-bind="
attr: {
id: documentContentsGridId,
tabindex: documentIds().length <= 0 ? -1 : 0
},
style: { height: dataContentsGridScrollHeight },
event: { keydown: accessibleDocumentList.onKeyDown }"
>
<table id="tabsTable" class="table table-hover can-select dataTable">
<thead id="theadcontent">
<tr>
<th class="documentsGridHeader" data-bind="text: idHeader" tabindex="0"></th>
<!-- ko if: showPartitionKey -->
<!-- ko foreach: partitionKeyPropertyHeaders -->
<th
class="documentsGridHeader documentsGridPartition evenlySpacedHeader"
data-bind="
attr: {
title: $data
},
text: $data"
tabindex="0"
></th>
<!-- /ko -->
<!-- /ko -->
<th
class="refreshColHeader"
role="button"
aria-label="Refresh documents"
data-bind="event: { keydown: onRefreshButtonKeyDown }"
>
<img
class="refreshcol"
src="/refresh-cosmos.svg"
data-bind="click: refreshDocumentsGrid.bind($data, false)"
alt="Refresh documents"
tabindex="0"
/>
</th>
</tr>
</thead>
<tbody id="tbodycontent">
<!-- ko foreach: documentIds -->
<tr
class="pointer accessibleListElement"
data-bind="
click: $data.click,
css: {
gridRowSelected: $parent.selectedDocumentId && $parent.selectedDocumentId() && $parent.selectedDocumentId().rid === $data.rid,
gridRowHighlighted: $parent.accessibleDocumentList.currentItem() && $parent.accessibleDocumentList.currentItem().rid === $data.rid
}"
tabindex="0"
>
<td class="tabdocumentsGridElement"><a data-bind="text: $data.id, attr: { title: $data.id }"></a></td>
<!-- ko if: $data.partitionKeyProperties -->
<!-- ko foreach: $data.stringPartitionKeyValues -->
<td class="tabdocumentsGridElement" data-bind="colspan: $parent.stringPartitionKeyValues.length + 1">
<a data-bind="text: $data, attr: { title: $data }"></a>
</td>
<!-- /ko -->
<!-- /ko -->
</tr>
<!-- /ko -->
</tbody>
</table>
</div>
<div class="loadMore">
<a
role="button"
data-bind="click: loadNextPage.bind($data, false), event: { keypress: onLoadMoreKeyInput }"
tabindex="0"
>Load more</a
>
</div>
<!-- Document Ids - End -->
<!-- Splitter -->
</div>
<div class="splitter ui-resizable-handle ui-resizable-e colResizePointer" id="h_splitter2"></div>
</div>
<div class="documentWaterMark" data-bind="visible: !shouldShowEditor()">
<p><img src="/DocumentWaterMark.svg" alt="Document WaterMark" /></p>
<p class="documentWaterMarkText">Create new or work with existing document(s).</p>
</div>
<!-- Editor - Start -->
<json-editor
class="editorDivContent"
data-bind="visible: shouldShowEditor, css: { mongoDocumentEditor: isPreferredApiMongoDB }"
params="{content: initialDocumentContent, isReadOnly: false,lineNumbers: 'on',ariaLabel: 'Document editor',
updatedContent: selectedDocumentContent}"
></json-editor>
<!-- Editor - End -->
</div>
<!-- Ids and Editor - End -->
</div>

View File

@ -1,152 +0,0 @@
import * as ko from "knockout";
import { DatabaseAccount } from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import { updateUserContext } from "../../UserContext";
import Explorer from "../Explorer";
import DocumentId from "../Tree/DocumentId";
import DocumentsTab from "./DocumentsTab";
describe("Documents tab", () => {
describe("buildQuery", () => {
it("should generate the right select query for SQL API", () => {
const documentsTab = new DocumentsTab({
partitionKey: null,
documentIds: ko.observableArray<DocumentId>(),
tabKind: ViewModels.CollectionTabKind.Documents,
title: "",
tabPath: "",
});
expect(documentsTab.buildQuery("")).toContain("select");
});
});
describe("showPartitionKey", () => {
const explorer = new Explorer();
const mongoExplorer = new Explorer();
updateUserContext({
databaseAccount: {
properties: {
capabilities: [{ name: "EnableGremlin" }],
},
} as DatabaseAccount,
});
const collectionWithoutPartitionKey = <ViewModels.Collection>(<unknown>{
id: ko.observable<string>("foo"),
database: {
id: ko.observable<string>("foo"),
},
container: explorer,
});
const collectionWithSystemPartitionKey = <ViewModels.Collection>(<unknown>{
id: ko.observable<string>("foo"),
database: {
id: ko.observable<string>("foo"),
},
partitionKey: {
paths: ["/foo"],
kind: "Hash",
version: 2,
systemKey: true,
},
container: explorer,
});
const collectionWithNonSystemPartitionKey = <ViewModels.Collection>(<unknown>{
id: ko.observable<string>("foo"),
database: {
id: ko.observable<string>("foo"),
},
partitionKey: {
paths: ["/foo"],
kind: "Hash",
version: 2,
systemKey: false,
},
container: explorer,
});
const mongoCollectionWithSystemPartitionKey = <ViewModels.Collection>(<unknown>{
id: ko.observable<string>("foo"),
database: {
id: ko.observable<string>("foo"),
},
partitionKey: {
paths: ["/foo"],
kind: "Hash",
version: 2,
systemKey: true,
},
container: mongoExplorer,
});
it("should be false for null or undefined collection", () => {
const documentsTab = new DocumentsTab({
partitionKey: null,
documentIds: ko.observableArray<DocumentId>(),
tabKind: ViewModels.CollectionTabKind.Documents,
title: "",
tabPath: "",
});
expect(documentsTab.showPartitionKey).toBe(false);
});
it("should be false for null or undefined partitionKey", () => {
const documentsTab = new DocumentsTab({
collection: collectionWithoutPartitionKey,
partitionKey: null,
documentIds: ko.observableArray<DocumentId>(),
tabKind: ViewModels.CollectionTabKind.Documents,
title: "",
tabPath: "",
});
expect(documentsTab.showPartitionKey).toBe(false);
});
it("should be true for non-Mongo accounts with system partitionKey", () => {
const documentsTab = new DocumentsTab({
collection: collectionWithSystemPartitionKey,
partitionKey: null,
documentIds: ko.observableArray<DocumentId>(),
tabKind: ViewModels.CollectionTabKind.Documents,
title: "",
tabPath: "",
});
expect(documentsTab.showPartitionKey).toBe(true);
});
it("should be false for Mongo accounts with system partitionKey", () => {
updateUserContext({
apiType: "Mongo",
});
const documentsTab = new DocumentsTab({
collection: mongoCollectionWithSystemPartitionKey,
partitionKey: null,
documentIds: ko.observableArray<DocumentId>(),
tabKind: ViewModels.CollectionTabKind.Documents,
title: "",
tabPath: "",
});
expect(documentsTab.showPartitionKey).toBe(false);
});
it("should be true for non-system partitionKey", () => {
const documentsTab = new DocumentsTab({
collection: collectionWithNonSystemPartitionKey,
partitionKey: null,
documentIds: ko.observableArray<DocumentId>(),
tabKind: ViewModels.CollectionTabKind.Documents,
title: "",
tabPath: "",
});
expect(documentsTab.showPartitionKey).toBe(true);
});
});
});

File diff suppressed because it is too large Load Diff

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