Compare commits

..

11 Commits

Author SHA1 Message Date
Tara Zou
de89fd2011 use unitPriceinBillingCurrency instead 2024-06-05 15:33:46 -04:00
Tara Zou
5ee2af75dd fix format 2024-05-10 15:11:25 -04:00
Tara Zou
586ae376a3 update pricing UPAPI properties 2024-05-10 15:09:12 -04:00
Tara Zou
5de6c46a93 Merge branch 'master' into users/tarazou/UpdateFetchPriceAPI 2024-04-29 16:30:32 -04:00
Tara Zou
d712327653 remove dupicate check 2024-04-29 15:53:26 -04:00
Tara Zou
e2d34d5131 Merge branch 'master' into users/tarazou/UpdateFetchPriceAPI 2024-04-26 16:24:48 -04:00
Tara Zou
d8bff694c6 fix lint 2024-04-26 16:19:35 -04:00
Tara Zou
7fbc33e82b fix compile 2024-04-26 16:12:56 -04:00
Tara Zou
c7c0a3a979 fix format 2024-04-26 15:12:11 -04:00
Tara Zou
6ba2ff6000 update UPAPI calls 2024-04-26 14:15:59 -04:00
Tara Zou
d7718c42da upapi update 2024-04-24 18:51:09 -04:00
171 changed files with 6286 additions and 14430 deletions

View File

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

View File

@@ -104,6 +104,79 @@ 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
env:
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
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
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/sql/resourceToken.spec.ts
- ./test/tables/container.spec.ts
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: 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:
name: Publish Nuget
if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')
@@ -153,70 +226,3 @@ jobs:
name: packages
with:
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

6
.gitignore vendored
View File

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

13
jest-playwright.config.js Normal file
View File

@@ -0,0 +1,13 @@
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/"],
// A list of reporter names that Jest uses when writing coverage reports
coverageReporters: ["json", "text", "cobertura", "lcov"],
coverageReporters: ["json", "text", "cobertura"],
// An object that configures minimum threshold enforcement for coverage results
coverageThreshold: {

View File

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

View File

@@ -2264,33 +2264,33 @@ a:link {
width: 82px;
}
// .tabdocuments .scrollable {
// height: 100%;
// overflow-y: auto;
// overflow-x: hidden;
// padding-left: 5px;
// padding-right: 5px;
// width: 100%;
// }
.tabdocuments .scrollable {
height: 100%;
overflow-y: auto;
overflow-x: hidden;
padding-left: 5px;
padding-right: 5px;
width: 100%;
}
// .tabdocuments > .tabdocumentsGridElement {
// width: 50%;
// }
.tabdocuments > .tabdocumentsGridElement {
width: 50%;
}
// .tabdocuments > .evenlySpacedHeader {
// width: 30%;
// }
.tabdocuments > .evenlySpacedHeader {
width: 30%;
}
// .tabdocuments.scrollable:focus,
// .tabdocuments.scrollable:active {
// outline: 1px dotted;
// }
.tabdocuments.scrollable:focus,
.tabdocuments.scrollable:active {
outline: 1px dotted;
}
// .tabdocuments .scrollable table td {
// white-space: nowrap;
// overflow: hidden;
// text-overflow: ellipsis;
// }
.tabdocuments .scrollable table td {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.mongoDocumentEditor .monaco-editor.vs .redsquiggly {
display: none !important;
@@ -2316,9 +2316,10 @@ td a:hover {
}
.loadMore {
display: block;
width: 100%;
text-align: center;
padding-left: 30%;
padding-top: 2px;
cursor: pointer;
}
.loadMore > a:focus {
@@ -2366,9 +2367,9 @@ a:link {
.tabsManagerContainer {
height: 100%;
flex-grow: 1;
display: flex;
flex-direction: column;
overflow: hidden;
min-height: 300px;
overflow-y: scroll;
}
.tabs {
@@ -2557,12 +2558,10 @@ a:link {
}
.filterdivs {
margin: 10px 0px;
padding-top: 15px;
height: 45px;
margin-bottom: 8px;
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 {
@@ -2579,18 +2578,6 @@ a:link {
cursor: pointer;
}
.documentsTab {
.documentsTable {
.documentsTableCell {
border-left: 1px solid @BaseMedium;
height: 100%;
}
.documentsTableHeader {
border-bottom: 1px solid @BaseMedium;
}
}
}
.querydropdown {
border: 1px solid @BaseMedium;
font-style: normal;
@@ -2671,7 +2658,7 @@ a:link {
width: @ActiveTabWidth;
}
.nav-tabs > li.active > .tabNavContentContainer > .tab_Content > .contentWrapper > .tabNavText {
.nav-tabs > li.active > .tabNavContentContainer > .tab_Content > .tabNavText {
font-weight: bolder;
border-bottom: 2px solid rgba(0, 120, 212, 1);
}
@@ -2707,71 +2694,67 @@ a:link {
width: @TabsWidth;
border-right: @ButtonBorderWidth solid @BaseMedium;
white-space: nowrap;
.contentWrapper {
.flex-display();
width: @ContentWrapper;
.statusIconContainer {
width: @StatusIconContainerSize;
height: @StatusIconContainerSize;
margin-left: @SmallSpace;
display: inline-flex;
.statusIconContainer {
width: @StatusIconContainerSize;
height: @StatusIconContainerSize;
margin-left: @SmallSpace;
display: inline-flex;
.errorIconContainer {
width: @ErrorIconContainer;
height: @ErrorIconContainer;
margin-top: 1px;
.errorIconContainer {
width: @ErrorIconContainer;
height: @ErrorIconContainer;
margin-top: 1px;
.errorIcon {
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;
.errorIcon {
width: @ErrorIconWidth;
height: @LoadingErrorIconSize;
margin: 0px 0px @SmallSpace @SmallSpace;
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;
}
}
.tabNavText {
margin-left: @SmallSpace;
margin-right: 2px;
color: @BaseDark;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
flex-grow: 1;
.errorIconContainer.actionsEnabled {
&:hover {
.hover();
}
&:focus {
.focus();
}
&: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 {
width: 29px;
width: 30px;
position: relative;
padding-top: 2px;

View File

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

2085
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,7 @@
"main": "index.js",
"dependencies": {
"@azure/arm-cosmosdb": "9.1.0",
"@azure/cosmos": "4.0.1-beta.3",
"@azure/cosmos": "4.0.1-beta.2",
"@azure/cosmos-language-service": "0.0.5",
"@azure/identity": "1.5.2",
"@azure/ms-rest-nodeauth": "3.1.1",
@@ -46,7 +46,6 @@
"@types/lodash": "4.14.171",
"@types/mkdirp": "1.0.1",
"@types/node-fetch": "2.5.7",
"@uiw/react-split": "5.9.3",
"@xmldom/xmldom": "0.7.13",
"applicationinsights": "1.8.0",
"bootstrap": "3.4.1",
@@ -99,7 +98,6 @@
"react-splitter-layout": "4.0.0",
"react-string-format": "1.0.1",
"react-youtube": "9.0.1",
"react-window": "1.8.10",
"reflect-metadata": "0.1.13",
"rx-jupyter": "5.5.12",
"sanitize-html": "2.3.3",
@@ -117,7 +115,6 @@
"@babel/preset-env": "7.9.0",
"@babel/preset-react": "7.9.4",
"@babel/preset-typescript": "7.9.0",
"@playwright/test": "1.44.0",
"@testing-library/react": "11.2.3",
"@types/applicationinsights-js": "1.0.7",
"@types/codemirror": "0.0.56",
@@ -126,8 +123,8 @@
"@types/datatables.net": "1.10.28",
"@types/datatables.net-colreorder": "1.4.5",
"@types/dom-to-image": "2.6.2",
"@types/enzyme": "3.10.12",
"@types/enzyme-adapter-react-16": "1.0.9",
"@types/enzyme": "3.10.7",
"@types/enzyme-adapter-react-16": "1.0.6",
"@types/hasher": "0.0.31",
"@types/jest": "26.0.20",
"@types/jquery": "3.5.29",
@@ -139,7 +136,6 @@
"@types/react-notification-system": "0.2.39",
"@types/react-redux": "7.1.7",
"@types/react-splitter-layout": "3.0.1",
"@types/react-window": "1.8.8",
"@types/sanitize-html": "1.27.2",
"@types/sinon": "2.3.3",
"@types/styled-components": "5.1.1",
@@ -155,13 +151,14 @@
"create-file-webpack": "1.0.2",
"css-loader": "6.8.1",
"enzyme": "3.11.0",
"enzyme-adapter-react-16": "1.15.8",
"enzyme-to-json": "3.6.2",
"enzyme-adapter-react-16": "1.15.5",
"enzyme-to-json": "3.6.1",
"eslint": "8.50.0",
"eslint-cli": "1.1.1",
"eslint-plugin-no-null": "1.0.2",
"eslint-plugin-prefer-arrow": "1.2.3",
"eslint-plugin-react-hooks": "4.6.0",
"expect-playwright": "0.3.3",
"fast-glob": "3.2.5",
"fs-extra": "7.0.0",
"html-inline-css-webpack-plugin": "1.11.2",
@@ -170,6 +167,7 @@
"html-webpack-plugin": "5.5.3",
"jest": "26.6.3",
"jest-canvas-mock": "2.3.1",
"jest-playwright-preset": "1.5.1",
"jest-react-hooks-shallow": "1.5.1",
"jest-trx-results-processor": "0.0.7",
"less": "3.8.1",
@@ -178,6 +176,7 @@
"mini-css-extract-plugin": "2.1.0",
"monaco-editor-webpack-plugin": "7.1.0",
"node-fetch": "2.6.7",
"playwright": "1.13.0",
"prettier": "3.0.3",
"process": "0.11.10",
"querystring-es3": "0.2.1",
@@ -209,7 +208,6 @@
"test": "rimraf coverage && jest",
"test:debug": "jest --runInBand",
"test:e2e": "jest -c ./jest.config.playwright.js --detectOpenHandles",
"test:file": "jest --coverage=false",
"watch": "npm run start",
"wait-for-server": "wait-on -t 240000 -i 5000 -v https-get://0.0.0.0:1234/",
"build:ase": "gulp build:ase",

View File

@@ -1,11 +0,0 @@
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;
}

View File

@@ -1,53 +0,0 @@
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,12 +88,6 @@ export class CapabilityNames {
public static readonly EnableStorageAnalytics: string = "EnableStorageAnalytics";
public static readonly EnableMongo: string = "EnableMongo";
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

View File

@@ -1,6 +1,6 @@
import * as Cosmos from "@azure/cosmos";
import { getAuthorizationTokenUsingResourceTokens } from "Common/getAuthorizationTokenUsingResourceTokens";
import { AuthorizationToken } from "Contracts/FabricMessageTypes";
import { AuthorizationToken } from "Contracts/MessageTypes";
import { checkDatabaseResourceTokensValidity } from "Platform/Fabric/FabricUtil";
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
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 **************
// User master tokens
const authorizationToken = await sendCachedDataMessage<AuthorizationToken>(
FabricMessageTypes.GetAuthorizationToken,
MessageTypes.GetAuthorizationToken,
[requestInfo],
userContext.fabricContext.connectionId,
);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,3 @@
import { BulkOperationType, OperationInput } from "@azure/cosmos";
import { CollectionBase } from "../../Contracts/ViewModels";
import DocumentId from "../../Explorer/Tree/DocumentId";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
@@ -25,58 +24,3 @@ export const deleteDocument = async (collection: CollectionBase, documentId: Doc
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,5 +1,4 @@
import { FeedOptions, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
import { isVectorSearchEnabled } from "Utils/CapabilityUtils";
import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility";
import { Queries } from "../Constants";
import { client } from "../CosmosClient";
@@ -27,6 +26,5 @@ export const getCommonQueryOptions = (options: FeedOptions): FeedOptions => {
(storedItemPerPageSetting !== undefined && storedItemPerPageSetting) ||
Queries.itemsPerPage;
options.maxDegreeOfParallelism = LocalStorageUtility.getEntryNumber(StorageKey.MaxDegreeOfParellism);
options.disableNonStreamingOrderByQuery = !isVectorSearchEnabled();
return options;
};

View File

@@ -42,7 +42,6 @@ export interface ConfigContext {
ARM_API_VERSION: string;
GRAPH_ENDPOINT: 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;
@@ -199,9 +198,6 @@ if (process.env.NODE_ENV === "development") {
updateConfigContext({
PROXY_PATH: "/proxy",
EMULATOR_ENDPOINT: "https://localhost:8081",
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Mpac,
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Mpac,
CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Mpac,
});
}

View File

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

View File

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

View File

@@ -1,13 +0,0 @@
/**
* 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 "Contracts/FabricMessageTypes";
import { AuthorizationToken } from "./MessageTypes";
// This is the version of these messages
export const FABRIC_RPC_VERSION = "2";

View File

@@ -1,13 +1,12 @@
/**
* Messaging types used with Data Explorer <-> Portal communication,
* Hosted <-> Explorer communication
* Hosted <-> Explorer communication and Data Explorer -> Fabric communication.
*
* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
* 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.
*
*/
export enum MessageTypes {
TelemetryInfo,
@@ -44,9 +43,14 @@ export enum MessageTypes {
DisplayNPSSurvey,
OpenVCoreMongoNetworkingBlade,
OpenVCoreMongoConnectionStringsBlade,
GetAuthorizationToken, // unused. Can be removed if the portal uses the same list of enums.
GetAllResourceTokens, // unused. Can be removed if the portal uses the same list of enums.
Ready, // unused. Can be removed if the portal uses the same list of enums.
GetAuthorizationToken, // Data Explorer -> Fabric
GetAllResourceTokens, // Data Explorer -> Fabric
Ready, // Data Explorer -> Fabric
OpenCESCVAFeedbackBlade,
ActivateTab,
}
export interface AuthorizationToken {
XDate: string;
PrimaryReadWriteToken: string;
}

View File

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

View File

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

View File

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

View File

@@ -31,7 +31,7 @@ export interface CommandButtonComponentProps {
/**
* Click handler for command button click
*/
onCommandClick?: (e: React.SyntheticEvent | KeyboardEvent) => void;
onCommandClick: (e: React.SyntheticEvent | KeyboardEvent) => void;
/**
* Label for the button

View File

@@ -20,10 +20,7 @@ export interface EditorReactProps {
lineDecorationsWidth?: monaco.editor.IEditorOptions["lineDecorationsWidth"];
minimap?: monaco.editor.IEditorOptions["minimap"];
scrollBeyondLastLine?: monaco.editor.IEditorOptions["scrollBeyondLastLine"];
fontSize?: monaco.editor.IEditorOptions["fontSize"];
monacoContainerStyles?: React.CSSProperties;
className?: string;
spinnerClassName?: string;
}
export class EditorReact extends React.Component<EditorReactProps, EditorReactStates> {
@@ -78,11 +75,9 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
public render(): JSX.Element {
return (
<React.Fragment>
{!this.state.showEditor && (
<Spinner size={SpinnerSize.large} className={this.props.spinnerClassName || "spinner"} />
)}
{!this.state.showEditor && <Spinner size={SpinnerSize.large} className="spinner" />}
<div
className={this.props.className || "jsonEditor"}
className="jsonEditor"
style={this.props.monacoContainerStyles}
ref={(elt: HTMLElement) => this.setRef(elt)}
/>
@@ -124,7 +119,7 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
value: this.props.content,
readOnly: this.props.isReadOnly,
ariaLabel: this.props.ariaLabel,
fontSize: this.props.fontSize || 12,
fontSize: 12,
automaticLayout: true,
theme: this.props.theme,
wordWrap: this.props.wordWrap || "off",
@@ -137,13 +132,7 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
this.rootNode.innerHTML = "";
const monaco = await loadMonaco();
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;
}
createCallback(monaco?.editor?.create(this.rootNode, options));
if (this.rootNode.innerHTML) {
this.setState({

View File

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

View File

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

View File

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

View File

@@ -1,30 +0,0 @@
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,7 +16,6 @@ export interface IndexingPolicyComponentProps {
logIndexingPolicySuccessMessage: () => void;
indexTransformationProgress: number;
refreshIndexTransformationProgress: () => Promise<void>;
isVectorSearchEnabled?: boolean;
onIndexingPolicyDirtyChange: (isIndexingPolicyDirty: boolean) => void;
}
@@ -120,15 +119,10 @@ export class IndexingPolicyComponent extends React.Component<
indexTransformationProgress={this.props.indexTransformationProgress}
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) && (
<MessageBar messageBarType={MessageBarType.warning}>{unsavedEditorWarningMessage("indexPolicy")}</MessageBar>
)}
<div className="settingsV2Editor" tabIndex={0} ref={this.indexingPolicyDiv}></div>
<div className="settingsV2IndexingPolicyEditor" tabIndex={0} ref={this.indexingPolicyDiv}></div>
</Stack>
);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,194 +0,0 @@
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

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

View File

@@ -0,0 +1,147 @@
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,6 +38,7 @@ import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils";
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils";
import { update } from "../Utils/arm/generatedClients/cosmos/databaseAccounts";
import { listByDatabaseAccount } from "../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces";
import { useSidePanel } from "../hooks/useSidePanel";
import { useTabs } from "../hooks/useTabs";
import "./ComponentRegisterer";
@@ -55,6 +56,7 @@ import { AddCollectionPanel } from "./Panes/AddCollectionPanel";
import { CassandraAddCollectionPane } from "./Panes/CassandraAddCollectionPane/CassandraAddCollectionPane";
import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane/ExecuteSprocParamsPane";
import { StringInputPane } from "./Panes/StringInputPane/StringInputPane";
import { UploadFilePane } from "./Panes/UploadFilePane/UploadFilePane";
import { UploadItemsPane } from "./Panes/UploadItemsPane/UploadItemsPane";
import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient";
import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab";
@@ -136,6 +138,14 @@ export default class Explorer {
this.isTabsContentExpanded = ko.observable(false);
document.addEventListener(
"contextmenu",
(e) => {
e.preventDefault();
},
false,
);
$(() => {
$(document.body).click(() => $(".commandDropdownContainer").hide());
});
@@ -500,6 +510,104 @@ export default class Explorer {
.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(
updatedDatabaseList: DataModels.Database[],
databases: ViewModels.Database[],
@@ -902,6 +1010,92 @@ 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.
public async refreshContentItem(item: NotebookContentItem): Promise<void> {
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
@@ -936,6 +1130,10 @@ export default class Explorer {
let title: string;
switch (kind) {
case ViewModels.TerminalKind.Default:
title = "Terminal";
break;
case ViewModels.TerminalKind.Mongo:
title = "Mongo Shell";
break;
@@ -1089,6 +1287,36 @@ export default class Explorer {
.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 {
if (useNotebook.getState().isPhoenixNotebooks) {
return (
@@ -1157,25 +1385,21 @@ export default class Explorer {
}
public async refreshSampleData(): Promise<void> {
try {
if (!userContext.sampleDataConnectionInfo || useDatabases.getState().sampleDataResourceTokenCollection) {
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");
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 });
}
}

View File

@@ -60,23 +60,21 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol
imageProps: btn.iconSrc ? { src: btn.iconSrc, alt: btn.iconAlt } : undefined,
iconName: btn.iconName,
},
onClick: btn.onCommandClick
? (ev?: React.MouseEvent<HTMLElement, MouseEvent> | React.KeyboardEvent<HTMLElement>) => {
btn.onCommandClick(ev);
let copilotEnabled = false;
if (useQueryCopilot.getState().copilotEnabled && useQueryCopilot.getState().copilotUserDBEnabled) {
copilotEnabled = useQueryCopilot.getState().copilotEnabledforExecution;
}
TelemetryProcessor.trace(Action.ClickCommandBarButton, ActionModifiers.Mark, { label, copilotEnabled });
}
: undefined,
onClick: (ev?: React.MouseEvent<HTMLElement, MouseEvent> | React.KeyboardEvent<HTMLElement>) => {
btn.onCommandClick(ev);
let copilotEnabled = false;
if (useQueryCopilot.getState().copilotEnabled && useQueryCopilot.getState().copilotUserDBEnabled) {
copilotEnabled = useQueryCopilot.getState().copilotEnabledforExecution;
}
TelemetryProcessor.trace(Action.ClickCommandBarButton, ActionModifiers.Mark, { label, copilotEnabled });
},
key: `${btn.commandButtonLabel}${index}`,
text: label,
"data-test": label,
title: btn.tooltipText,
name: label,
disabled: btn.disabled,
ariaLabel: btn.ariaLabel,
"data-test": `CommandBar/Button:${label}`,
buttonStyles: {
root: {
backgroundColor: backgroundColor,

View File

@@ -57,10 +57,6 @@ export class NotificationConsoleComponent extends React.Component<
this.prevHeaderStatus = undefined;
}
public componentDidMount() {
this.componentDidUpdate(this.props, this.state);
}
public componentDidUpdate(
prevProps: NotificationConsoleComponentProps,
prevState: NotificationConsoleComponentState,
@@ -269,29 +265,20 @@ export class NotificationConsoleComponent extends React.Component<
};
private updateConsoleData = (prevProps: NotificationConsoleComponentProps): void => {
let updatedConsoleData: ConsoleData[] = [...this.state.allConsoleData];
let refresh = false;
if (!this.areConsoleDataEqual(this.props.consoleData, prevProps.consoleData)) {
updatedConsoleData = [this.props.consoleData, ...updatedConsoleData];
refresh = true;
this.setState({ allConsoleData: [this.props.consoleData, ...this.state.allConsoleData] });
}
if (this.props.inProgressConsoleDataIdToBeDeleted) {
const hasMatchingItem = updatedConsoleData.some(
if (
this.props.inProgressConsoleDataIdToBeDeleted &&
prevProps.inProgressConsoleDataIdToBeDeleted !== this.props.inProgressConsoleDataIdToBeDeleted
) {
const allConsoleData = this.state.allConsoleData.filter(
(data: ConsoleData) =>
data.type === ConsoleDataType.InProgress && data.id === this.props.inProgressConsoleDataIdToBeDeleted,
!(data.type === ConsoleDataType.InProgress && data.id === this.props.inProgressConsoleDataIdToBeDeleted),
);
if (hasMatchingItem) {
updatedConsoleData = updatedConsoleData.filter(
(data: ConsoleData) =>
!(data.type === ConsoleDataType.InProgress && data.id === this.props.inProgressConsoleDataIdToBeDeleted),
);
refresh = true;
}
this.setState({ allConsoleData });
}
refresh && this.setState({ allConsoleData: updatedConsoleData });
};
private areConsoleDataEqual = (currentData: ConsoleData, prevData: ConsoleData): boolean => {

View File

@@ -21,7 +21,6 @@ import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
import { configContext, Platform } from "ConfigContext";
import * as DataModels from "Contracts/DataModels";
import { SubscriptionType } from "Contracts/SubscriptionType";
import { AddVectorEmbeddingPolicyForm } from "Explorer/Panes/VectorSearchPanel/AddVectorEmbeddingPolicyForm";
import { useSidePanel } from "hooks/useSidePanel";
import { useTeachingBubble } from "hooks/useTeachingBubble";
import React from "react";
@@ -30,7 +29,7 @@ import { Action } from "Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
import { userContext } from "UserContext";
import { getCollectionName } from "Utils/APITypeUtils";
import { isCapabilityEnabled, isServerlessAccount, isVectorSearchEnabled } from "Utils/CapabilityUtils";
import { isCapabilityEnabled, isServerlessAccount } from "Utils/CapabilityUtils";
import { getUpsellMessage } from "Utils/PricingUtils";
import { CollapsibleSectionComponent } from "../Controls/CollapsiblePanel/CollapsibleSectionComponent";
import { ThroughputInput } from "../Controls/ThroughputInput/ThroughputInput";
@@ -82,10 +81,6 @@ export const AllPropertiesIndexed: DataModels.IndexingPolicy = {
excludedPaths: [],
};
export const DefaultVectorEmbeddingPolicy: DataModels.VectorEmbeddingPolicy = {
vectorEmbeddings: [],
};
export interface AddCollectionPanelState {
createNewDatabase: boolean;
newDatabaseId: string;
@@ -106,9 +101,6 @@ export interface AddCollectionPanelState {
isExecuting: boolean;
isThroughputCapExceeded: boolean;
teachingBubbleStep: number;
vectorIndexingPolicy: DataModels.VectorIndex[];
vectorEmbeddingPolicy: DataModels.VectorEmbedding[];
vectorPolicyValidated: boolean;
}
export class AddCollectionPanel extends React.Component<AddCollectionPanelProps, AddCollectionPanelState> {
@@ -144,9 +136,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
isExecuting: false,
isThroughputCapExceeded: false,
teachingBubbleStep: 0,
vectorEmbeddingPolicy: [],
vectorIndexingPolicy: [],
vectorPolicyValidated: true,
};
}
@@ -156,17 +145,11 @@ 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 {
const isFirstResourceCreated = useDatabases.getState().isFirstResourceCreated();
return (
<form className="panelFormWrapper" onSubmit={this.submit.bind(this)} id="panelContainer">
<form className="panelFormWrapper" onSubmit={this.submit.bind(this)}>
{this.state.errorMessage && (
<PanelInfoErrorComponent
message={this.state.errorMessage}
@@ -399,7 +382,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
)}
{!this.state.createNewDatabase && (
<Dropdown
ariaLabel="Choose an existing database"
styles={{ title: { height: 27, lineHeight: 27 }, dropdownItem: { fontSize: 12 } }}
style={{ width: 300, fontSize: 12 }}
placeholder="Choose an existing database"
@@ -818,7 +800,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
iconName="Info"
className="panelInfoIcon"
tabIndex={0}
ariaLabel="Enable analytical store capability to perform near real-time analytics on your operational data, without impacting the performance of transactional workloads."
ariaLabel="Enable analytical store capability to perform near real-time analytics on your operational data, without
impacting the performance of transactional workloads."
/>
</TooltipHost>
</Stack>
@@ -880,44 +863,17 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
)}
</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" && (
<CollapsibleSectionComponent
title="Advanced"
isExpandedByDefault={false}
onExpand={() => {
TelemetryProcessor.traceOpen(Action.ExpandAddCollectionPaneAdvancedSection);
this.scrollToSection("collapsibleAdvancedSectionContent");
this.scrollToAdvancedSection();
}}
>
<Stack className="panelGroupSpacing" id="collapsibleAdvancedSectionContent">
<Stack className="panelGroupSpacing" id="collapsibleSectionContent">
{isCapabilityEnabled("EnableMongo") && !isCapabilityEnabled("EnableMongo16MBDocumentSupport") && (
<Stack className="panelGroupSpacing">
<Stack horizontal>
@@ -968,9 +924,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
}
/>
<Text variant="small">
<Icon iconName="InfoSolid" className="removeIcon" /> 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.{" "}
<Icon iconName="InfoSolid" className="removeIcon" 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.{" "}
<Link href="https://aka.ms/cosmos-large-pk" target="_blank">
Learn more
</Link>
@@ -1113,18 +1070,6 @@ 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 {
if (!this.state.selectedDatabaseId) {
return false;
@@ -1205,18 +1150,6 @@ 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 {
if (isServerlessAccount()) {
return false;
@@ -1276,10 +1209,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
);
}
private shouldShowVectorSearchParameters() {
return isVectorSearchEnabled() && (isServerlessAccount() || this.shouldShowCollectionThroughputInput());
}
private parseUniqueKeys(): DataModels.UniqueKeyPolicy {
if (this.state.uniqueKeys?.length === 0) {
return undefined;
@@ -1336,11 +1265,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
return false;
}
if (this.shouldShowVectorSearchParameters() && !this.state.vectorPolicyValidated) {
this.setState({ errorMessage: "Please fix errors in container vector policy" });
return false;
}
return true;
}
@@ -1363,8 +1287,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
return Constants.AnalyticalStorageTtl.Disabled;
}
private scrollToSection(id: string): void {
document.getElementById(id)?.scrollIntoView();
private scrollToAdvancedSection(): void {
document.getElementById("collapsibleSectionContent")?.scrollIntoView();
}
private getSampleDBName(): string {
@@ -1420,15 +1344,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
? AllPropertiesIndexed
: SharedDatabaseDefault;
let vectorEmbeddingPolicy: DataModels.VectorEmbeddingPolicy;
if (this.shouldShowVectorSearchParameters()) {
indexingPolicy.vectorIndexes = this.state.vectorIndexingPolicy;
vectorEmbeddingPolicy = {
vectorEmbeddings: this.state.vectorEmbeddingPolicy,
};
}
const telemetryData = {
database: {
id: databaseId,
@@ -1487,7 +1402,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
partitionKey,
uniqueKeyPolicy,
createMongoWildcardIndex: this.state.createMongoWildCardIndex,
vectorEmbeddingPolicy,
};
this.setState({ isExecuting: true });

View File

@@ -275,7 +275,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
<span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small">
Enter CQL command to create the table.{" "}
<Link className="underlinedLink" href="https://aka.ms/cassandra-create-table" target="_blank">
<Link href="https://aka.ms/cassandra-create-table" target="_blank">
Learn More
</Link>
</Text>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,15 +9,12 @@ import {
Toggle,
} from "@fluentui/react";
import * as Constants from "Common/Constants";
import { SplitterDirection } from "Common/Splitter";
import { InfoTooltip } from "Common/Tooltip/InfoTooltip";
import { configContext } from "ConfigContext";
import { useDatabases } from "Explorer/useDatabases";
import {
DefaultRUThreshold,
LocalStorageUtility,
StorageKey,
getDefaultQueryResultsView,
getRUThreshold,
ruThresholdEnabled as isRUThresholdEnabled,
} from "Shared/StorageUtility";
@@ -50,9 +47,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
LocalStorageUtility.getEntryBoolean(StorageKey.QueryTimeoutEnabled),
);
const [queryTimeout, setQueryTimeout] = useState<number>(LocalStorageUtility.getEntryNumber(StorageKey.QueryTimeout));
const [defaultQueryResultsView, setDefaultQueryResultsView] = useState<SplitterDirection>(
getDefaultQueryResultsView(),
);
const [automaticallyCancelQueryAfterTimeout, setAutomaticallyCancelQueryAfterTimeout] = useState<boolean>(
LocalStorageUtility.getEntryBoolean(StorageKey.AutomaticallyCancelQueryAfterTimeout),
);
@@ -108,10 +102,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
const shouldShowCrossPartitionOption = userContext.apiType !== "Gremlin";
const shouldShowParallelismOption = userContext.apiType !== "Gremlin";
const shouldShowPriorityLevelOption = PriorityBasedExecutionUtils.isFeatureEnabled();
const shouldShowCopilotSampleDBOption =
userContext.apiType === "SQL" &&
useQueryCopilot.getState().copilotEnabled &&
useDatabases.getState().sampleDataResourceTokenCollection;
const shouldShowCopilotSampleDBOption = userContext.apiType === "SQL" && useQueryCopilot.getState().copilotEnabled;
const handlerOnSubmit = async () => {
setIsExecuting(true);
@@ -130,7 +121,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
LocalStorageUtility.setEntryNumber(StorageKey.MaxDegreeOfParellism, maxDegreeOfParallelism);
LocalStorageUtility.setEntryString(StorageKey.PriorityLevel, priorityLevel.toString());
LocalStorageUtility.setEntryString(StorageKey.CopilotSampleDBEnabled, copilotSampleDBEnabled.toString());
LocalStorageUtility.setEntryString(StorageKey.DefaultQueryResultsView, defaultQueryResultsView);
if (shouldShowGraphAutoVizOption) {
LocalStorageUtility.setEntryBoolean(
@@ -207,11 +197,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
{ key: Constants.PriorityLevel.High, text: "High" },
];
const defaultQueryResultsViewOptionList: IChoiceGroupOption[] = [
{ key: SplitterDirection.Vertical, text: "Vertical" },
{ key: SplitterDirection.Horizontal, text: "Horizontal" },
];
const handleOnPriorityLevelOptionChange = (
ev: React.FormEvent<HTMLInputElement>,
option: IChoiceGroupOption,
@@ -249,13 +234,6 @@ 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 retryAttempts = Number(newValue);
if (!isNaN(retryAttempts)) {
@@ -460,25 +438,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
)}
</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">

View File

@@ -205,67 +205,6 @@ exports[`Settings Pane should render Default properly 1`] = `
</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
className="settingsSection"
>

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,91 @@
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

@@ -1,84 +0,0 @@
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

@@ -1,300 +0,0 @@
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

@@ -1,16 +0,0 @@
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,7 +3,6 @@
exports[`AddCollectionPanel should render Default properly 1`] = `
<form
className="panelFormWrapper"
id="panelContainer"
onSubmit={[Function]}
>
<div
@@ -434,7 +433,7 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
>
<Stack
className="panelGroupSpacing"
id="collapsibleAdvancedSectionContent"
id="collapsibleSectionContent"
>
<Stack
className="panelGroupSpacing"
@@ -467,6 +466,7 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
<Icon
className="removeIcon"
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.
@@ -486,4 +486,4 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
isButtonDisabled={false}
/>
</form>
`;
`;

View File

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

View File

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

View File

@@ -504,12 +504,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
{!showFeedbackBar && (
<Text style={{ fontSize: 12 }}>
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" }}
className="underlinedLink"
>
<Link href="https://aka.ms/cdb-copilot-preview-terms" target="_blank" style={{ color: "#0072D4" }}>
Read preview terms
</Link>
{showErrorMessageBar && (

View File

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

View File

@@ -25,6 +25,7 @@ import * as React from "react";
import ConnectIcon from "../../../images/Connect_color.svg";
import ContainersIcon from "../../../images/Containers.svg";
import LinkIcon from "../../../images/Link_blue.svg";
import NotebookColorIcon from "../../../images/Notebooks.svg";
import PowerShellIcon from "../../../images/PowerShell.svg";
import CopilotIcon from "../../../images/QueryCopilotNewLogo.svg";
import QuickStartIcon from "../../../images/Quickstart_Lightning.svg";
@@ -355,15 +356,15 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
) : (
<div className="moreStuffContainer">
<div className="moreStuffColumn commonTasks">
<h2 className="title">Recents</h2>
<div className="title">Recents</div>
{this.getRecentItems()}
</div>
<div className="moreStuffColumn">
<h2 className="title">Top 3 things you need to know</h2>
<div className="title">Top 3 things you need to know</div>
{this.top3Items()}
</div>
<div className="moreStuffColumn tipsContainer">
<h2 className="title">Learning Resources</h2>
<div className="title">Learning Resources</div>
{this.getLearningResourceItems()}
</div>
</div>
@@ -409,6 +410,14 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
},
};
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());
@@ -680,20 +689,11 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
title: "Learn the Fundamentals",
description: "Watch Azure Cosmos DB Live TV show introductory and how to videos.",
};
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[];
let items: item[];
switch (userContext.apiType) {
case "SQL":
case "Postgres":
apiItems = [
items = [
{
link: "https://aka.ms/msl-sdk-connect",
title: "Get Started using an SDK",
@@ -708,7 +708,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
];
break;
case "Mongo":
apiItems = [
items = [
{
link: "https://aka.ms/mongonodejs",
title: "Build an app with Node.js",
@@ -723,7 +723,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
];
break;
case "Cassandra":
apiItems = [
items = [
{
link: "https://aka.ms/cassandracontainer",
title: "Create a Container",
@@ -738,7 +738,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
];
break;
case "Gremlin":
apiItems = [
items = [
{
link: "https://aka.ms/graphquickstart",
title: "Get Started ",
@@ -753,7 +753,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
];
break;
case "Tables":
apiItems = [
items = [
{
link: "https://aka.ms/tabledotnet",
title: "Build a .NET App",
@@ -770,9 +770,6 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
default:
break;
}
const items = [...commonItems, ...apiItems];
return (
<Stack>
{items.map((item, i) => (

View File

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

View File

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

View File

@@ -1,8 +1,8 @@
import Q from "q";
import * as _ from "underscore";
import * as Constants from "./Constants";
import Q from "q";
import * as Entities from "./Entities";
import { CassandraTableKey } from "./TableDataClient";
import * as Constants from "./Constants";
/**
* Generates a pseudo-random GUID.
@@ -180,6 +180,30 @@ export function onEsc(
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
export var MAX_SAFE_INTEGER = Math.pow(2, 53) - 1;
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 * as Constants from "../../Common/Constants";
import { DocumentsGridMetrics, KeyCodes } from "../../Common/Constants";
import editable from "../../Common/EditableUtility";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import * as HeadersUtility from "../../Common/HeadersUtility";
import { MinimalQueryIterator } from "../../Common/IteratorUtilities";
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 editable from "../../Common/EditableUtility";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import * as HeadersUtility from "../../Common/HeadersUtility";
import { MinimalQueryIterator } from "../../Common/IteratorUtilities";
import { Splitter, SplitterBounds, SplitterDirection } from "../../Common/Splitter";
import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
@@ -117,15 +117,15 @@ export default class ConflictsTab extends TabsBase {
this.isEditorDirty = ko.computed<boolean>(() => {
switch (this.editorState()) {
case ViewModels.DocumentExplorerState.noDocumentSelected:
case ViewModels.DocumentExplorerState.existingDocumentNoEdits:
case ViewModels.DocumentExplorerState.exisitingDocumentNoEdits:
return false;
case ViewModels.DocumentExplorerState.newDocumentValid:
case ViewModels.DocumentExplorerState.newDocumentInvalid:
case ViewModels.DocumentExplorerState.existingDocumentDirtyInvalid:
case ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid:
return true;
case ViewModels.DocumentExplorerState.existingDocumentDirtyValid:
case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid:
return (
this.selectedConflictCurrent.getEditableOriginalValue() !==
this.selectedConflictCurrent.getEditableCurrentValue()
@@ -139,8 +139,8 @@ export default class ConflictsTab extends TabsBase {
this.acceptChangesButton = {
enabled: ko.computed<boolean>(() => {
switch (this.editorState()) {
case ViewModels.DocumentExplorerState.existingDocumentDirtyValid:
case ViewModels.DocumentExplorerState.existingDocumentNoEdits:
case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid:
case ViewModels.DocumentExplorerState.exisitingDocumentNoEdits:
return true;
}
@@ -155,8 +155,8 @@ export default class ConflictsTab extends TabsBase {
this.discardButton = {
enabled: ko.computed<boolean>(() => {
switch (this.editorState()) {
case ViewModels.DocumentExplorerState.existingDocumentDirtyValid:
case ViewModels.DocumentExplorerState.existingDocumentDirtyInvalid:
case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid:
case ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid:
return true;
}
@@ -171,8 +171,8 @@ export default class ConflictsTab extends TabsBase {
this.deleteButton = {
enabled: ko.computed<boolean>(() => {
switch (this.editorState()) {
case ViewModels.DocumentExplorerState.existingDocumentDirtyValid:
case ViewModels.DocumentExplorerState.existingDocumentNoEdits:
case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid:
case ViewModels.DocumentExplorerState.exisitingDocumentNoEdits:
return true;
}
@@ -247,7 +247,7 @@ export default class ConflictsTab extends TabsBase {
return Q();
}
this.editorState(ViewModels.DocumentExplorerState.existingDocumentNoEdits);
this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits);
return Q();
}
@@ -407,22 +407,22 @@ export default class ConflictsTab extends TabsBase {
public onDiscardClick = (): Q.Promise<any> => {
this.selectedConflictContent(this.selectedConflictContent.getEditableOriginalValue());
this.editorState(ViewModels.DocumentExplorerState.existingDocumentNoEdits);
this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits);
return Q();
};
public onValidDocumentEdit(): Q.Promise<any> {
this.editorState(ViewModels.DocumentExplorerState.existingDocumentDirtyValid);
this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid);
return Q();
}
public onInvalidDocumentEdit(): Q.Promise<any> {
if (
this.editorState() === ViewModels.DocumentExplorerState.existingDocumentNoEdits ||
this.editorState() === ViewModels.DocumentExplorerState.existingDocumentDirtyValid
this.editorState() === ViewModels.DocumentExplorerState.exisitingDocumentNoEdits ||
this.editorState() === ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid
) {
this.editorState(ViewModels.DocumentExplorerState.existingDocumentDirtyInvalid);
this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid);
return Q();
}
@@ -555,7 +555,7 @@ export default class ConflictsTab extends TabsBase {
let parsedConflictContent: any = JSON.parse(documentToInsert);
const renderedConflictContent: string = this.renderObjectForEditor(parsedConflictContent, null, 4);
this.selectedConflictContent.setBaseline(renderedConflictContent);
this.editorState(ViewModels.DocumentExplorerState.existingDocumentNoEdits);
this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits);
}
return Q();
@@ -576,7 +576,7 @@ export default class ConflictsTab extends TabsBase {
const renderedConflictContent: string = this.renderObjectForEditor(parsedConflictContent, null, 4);
this.selectedConflictContent.setBaseline(renderedConflictContent);
this.editorState(ViewModels.DocumentExplorerState.existingDocumentNoEdits);
this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits);
}
return Q();
@@ -588,7 +588,7 @@ export default class ConflictsTab extends TabsBase {
parsedDocumentToDelete = ConflictsTab.removeSystemProperties(parsedDocumentToDelete);
const renderedDocumentToDelete: string = this.renderObjectForEditor(parsedDocumentToDelete, null, 4);
this.selectedConflictContent.setBaseline(renderedDocumentToDelete);
this.editorState(ViewModels.DocumentExplorerState.existingDocumentNoEdits);
this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits);
}
return Q();

View File

@@ -0,0 +1,238 @@
<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

@@ -0,0 +1,152 @@
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

View File

@@ -1,476 +0,0 @@
import { FeedResponse, ItemDefinition, Resource } from "@azure/cosmos";
import { deleteDocuments } from "Common/dataAccess/deleteDocument";
import { Platform, updateConfigContext } from "ConfigContext";
import { EditorReactProps } from "Explorer/Controls/Editor/EditorReact";
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
import {
ButtonsDependencies,
DELETE_BUTTON_ID,
DISCARD_BUTTON_ID,
DocumentsTabComponent,
IDocumentsTabComponentProps,
NEW_DOCUMENT_BUTTON_ID,
SAVE_BUTTON_ID,
UPDATE_BUTTON_ID,
UPLOAD_BUTTON_ID,
buildQuery,
getDiscardExistingDocumentChangesButtonState,
getDiscardNewDocumentChangesButtonState,
getSaveExistingDocumentButtonState,
getSaveNewDocumentButtonState,
getTabsButtons,
showPartitionKey,
} from "Explorer/Tabs/DocumentsTabV2/DocumentsTabV2";
import { ReactWrapper, ShallowWrapper, mount, shallow } from "enzyme";
import * as ko from "knockout";
import React from "react";
import { act } from "react-dom/test-utils";
import { DatabaseAccount, DocumentId } from "../../../Contracts/DataModels";
import * as ViewModels from "../../../Contracts/ViewModels";
import { updateUserContext } from "../../../UserContext";
import Explorer from "../../Explorer";
jest.mock("Common/dataAccess/queryDocuments", () => ({
queryDocuments: jest.fn(() => ({
// Omit headers, because we can't mock a private field and we don't need to test it
fetchNext: (): Promise<Omit<FeedResponse<ItemDefinition & Resource>, "headers">> =>
Promise.resolve({
resources: [{ id: "id", _rid: "rid", _self: "self", _etag: "etag", _ts: 123 }],
hasMoreResults: false,
diagnostics: undefined,
continuation: undefined,
continuationToken: undefined,
queryMetrics: "queryMetrics",
requestCharge: 1,
activityId: "activityId",
indexMetrics: "indexMetrics",
}),
})),
}));
const PROPERTY_VALUE = "__SOME_PROPERTY_VALUE__";
jest.mock("Common/dataAccess/readDocument", () => ({
readDocument: jest.fn(() =>
Promise.resolve({
container: undefined,
id: "id",
property: PROPERTY_VALUE,
}),
),
}));
jest.mock("Explorer/Controls/Editor/EditorReact", () => ({
EditorReact: (props: EditorReactProps) => <>{props.content}</>,
}));
jest.mock("Explorer/Controls/Dialog", () => ({
useDialog: {
getState: jest.fn(() => ({
showOkCancelModalDialog: (title: string, subText: string, okLabel: string, onOk: () => void) => onOk(),
showOkModalDialog: () => {},
})),
},
}));
jest.mock("Common/dataAccess/deleteDocument", () => ({
deleteDocuments: jest.fn((collection: ViewModels.CollectionBase, documentIds: DocumentId[]) =>
Promise.resolve(documentIds),
),
}));
async function waitForComponentToPaint<P = unknown>(wrapper: ReactWrapper<P> | ShallowWrapper<P>, amount = 0) {
let newWrapper;
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, amount));
newWrapper = wrapper.update();
});
return newWrapper;
}
describe("Documents tab (noSql API)", () => {
describe("buildQuery", () => {
it("should generate the right select query for SQL API", () => {
expect(buildQuery(false, "")).toContain("select");
});
});
describe("showPartitionKey", () => {
const explorer = new Explorer();
const mongoExplorer = new Explorer();
updateUserContext({
databaseAccount: {
properties: {
capabilities: [{ name: "EnableGremlin" }],
},
} as DatabaseAccount,
});
const collectionWithoutPartitionKey: ViewModels.Collection = {
id: ko.observable<string>("foo"),
databaseId: "foo",
container: explorer,
} as ViewModels.Collection;
const collectionWithSystemPartitionKey: ViewModels.Collection = {
id: ko.observable<string>("foo"),
databaseId: "foo",
partitionKey: {
paths: ["/foo"],
kind: "Hash",
version: 2,
systemKey: true,
},
container: explorer,
} as ViewModels.Collection;
const collectionWithNonSystemPartitionKey: ViewModels.Collection = {
id: ko.observable<string>("foo"),
databaseId: "foo",
partitionKey: {
paths: ["/foo"],
kind: "Hash",
version: 2,
systemKey: false,
},
container: explorer,
} as ViewModels.Collection;
const mongoCollectionWithSystemPartitionKey: ViewModels.Collection = {
id: ko.observable<string>("foo"),
databaseId: "foo",
partitionKey: {
paths: ["/foo"],
kind: "Hash",
version: 2,
systemKey: true,
},
container: mongoExplorer,
} as ViewModels.Collection;
it("should be false for null or undefined collection", () => {
expect(showPartitionKey(undefined, false)).toBe(false);
expect(showPartitionKey(null, false)).toBe(false);
expect(showPartitionKey(undefined, true)).toBe(false);
expect(showPartitionKey(null, true)).toBe(false);
});
it("should be false for null or undefined partitionKey", () => {
expect(showPartitionKey(collectionWithoutPartitionKey, false)).toBe(false);
});
it("should be true for non-Mongo accounts with system partitionKey", () => {
expect(showPartitionKey(collectionWithSystemPartitionKey, false)).toBe(true);
});
it("should be false for Mongo accounts with system partitionKey", () => {
expect(showPartitionKey(mongoCollectionWithSystemPartitionKey, true)).toBe(false);
});
it("should be true for non-system partitionKey", () => {
expect(showPartitionKey(collectionWithNonSystemPartitionKey, false)).toBe(true);
});
});
describe("when getting command bar button state", () => {
describe("should set Save New Document state", () => {
const testCases = new Set<{ state: ViewModels.DocumentExplorerState; enabled: boolean; visible: boolean }>();
testCases.add({ state: ViewModels.DocumentExplorerState.noDocumentSelected, enabled: false, visible: false });
testCases.add({ state: ViewModels.DocumentExplorerState.newDocumentValid, enabled: true, visible: true });
testCases.add({ state: ViewModels.DocumentExplorerState.newDocumentInvalid, enabled: false, visible: true });
testCases.add({
state: ViewModels.DocumentExplorerState.existingDocumentNoEdits,
enabled: false,
visible: false,
});
testCases.add({
state: ViewModels.DocumentExplorerState.existingDocumentDirtyValid,
enabled: false,
visible: false,
});
testCases.add({
state: ViewModels.DocumentExplorerState.existingDocumentDirtyInvalid,
enabled: false,
visible: false,
});
testCases.forEach((testCase) => {
const state = getSaveNewDocumentButtonState(testCase.state);
it(`enable for ${testCase.state}`, () => {
expect(state.enabled).toBe(testCase.enabled);
});
it(`visible for ${testCase.state}`, () => {
expect(state.visible).toBe(testCase.visible);
});
});
});
describe("should set Discard New Document state", () => {
const testCases = new Set<{ state: ViewModels.DocumentExplorerState; enabled: boolean; visible: boolean }>();
testCases.add({ state: ViewModels.DocumentExplorerState.noDocumentSelected, enabled: false, visible: false });
testCases.add({ state: ViewModels.DocumentExplorerState.newDocumentValid, enabled: true, visible: true });
testCases.add({ state: ViewModels.DocumentExplorerState.newDocumentInvalid, enabled: true, visible: true });
testCases.add({
state: ViewModels.DocumentExplorerState.existingDocumentNoEdits,
enabled: false,
visible: false,
});
testCases.add({
state: ViewModels.DocumentExplorerState.existingDocumentDirtyValid,
enabled: false,
visible: false,
});
testCases.add({
state: ViewModels.DocumentExplorerState.existingDocumentDirtyInvalid,
enabled: false,
visible: false,
});
testCases.forEach((testCase) => {
const state = getDiscardNewDocumentChangesButtonState(testCase.state);
it(`enable for ${testCase.state}`, () => {
expect(state.enabled).toBe(testCase.enabled);
});
it(`visible for ${testCase.state}`, () => {
expect(state.visible).toBe(testCase.visible);
});
});
});
describe("should set Save Existing Document state", () => {
const testCases = new Set<{ state: ViewModels.DocumentExplorerState; enabled: boolean; visible: boolean }>();
testCases.add({ state: ViewModels.DocumentExplorerState.noDocumentSelected, enabled: false, visible: false });
testCases.add({ state: ViewModels.DocumentExplorerState.newDocumentValid, enabled: false, visible: false });
testCases.add({ state: ViewModels.DocumentExplorerState.newDocumentInvalid, enabled: false, visible: false });
testCases.add({
state: ViewModels.DocumentExplorerState.existingDocumentNoEdits,
enabled: false,
visible: true,
});
testCases.add({
state: ViewModels.DocumentExplorerState.existingDocumentDirtyValid,
enabled: true,
visible: true,
});
testCases.add({
state: ViewModels.DocumentExplorerState.existingDocumentDirtyInvalid,
enabled: false,
visible: true,
});
testCases.forEach((testCase) => {
const state = getSaveExistingDocumentButtonState(testCase.state);
it(`enable for ${testCase.state}`, () => {
expect(state.enabled).toBe(testCase.enabled);
});
it(`visible for ${testCase.state}`, () => {
expect(state.visible).toBe(testCase.visible);
});
});
});
describe("should set Discard Existing Document state", () => {
const testCases = new Set<{ state: ViewModels.DocumentExplorerState; enabled: boolean; visible: boolean }>();
testCases.add({ state: ViewModels.DocumentExplorerState.noDocumentSelected, enabled: false, visible: false });
testCases.add({ state: ViewModels.DocumentExplorerState.newDocumentValid, enabled: false, visible: false });
testCases.add({ state: ViewModels.DocumentExplorerState.newDocumentInvalid, enabled: false, visible: false });
testCases.add({
state: ViewModels.DocumentExplorerState.existingDocumentNoEdits,
enabled: false,
visible: true,
});
testCases.add({
state: ViewModels.DocumentExplorerState.existingDocumentDirtyValid,
enabled: true,
visible: true,
});
testCases.add({
state: ViewModels.DocumentExplorerState.existingDocumentDirtyInvalid,
enabled: true,
visible: true,
});
testCases.forEach((testCase) => {
const state = getDiscardExistingDocumentChangesButtonState(testCase.state);
it(`enable for ${testCase.state}`, () => {
expect(state.enabled).toBe(testCase.enabled);
});
it(`visible for ${testCase.state}`, () => {
expect(state.visible).toBe(testCase.visible);
});
});
});
describe("should set Delete Existing Document state", () => {
const testCases = new Set<{ state: ViewModels.DocumentExplorerState; enabled: boolean; visible: boolean }>();
testCases.add({ state: ViewModels.DocumentExplorerState.noDocumentSelected, enabled: false, visible: false });
testCases.add({ state: ViewModels.DocumentExplorerState.newDocumentValid, enabled: false, visible: false });
testCases.add({ state: ViewModels.DocumentExplorerState.newDocumentInvalid, enabled: false, visible: false });
testCases.add({ state: ViewModels.DocumentExplorerState.existingDocumentNoEdits, enabled: true, visible: true });
testCases.add({
state: ViewModels.DocumentExplorerState.existingDocumentDirtyValid,
enabled: true,
visible: true,
});
testCases.add({
state: ViewModels.DocumentExplorerState.existingDocumentDirtyInvalid,
enabled: true,
visible: true,
});
});
});
it("Do not get tabs button for Fabric readonly", () => {
updateConfigContext({ platform: Platform.Fabric });
updateUserContext({
fabricContext: {
connectionId: "test",
databaseConnectionInfo: undefined,
isReadOnly: true,
isVisible: true,
},
});
const buttons = getTabsButtons({} as ButtonsDependencies);
expect(buttons.length).toBe(0);
});
describe("when rendered", () => {
const createMockProps = (): IDocumentsTabComponentProps => ({
isPreferredApiMongoDB: false,
documentIds: [],
collection: undefined,
partitionKey: undefined,
onLoadStartKey: 0,
tabTitle: "",
onExecutionErrorChange: (isExecutionError: boolean): void => {
isExecutionError;
},
onIsExecutingChange: (isExecuting: boolean): void => {
isExecuting;
},
isTabActive: true,
});
let wrapper: ShallowWrapper;
beforeEach(async () => {
const props: IDocumentsTabComponentProps = createMockProps();
wrapper = shallow(<DocumentsTabComponent {...props} />);
});
afterEach(() => {
wrapper.unmount();
});
it("should render the page", () => {
expect(wrapper).toMatchSnapshot();
});
it("clicking on Edit filter should render the Apply Filter button", () => {
wrapper
.findWhere((node) => node.text() === "Edit Filter")
.at(0)
.simulate("click");
expect(wrapper.findWhere((node) => node.text() === "Apply Filter").exists()).toBeTruthy();
});
it("clicking on Edit filter should render input for filter", () => {
wrapper
.findWhere((node) => node.text() === "Edit Filter")
.at(0)
.simulate("click");
expect(wrapper.find("#filterInput").exists()).toBeTruthy();
});
});
describe("Command bar buttons", () => {
const createMockProps = (): IDocumentsTabComponentProps => ({
isPreferredApiMongoDB: false,
documentIds: [],
collection: {
id: ko.observable<string>("foo"),
container: new Explorer(),
partitionKey: {
kind: "MultiHash",
paths: ["/pkey1", "/pkey2", "/pkey3"],
version: 2,
},
partitionKeyProperties: ["pkey1", "pkey2", "pkey3"],
partitionKeyPropertyHeaders: ["/pkey1", "/pkey2", "/pkey3"],
} as ViewModels.CollectionBase,
partitionKey: undefined,
onLoadStartKey: 0,
tabTitle: "",
onExecutionErrorChange: (isExecutionError: boolean): void => {
isExecutionError;
},
onIsExecutingChange: (isExecuting: boolean): void => {
isExecuting;
},
isTabActive: true,
});
let wrapper: ReactWrapper;
beforeEach(async () => {
updateConfigContext({ platform: Platform.Hosted });
const props: IDocumentsTabComponentProps = createMockProps();
wrapper = mount(<DocumentsTabComponent {...props} />);
wrapper = await waitForComponentToPaint(wrapper);
});
afterEach(() => {
wrapper.unmount();
});
it("renders by default the first document", async () => {
expect(wrapper.findWhere((node) => node.text().includes(PROPERTY_VALUE)).exists()).toBeTruthy();
});
it("default buttons", async () => {
expect(useCommandBar.getState().contextButtons.find((button) => button.id === UPDATE_BUTTON_ID)).toBeDefined();
expect(useCommandBar.getState().contextButtons.find((button) => button.id === DISCARD_BUTTON_ID)).toBeDefined();
expect(useCommandBar.getState().contextButtons.find((button) => button.id === DELETE_BUTTON_ID)).toBeDefined();
expect(useCommandBar.getState().contextButtons.find((button) => button.id === UPLOAD_BUTTON_ID)).toBeDefined();
});
it("clicking on New Document should show editor with new document", () => {
act(() => {
useCommandBar
.getState()
.contextButtons.find((button) => button.id === NEW_DOCUMENT_BUTTON_ID)
.onCommandClick(undefined);
});
expect(wrapper.findWhere((node) => node.text().includes("replace_with_new_document_id")).exists()).toBeTruthy();
});
it("clicking on New Document should show Save and Discard buttons", () => {
act(() => {
useCommandBar
.getState()
.contextButtons.find((button) => button.id === NEW_DOCUMENT_BUTTON_ID)
.onCommandClick(undefined);
});
expect(useCommandBar.getState().contextButtons.find((button) => button.id === SAVE_BUTTON_ID)).toBeDefined();
expect(useCommandBar.getState().contextButtons.find((button) => button.id === DISCARD_BUTTON_ID)).toBeDefined();
});
it("clicking Delete Document asks for confirmation", () => {
const mockDeleteDocuments = deleteDocuments as jest.Mock;
mockDeleteDocuments.mockClear();
act(() => {
useCommandBar
.getState()
.contextButtons.find((button) => button.id === DELETE_BUTTON_ID)
.onCommandClick(undefined);
});
expect(mockDeleteDocuments).toHaveBeenCalled();
});
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -1,195 +0,0 @@
import { deleteDocument } from "Common/MongoProxyClient";
import { Platform, updateConfigContext } from "ConfigContext";
import { EditorReactProps } from "Explorer/Controls/Editor/EditorReact";
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
import {
DELETE_BUTTON_ID,
DISCARD_BUTTON_ID,
DocumentsTabComponent,
IDocumentsTabComponentProps,
NEW_DOCUMENT_BUTTON_ID,
SAVE_BUTTON_ID,
UPDATE_BUTTON_ID,
buildQuery,
} from "Explorer/Tabs/DocumentsTabV2/DocumentsTabV2";
import { ReactWrapper, ShallowWrapper, mount } from "enzyme";
import * as ko from "knockout";
import React from "react";
import { act } from "react-dom/test-utils";
import * as ViewModels from "../../../Contracts/ViewModels";
import Explorer from "../../Explorer";
jest.requireActual("Explorer/Controls/Editor/EditorReact");
const PROPERTY_VALUE = "__SOME_PROPERTY_VALUE__";
jest.mock("Common/MongoProxyClient", () => ({
queryDocuments: jest.fn(() =>
Promise.resolve({
continuationToken: "",
documents: [
{
_rid: "_rid",
_self: "_self",
_etag: "etag",
_ts: 1234,
id: "id",
},
],
headers: {},
}),
),
readDocument: jest.fn(() =>
Promise.resolve({
_rid: "_rid1",
_self: "_self1",
_etag: "etag1",
property: PROPERTY_VALUE,
_ts: 5678,
id: "id1",
}),
),
deleteDocument: jest.fn(() => Promise.resolve()),
}));
jest.mock("Explorer/Controls/Editor/EditorReact", () => ({
EditorReact: (props: EditorReactProps) => <>{props.content}</>,
}));
jest.mock("Explorer/Controls/Dialog", () => ({
useDialog: {
getState: jest.fn(() => ({
showOkCancelModalDialog: (title: string, subText: string, okLabel: string, onOk: () => void) => onOk(),
showOkModalDialog: () => {},
})),
},
}));
async function waitForComponentToPaint<P = unknown>(wrapper: ReactWrapper<P> | ShallowWrapper<P>, amount = 0) {
let newWrapper;
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, amount));
newWrapper = wrapper.update();
});
return newWrapper;
}
describe("Documents tab (Mongo API)", () => {
describe("buildQuery", () => {
it("should generate the right select query for SQL API", () => {
expect(buildQuery(true, "")).toContain("{}");
});
});
describe("Command bar buttons", () => {
const createMockProps = (): IDocumentsTabComponentProps => ({
isPreferredApiMongoDB: true,
documentIds: [],
collection: {
id: ko.observable<string>("foo"),
container: new Explorer(),
partitionKey: {
kind: "Hash",
paths: ["/pkey"],
version: 2,
},
partitionKeyProperties: ["pkey"],
partitionKeyPropertyHeaders: ["/pkey"],
databaseId: "databaseId",
self: "self",
rawDataModel: undefined,
selectedSubnodeKind: undefined,
children: undefined,
isCollectionExpanded: undefined,
onDocumentDBDocumentsClick: (): void => {
throw new Error("Function not implemented.");
},
onNewQueryClick: (): void => {
throw new Error("Function not implemented.");
},
expandCollection: (): void => {
throw new Error("Function not implemented.");
},
collapseCollection: (): void => {
throw new Error("Function not implemented.");
},
getDatabase: (): ViewModels.Database => {
throw new Error("Function not implemented.");
},
nodeKind: "nodeKind",
rid: "rid",
},
partitionKey: undefined,
onLoadStartKey: 0,
tabTitle: "",
onExecutionErrorChange: (isExecutionError: boolean): void => {
isExecutionError;
},
onIsExecutingChange: (isExecuting: boolean): void => {
isExecuting;
},
isTabActive: true,
});
let wrapper: ReactWrapper;
beforeEach(async () => {
updateConfigContext({ platform: Platform.Hosted });
const props: IDocumentsTabComponentProps = createMockProps();
wrapper = mount(<DocumentsTabComponent {...props} />);
wrapper = await waitForComponentToPaint(wrapper);
});
afterEach(() => {
wrapper.unmount();
});
it("renders by default the first document", async () => {
expect(wrapper.findWhere((node) => node.text().includes(PROPERTY_VALUE)).exists()).toBeTruthy();
});
it("default buttons", async () => {
expect(useCommandBar.getState().contextButtons.find((button) => button.id === UPDATE_BUTTON_ID)).toBeDefined();
expect(useCommandBar.getState().contextButtons.find((button) => button.id === DISCARD_BUTTON_ID)).toBeDefined();
expect(useCommandBar.getState().contextButtons.find((button) => button.id === DELETE_BUTTON_ID)).toBeDefined();
});
it("clicking on New Document should show editor with new document", () => {
act(() => {
useCommandBar
.getState()
.contextButtons.find((button) => button.id === NEW_DOCUMENT_BUTTON_ID)
.onCommandClick(undefined);
});
expect(wrapper.findWhere((node) => node.text().includes("replace_with_new_document_id")).exists()).toBeTruthy();
});
it("clicking on New Document should show Save and Discard buttons", () => {
act(() => {
useCommandBar
.getState()
.contextButtons.find((button) => button.id === NEW_DOCUMENT_BUTTON_ID)
.onCommandClick(undefined);
});
expect(useCommandBar.getState().contextButtons.find((button) => button.id === SAVE_BUTTON_ID)).toBeDefined();
expect(useCommandBar.getState().contextButtons.find((button) => button.id === DISCARD_BUTTON_ID)).toBeDefined();
});
it("clicking Delete Document asks for confirmation", () => {
const mockDeleteDocument = deleteDocument as jest.Mock;
mockDeleteDocument.mockClear();
act(() => {
useCommandBar
.getState()
.contextButtons.find((button) => button.id === DELETE_BUTTON_ID)
.onCommandClick(undefined);
});
expect(mockDeleteDocument).toHaveBeenCalled();
});
});
});

View File

@@ -1,42 +0,0 @@
import { TableRowId } from "@fluentui/react-components";
import { mount } from "enzyme";
import React from "react";
import { DocumentsTableComponent, IDocumentsTableComponentProps } from "./DocumentsTableComponent";
const PARTITION_KEY_HEADER = "partitionKey";
const ID_HEADER = "id";
describe("DocumentsTableComponent", () => {
const createMockProps = (): IDocumentsTableComponentProps => ({
items: [
{ [ID_HEADER]: "1", [PARTITION_KEY_HEADER]: "pk1" },
{ [ID_HEADER]: "2", [PARTITION_KEY_HEADER]: "pk2" },
{ [ID_HEADER]: "3", [PARTITION_KEY_HEADER]: "pk3" },
],
onItemClicked: (): void => {},
onSelectedRowsChange: (): void => {},
selectedRows: new Set<TableRowId>(),
size: {
height: 0,
width: 0,
},
columnHeaders: {
idHeader: ID_HEADER,
partitionKeyHeaders: [PARTITION_KEY_HEADER],
},
isSelectionDisabled: false,
});
it("should render documents and partition keys in header", () => {
const props: IDocumentsTableComponentProps = createMockProps();
const wrapper = mount(<DocumentsTableComponent {...props} />);
expect(wrapper).toMatchSnapshot();
});
it("should not render selection column when isSelectionDisabled is true", () => {
const props: IDocumentsTableComponentProps = createMockProps();
props.isSelectionDisabled = true;
const wrapper = mount(<DocumentsTableComponent {...props} />);
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -1,313 +0,0 @@
import {
Menu,
MenuItem,
MenuList,
MenuPopover,
MenuTrigger,
TableRowData as RowStateBase,
Table,
TableBody,
TableCell,
TableCellLayout,
TableColumnDefinition,
TableColumnSizingOptions,
TableHeader,
TableHeaderCell,
TableRow,
TableRowId,
TableSelectionCell,
createTableColumn,
useArrowNavigationGroup,
useTableColumnSizing_unstable,
useTableFeatures,
useTableSelection,
} from "@fluentui/react-components";
import { NormalizedEventKey } from "Common/Constants";
import { selectionHelper } from "Explorer/Tabs/DocumentsTabV2/SelectionHelper";
import { isEnvironmentCtrlPressed, isEnvironmentShiftPressed } from "Utils/KeyboardUtils";
import React, { useCallback, useMemo } from "react";
import { FixedSizeList as List, ListChildComponentProps } from "react-window";
export type DocumentsTableComponentItem = {
id: string;
} & Record<string, string>;
export type ColumnHeaders = {
idHeader: string;
partitionKeyHeaders: string[];
};
export interface IDocumentsTableComponentProps {
items: DocumentsTableComponentItem[];
onItemClicked: (index: number) => void;
onSelectedRowsChange: (selectedItemsIndices: Set<TableRowId>) => void;
selectedRows: Set<TableRowId>;
size: { height: number; width: number };
columnHeaders: ColumnHeaders;
style?: React.CSSProperties;
isSelectionDisabled?: boolean;
}
interface TableRowData extends RowStateBase<DocumentsTableComponentItem> {
onClick: (e: React.MouseEvent) => void;
onKeyDown: (e: React.KeyboardEvent) => void;
selected: boolean;
appearance: "brand" | "none";
}
interface ReactWindowRenderFnProps extends ListChildComponentProps {
data: TableRowData[];
}
export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> = ({
items,
onSelectedRowsChange,
selectedRows,
style,
size,
columnHeaders,
isSelectionDisabled,
}: IDocumentsTableComponentProps) => {
const initialSizingOptions: TableColumnSizingOptions = {
id: {
idealWidth: 280,
minWidth: 50,
},
};
columnHeaders.partitionKeyHeaders.forEach((pkHeader) => {
initialSizingOptions[pkHeader] = {
idealWidth: 200,
minWidth: 50,
};
});
const [columnSizingOptions, setColumnSizingOptions] = React.useState<TableColumnSizingOptions>(initialSizingOptions);
const onColumnResize = React.useCallback((_, { columnId, width }) => {
setColumnSizingOptions((state) => ({
...state,
[columnId]: {
...state[columnId],
idealWidth: width,
},
}));
}, []);
// Columns must be a static object and cannot change on re-renders otherwise React will complain about too many refreshes
const columns: TableColumnDefinition<DocumentsTableComponentItem>[] = useMemo(
() =>
[
createTableColumn<DocumentsTableComponentItem>({
columnId: "id",
compare: (a, b) => a.id.localeCompare(b.id),
renderHeaderCell: () => columnHeaders.idHeader,
renderCell: (item) => (
<TableCellLayout truncate title={item.id}>
{item.id}
</TableCellLayout>
),
}),
].concat(
columnHeaders.partitionKeyHeaders.map((pkHeader) =>
createTableColumn<DocumentsTableComponentItem>({
columnId: pkHeader,
compare: (a, b) => a[pkHeader].localeCompare(b[pkHeader]),
// Show Refresh button on last column
renderHeaderCell: () => <span title={pkHeader}>{pkHeader}</span>,
renderCell: (item) => (
<TableCellLayout truncate title={item[pkHeader]}>
{item[pkHeader]}
</TableCellLayout>
),
}),
),
),
[columnHeaders],
);
const [selectionStartIndex, setSelectionStartIndex] = React.useState<number>(undefined);
const onTableCellClicked = useCallback(
(e: React.MouseEvent, index: number) => {
if (isSelectionDisabled) {
// Only allow click
onSelectedRowsChange(new Set<TableRowId>([index]));
setSelectionStartIndex(index);
return;
}
const result = selectionHelper(
selectedRows as Set<number>,
index,
isEnvironmentShiftPressed(e),
isEnvironmentCtrlPressed(e),
selectionStartIndex,
);
onSelectedRowsChange(result.selection);
if (result.selectionStartIndex !== undefined) {
setSelectionStartIndex(result.selectionStartIndex);
}
},
[isSelectionDisabled, selectedRows, selectionStartIndex, onSelectedRowsChange],
);
/**
* Callback for when:
* - a key has been pressed on the cell
* - a key is down and the cell is clicked by the mouse
*/
const onIdClicked = useCallback(
(e: React.KeyboardEvent<Element>, index: number) => {
if (e.key === NormalizedEventKey.Enter || e.key === NormalizedEventKey.Space) {
onSelectedRowsChange(new Set<TableRowId>([index]));
}
},
[onSelectedRowsChange],
);
const RenderRow = ({ index, style, data }: ReactWindowRenderFnProps) => {
const { item, selected, appearance, onClick, onKeyDown } = data[index];
return (
<TableRow
aria-rowindex={index + 2}
style={{ ...style, cursor: "pointer", userSelect: "none" }}
key={item.id}
aria-selected={selected}
appearance={appearance}
>
{!isSelectionDisabled && (
<TableSelectionCell
checked={selected}
checkboxIndicator={{ "aria-label": "Select row" }}
onClick={(e: React.MouseEvent) => {
setSelectionStartIndex(index);
onClick(e);
}}
onKeyDown={onKeyDown}
/>
)}
{columns.map((column) => (
<TableCell
key={column.columnId}
className="documentsTableCell"
// When clicking on a cell with shift/ctrl key, onKeyDown is called instead of onClick.
onClick={(e: React.MouseEvent<Element, MouseEvent>) => onTableCellClicked(e, index)}
onKeyPress={(e: React.KeyboardEvent<Element>) => onIdClicked(e, index)}
{...columnSizing.getTableCellProps(column.columnId)}
tabIndex={column.columnId === "id" ? 0 : -1}
>
{column.renderCell(item)}
</TableCell>
))}
</TableRow>
);
};
const {
getRows,
columnSizing_unstable: columnSizing,
tableRef,
selection: { allRowsSelected, someRowsSelected, toggleAllRows, toggleRow, isRowSelected },
} = useTableFeatures(
{
columns,
items,
},
[
useTableColumnSizing_unstable({ columnSizingOptions, onColumnResize }),
useTableSelection({
selectionMode: isSelectionDisabled ? "single" : "multiselect",
selectedItems: selectedRows,
// eslint-disable-next-line react/prop-types
onSelectionChange: (e, data) => onSelectedRowsChange(data.selectedItems),
}),
],
);
const rows: TableRowData[] = getRows((row) => {
const selected = isRowSelected(row.rowId);
return {
...row,
onClick: (e: React.MouseEvent) => toggleRow(e, row.rowId),
onKeyDown: (e: React.KeyboardEvent) => {
if (e.key === " ") {
e.preventDefault();
toggleRow(e, row.rowId);
}
},
selected,
appearance: selected ? ("brand" as const) : ("none" as const),
};
});
const toggleAllKeydown = React.useCallback(
(e: React.KeyboardEvent<HTMLDivElement>) => {
if (e.key === " ") {
toggleAllRows(e);
e.preventDefault();
}
},
[toggleAllRows],
);
// Cell keyboard navigation
const keyboardNavAttr = useArrowNavigationGroup({ axis: "grid" });
// TODO: Bug in fluent UI typings that requires any here
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const tableProps: any = {
"aria-label": "Filtered documents table",
role: "grid",
...columnSizing.getTableProps(),
...keyboardNavAttr,
size: "extra-small",
ref: tableRef,
...style,
};
return (
<Table className="documentsTable" noNativeElements {...tableProps}>
<TableHeader className="documentsTableHeader">
<TableRow style={{ width: size ? size.width - 15 : "100%" }}>
{!isSelectionDisabled && (
<TableSelectionCell
checked={allRowsSelected ? true : someRowsSelected ? "mixed" : false}
onClick={toggleAllRows}
onKeyDown={toggleAllKeydown}
checkboxIndicator={{ "aria-label": "Select all rows " }}
/>
)}
{columns.map((column /* index */) => (
<Menu openOnContext key={column.columnId}>
<MenuTrigger>
<TableHeaderCell
className="documentsTableCell"
key={column.columnId}
{...columnSizing.getTableHeaderCellProps(column.columnId)}
>
{column.renderHeaderCell()}
</TableHeaderCell>
</MenuTrigger>
<MenuPopover>
<MenuList>
<MenuItem onClick={columnSizing.enableKeyboardMode(column.columnId)}>
Keyboard Column Resizing
</MenuItem>
</MenuList>
</MenuPopover>
</Menu>
))}
</TableRow>
</TableHeader>
<TableBody>
<List
height={size !== undefined ? size.height - 32 /* table header */ - 21 /* load more */ : 0}
itemCount={items.length}
itemSize={30}
width={size ? size.width : 0}
itemData={rows}
style={{ overflowY: "scroll" }}
>
{RenderRow}
</List>
</TableBody>
</Table>
);
};

View File

@@ -1,92 +0,0 @@
/**
* Utility class to help with selection.
* This emulates File Explorer selection behavior.
* ctrl: toggle selection of index.
* shift: select all rows between selectionStartIndex and index
* shift + ctrl: select or deselect all rows between selectionStartIndex and index depending on whether selectionStartIndex is selected
* No modifier only selects the clicked row
* ctrl: updates selection start index
* shift: do not update selection start index
*
* @param currentSelection current selection
* @param clickedIndex index of clicked row
* @param isShiftKey shift key is pressed
* @param isCtrlKey ctrl key is pressed
* @param selectionStartIndex index of current selected row
* @returns new selection and selection start
*/
export const selectionHelper = (
currentSelection: Set<number>,
clickedIndex: number,
isShiftKey: boolean,
isCtrlKey: boolean,
selectionStartIndex: number,
): {
selection: Set<number>;
selectionStartIndex: number;
} => {
if (isShiftKey) {
// Shift is about selecting range of rows
if (isCtrlKey) {
// shift + ctrl
const isSelectionStartIndexSelected = currentSelection.has(selectionStartIndex);
const min = Math.min(clickedIndex, selectionStartIndex);
const max = Math.max(clickedIndex, selectionStartIndex);
const newSelection = new Set<number>(currentSelection);
for (let i = min; i <= max; i++) {
// Select or deselect range depending on how selectionStartIndex is selected
if (isSelectionStartIndexSelected) {
// Select range
newSelection.add(i);
} else {
// Deselect range
newSelection.delete(i);
}
}
return {
selection: newSelection,
selectionStartIndex: undefined,
};
} else {
// shift only
// Shift only: enable everything between lastClickedIndex and clickedIndex and disable everything else
const min = Math.min(clickedIndex, selectionStartIndex);
const max = Math.max(clickedIndex, selectionStartIndex);
const newSelection = new Set<number>();
for (let i = min; i <= max; i++) {
newSelection.add(i);
}
return {
selection: newSelection,
selectionStartIndex: undefined, // do not change selection start
};
}
} else {
if (isCtrlKey) {
// Ctrl only: toggle selection where we clicked
const isNotSelected = !currentSelection.has(clickedIndex);
if (isNotSelected) {
return {
selection: new Set(currentSelection.add(clickedIndex)),
selectionStartIndex: clickedIndex,
};
} else {
// Remove
currentSelection.delete(clickedIndex);
return {
selection: new Set(currentSelection),
selectionStartIndex: clickedIndex,
};
}
} else {
// If no modifier keys are pressed, select only the clicked row
return {
selection: new Set<number>([clickedIndex]),
selectionStartIndex: clickedIndex,
};
}
}
};

View File

@@ -1,559 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Documents tab (noSql API) when rendered should render the page 1`] = `
<FluentProvider
style={
Object {
"height": "100%",
}
}
theme={
Object {
"borderRadiusCircular": "10000px",
"borderRadiusLarge": "6px",
"borderRadiusMedium": "4px",
"borderRadiusNone": "0",
"borderRadiusSmall": "2px",
"borderRadiusXLarge": "8px",
"colorBackgroundOverlay": "rgba(0, 0, 0, 0.4)",
"colorBrandBackground": "#117865",
"colorBrandBackground2": "#e3f7ef",
"colorBrandBackground2Hover": "#c0ecdd",
"colorBrandBackground2Pressed": "#78d3b9",
"colorBrandBackgroundHover": "#0c695a",
"colorBrandBackgroundInverted": "#ffffff",
"colorBrandBackgroundInvertedHover": "#e3f7ef",
"colorBrandBackgroundInvertedPressed": "#9ee0cb",
"colorBrandBackgroundInvertedSelected": "#c0ecdd",
"colorBrandBackgroundPressed": "#033f38",
"colorBrandBackgroundSelected": "#0a5c50",
"colorBrandBackgroundStatic": "#117865",
"colorBrandForeground1": "#117865",
"colorBrandForeground2": "#0c695a",
"colorBrandForeground2Hover": "#0a5c50",
"colorBrandForeground2Pressed": "#01322E",
"colorBrandForegroundInverted": "#2aaC94",
"colorBrandForegroundInvertedHover": "#3abb9f",
"colorBrandForegroundInvertedPressed": "#2aaC94",
"colorBrandForegroundLink": "#0c695a",
"colorBrandForegroundLinkHover": "#0a5c50",
"colorBrandForegroundLinkPressed": "#033f38",
"colorBrandForegroundLinkSelected": "#0c695a",
"colorBrandForegroundOnLight": "#117865",
"colorBrandForegroundOnLightHover": "#0c695a",
"colorBrandForegroundOnLightPressed": "#054d43",
"colorBrandForegroundOnLightSelected": "#0a5c50",
"colorBrandShadowAmbient": "rgba(0,0,0,0.30)",
"colorBrandShadowKey": "rgba(0,0,0,0.25)",
"colorBrandStroke1": "#117865",
"colorBrandStroke2": "#9ee0cb",
"colorBrandStroke2Contrast": "#9ee0cb",
"colorBrandStroke2Hover": "#52c7aa",
"colorBrandStroke2Pressed": "#117865",
"colorCompoundBrandBackground": "#117865",
"colorCompoundBrandBackgroundHover": "#0c695a",
"colorCompoundBrandBackgroundPressed": "#0a5c50",
"colorCompoundBrandForeground1": "#117865",
"colorCompoundBrandForeground1Hover": "#0c695a",
"colorCompoundBrandForeground1Pressed": "#0a5c50",
"colorCompoundBrandStroke": "#117865",
"colorCompoundBrandStrokeHover": "#0c695a",
"colorCompoundBrandStrokePressed": "#0a5c50",
"colorNeutralBackground1": "#ffffff",
"colorNeutralBackground1Hover": "#f5f5f5",
"colorNeutralBackground1Pressed": "#e0e0e0",
"colorNeutralBackground1Selected": "#ebebeb",
"colorNeutralBackground2": "#fafafa",
"colorNeutralBackground2Hover": "#f0f0f0",
"colorNeutralBackground2Pressed": "#dbdbdb",
"colorNeutralBackground2Selected": "#e6e6e6",
"colorNeutralBackground3": "#f5f5f5",
"colorNeutralBackground3Hover": "#ebebeb",
"colorNeutralBackground3Pressed": "#d6d6d6",
"colorNeutralBackground3Selected": "#e0e0e0",
"colorNeutralBackground4": "#f0f0f0",
"colorNeutralBackground4Hover": "#fafafa",
"colorNeutralBackground4Pressed": "#f5f5f5",
"colorNeutralBackground4Selected": "#ffffff",
"colorNeutralBackground5": "#ebebeb",
"colorNeutralBackground5Hover": "#f5f5f5",
"colorNeutralBackground5Pressed": "#f0f0f0",
"colorNeutralBackground5Selected": "#fafafa",
"colorNeutralBackground6": "#e6e6e6",
"colorNeutralBackgroundAlpha": "rgba(255, 255, 255, 0.5)",
"colorNeutralBackgroundAlpha2": "rgba(255, 255, 255, 0.8)",
"colorNeutralBackgroundDisabled": "#f0f0f0",
"colorNeutralBackgroundInverted": "#292929",
"colorNeutralBackgroundInvertedDisabled": "rgba(255, 255, 255, 0.1)",
"colorNeutralBackgroundStatic": "#333333",
"colorNeutralForeground1": "#242424",
"colorNeutralForeground1Hover": "#242424",
"colorNeutralForeground1Pressed": "#242424",
"colorNeutralForeground1Selected": "#242424",
"colorNeutralForeground1Static": "#242424",
"colorNeutralForeground2": "#424242",
"colorNeutralForeground2BrandHover": "#117865",
"colorNeutralForeground2BrandPressed": "#0c695a",
"colorNeutralForeground2BrandSelected": "#117865",
"colorNeutralForeground2Hover": "#242424",
"colorNeutralForeground2Link": "#424242",
"colorNeutralForeground2LinkHover": "#242424",
"colorNeutralForeground2LinkPressed": "#242424",
"colorNeutralForeground2LinkSelected": "#242424",
"colorNeutralForeground2Pressed": "#242424",
"colorNeutralForeground2Selected": "#242424",
"colorNeutralForeground3": "#616161",
"colorNeutralForeground3BrandHover": "#117865",
"colorNeutralForeground3BrandPressed": "#0c695a",
"colorNeutralForeground3BrandSelected": "#117865",
"colorNeutralForeground3Hover": "#424242",
"colorNeutralForeground3Pressed": "#424242",
"colorNeutralForeground3Selected": "#424242",
"colorNeutralForeground4": "#707070",
"colorNeutralForegroundDisabled": "#bdbdbd",
"colorNeutralForegroundInverted": "#ffffff",
"colorNeutralForegroundInverted2": "#ffffff",
"colorNeutralForegroundInvertedDisabled": "rgba(255, 255, 255, 0.4)",
"colorNeutralForegroundInvertedHover": "#ffffff",
"colorNeutralForegroundInvertedLink": "#ffffff",
"colorNeutralForegroundInvertedLinkHover": "#ffffff",
"colorNeutralForegroundInvertedLinkPressed": "#ffffff",
"colorNeutralForegroundInvertedLinkSelected": "#ffffff",
"colorNeutralForegroundInvertedPressed": "#ffffff",
"colorNeutralForegroundInvertedSelected": "#ffffff",
"colorNeutralForegroundOnBrand": "#ffffff",
"colorNeutralForegroundStaticInverted": "#ffffff",
"colorNeutralShadowAmbient": "rgba(0,0,0,0.12)",
"colorNeutralShadowAmbientDarker": "rgba(0,0,0,0.20)",
"colorNeutralShadowAmbientLighter": "rgba(0,0,0,0.06)",
"colorNeutralShadowKey": "rgba(0,0,0,0.14)",
"colorNeutralShadowKeyDarker": "rgba(0,0,0,0.24)",
"colorNeutralShadowKeyLighter": "rgba(0,0,0,0.07)",
"colorNeutralStencil1": "#e6e6e6",
"colorNeutralStencil1Alpha": "rgba(0, 0, 0, 0.1)",
"colorNeutralStencil2": "#fafafa",
"colorNeutralStencil2Alpha": "rgba(0, 0, 0, 0.05)",
"colorNeutralStroke1": "#d1d1d1",
"colorNeutralStroke1Hover": "#c7c7c7",
"colorNeutralStroke1Pressed": "#b3b3b3",
"colorNeutralStroke1Selected": "#bdbdbd",
"colorNeutralStroke2": "#e0e0e0",
"colorNeutralStroke3": "#f0f0f0",
"colorNeutralStrokeAccessible": "#616161",
"colorNeutralStrokeAccessibleHover": "#575757",
"colorNeutralStrokeAccessiblePressed": "#4d4d4d",
"colorNeutralStrokeAccessibleSelected": "#117865",
"colorNeutralStrokeAlpha": "rgba(0, 0, 0, 0.05)",
"colorNeutralStrokeAlpha2": "rgba(255, 255, 255, 0.2)",
"colorNeutralStrokeDisabled": "#e0e0e0",
"colorNeutralStrokeInvertedDisabled": "rgba(255, 255, 255, 0.4)",
"colorNeutralStrokeOnBrand": "#ffffff",
"colorNeutralStrokeOnBrand2": "#ffffff",
"colorNeutralStrokeOnBrand2Hover": "#ffffff",
"colorNeutralStrokeOnBrand2Pressed": "#ffffff",
"colorNeutralStrokeOnBrand2Selected": "#ffffff",
"colorNeutralStrokeSubtle": "#e0e0e0",
"colorPaletteAnchorBackground2": "#bcc3c7",
"colorPaletteAnchorBorderActive": "#394146",
"colorPaletteAnchorForeground2": "#202427",
"colorPaletteBeigeBackground2": "#d7d4d4",
"colorPaletteBeigeBorderActive": "#7a7574",
"colorPaletteBeigeForeground2": "#444241",
"colorPaletteBerryBackground1": "#fdf5fc",
"colorPaletteBerryBackground2": "#edbbe7",
"colorPaletteBerryBackground3": "#c239b3",
"colorPaletteBerryBorder1": "#edbbe7",
"colorPaletteBerryBorder2": "#c239b3",
"colorPaletteBerryBorderActive": "#c239b3",
"colorPaletteBerryForeground1": "#af33a1",
"colorPaletteBerryForeground2": "#6d2064",
"colorPaletteBerryForeground3": "#c239b3",
"colorPaletteBlueBackground2": "#a9d3f2",
"colorPaletteBlueBorderActive": "#0078d4",
"colorPaletteBlueForeground2": "#004377",
"colorPaletteBrassBackground2": "#e0cea2",
"colorPaletteBrassBorderActive": "#986f0b",
"colorPaletteBrassForeground2": "#553e06",
"colorPaletteBrownBackground2": "#ddc3b0",
"colorPaletteBrownBorderActive": "#8e562e",
"colorPaletteBrownForeground2": "#50301a",
"colorPaletteCornflowerBackground2": "#c8d1fa",
"colorPaletteCornflowerBorderActive": "#4f6bed",
"colorPaletteCornflowerForeground2": "#2c3c85",
"colorPaletteCranberryBackground2": "#eeacb2",
"colorPaletteCranberryBorderActive": "#c50f1f",
"colorPaletteCranberryForeground2": "#6e0811",
"colorPaletteDarkGreenBackground2": "#9ad29a",
"colorPaletteDarkGreenBorderActive": "#0b6a0b",
"colorPaletteDarkGreenForeground2": "#063b06",
"colorPaletteDarkOrangeBackground1": "#fdf6f3",
"colorPaletteDarkOrangeBackground2": "#f4bfab",
"colorPaletteDarkOrangeBackground3": "#da3b01",
"colorPaletteDarkOrangeBorder1": "#f4bfab",
"colorPaletteDarkOrangeBorder2": "#da3b01",
"colorPaletteDarkOrangeBorderActive": "#da3b01",
"colorPaletteDarkOrangeForeground1": "#c43501",
"colorPaletteDarkOrangeForeground2": "#7a2101",
"colorPaletteDarkOrangeForeground3": "#da3b01",
"colorPaletteDarkRedBackground2": "#d69ca5",
"colorPaletteDarkRedBorderActive": "#750b1c",
"colorPaletteDarkRedForeground2": "#420610",
"colorPaletteForestBackground2": "#bdd99b",
"colorPaletteForestBorderActive": "#498205",
"colorPaletteForestForeground2": "#294903",
"colorPaletteGoldBackground2": "#ecdfa5",
"colorPaletteGoldBorderActive": "#c19c00",
"colorPaletteGoldForeground2": "#6c5700",
"colorPaletteGrapeBackground2": "#d9a7e0",
"colorPaletteGrapeBorderActive": "#881798",
"colorPaletteGrapeForeground2": "#4c0d55",
"colorPaletteGreenBackground1": "#f1faf1",
"colorPaletteGreenBackground2": "#9fd89f",
"colorPaletteGreenBackground3": "#107c10",
"colorPaletteGreenBorder1": "#9fd89f",
"colorPaletteGreenBorder2": "#107c10",
"colorPaletteGreenBorderActive": "#107c10",
"colorPaletteGreenForeground1": "#0e700e",
"colorPaletteGreenForeground2": "#094509",
"colorPaletteGreenForeground3": "#107c10",
"colorPaletteGreenForegroundInverted": "#359b35",
"colorPaletteLavenderBackground2": "#d2ccf8",
"colorPaletteLavenderBorderActive": "#7160e8",
"colorPaletteLavenderForeground2": "#3f3682",
"colorPaletteLightGreenBackground1": "#f2fbf2",
"colorPaletteLightGreenBackground2": "#a7e3a5",
"colorPaletteLightGreenBackground3": "#13a10e",
"colorPaletteLightGreenBorder1": "#a7e3a5",
"colorPaletteLightGreenBorder2": "#13a10e",
"colorPaletteLightGreenBorderActive": "#13a10e",
"colorPaletteLightGreenForeground1": "#11910d",
"colorPaletteLightGreenForeground2": "#0b5a08",
"colorPaletteLightGreenForeground3": "#13a10e",
"colorPaletteLightTealBackground2": "#a6e9ed",
"colorPaletteLightTealBorderActive": "#00b7c3",
"colorPaletteLightTealForeground2": "#00666d",
"colorPaletteLilacBackground2": "#e6bfed",
"colorPaletteLilacBorderActive": "#b146c2",
"colorPaletteLilacForeground2": "#63276d",
"colorPaletteMagentaBackground2": "#eca5d1",
"colorPaletteMagentaBorderActive": "#bf0077",
"colorPaletteMagentaForeground2": "#6b0043",
"colorPaletteMarigoldBackground1": "#fefbf4",
"colorPaletteMarigoldBackground2": "#f9e2ae",
"colorPaletteMarigoldBackground3": "#eaa300",
"colorPaletteMarigoldBorder1": "#f9e2ae",
"colorPaletteMarigoldBorder2": "#eaa300",
"colorPaletteMarigoldBorderActive": "#eaa300",
"colorPaletteMarigoldForeground1": "#d39300",
"colorPaletteMarigoldForeground2": "#835b00",
"colorPaletteMarigoldForeground3": "#eaa300",
"colorPaletteMinkBackground2": "#cecccb",
"colorPaletteMinkBorderActive": "#5d5a58",
"colorPaletteMinkForeground2": "#343231",
"colorPaletteNavyBackground2": "#a3b2e8",
"colorPaletteNavyBorderActive": "#0027b4",
"colorPaletteNavyForeground2": "#001665",
"colorPalettePeachBackground2": "#ffddb3",
"colorPalettePeachBorderActive": "#ff8c00",
"colorPalettePeachForeground2": "#8f4e00",
"colorPalettePinkBackground2": "#f7c0e3",
"colorPalettePinkBorderActive": "#e43ba6",
"colorPalettePinkForeground2": "#80215d",
"colorPalettePlatinumBackground2": "#cdd6d8",
"colorPalettePlatinumBorderActive": "#69797e",
"colorPalettePlatinumForeground2": "#3b4447",
"colorPalettePlumBackground2": "#d696c0",
"colorPalettePlumBorderActive": "#77004d",
"colorPalettePlumForeground2": "#43002b",
"colorPalettePumpkinBackground2": "#efc4ad",
"colorPalettePumpkinBorderActive": "#ca5010",
"colorPalettePumpkinForeground2": "#712d09",
"colorPalettePurpleBackground2": "#c6b1de",
"colorPalettePurpleBorderActive": "#5c2e91",
"colorPalettePurpleForeground2": "#341a51",
"colorPaletteRedBackground1": "#fdf6f6",
"colorPaletteRedBackground2": "#f1bbbc",
"colorPaletteRedBackground3": "#d13438",
"colorPaletteRedBorder1": "#f1bbbc",
"colorPaletteRedBorder2": "#d13438",
"colorPaletteRedBorderActive": "#d13438",
"colorPaletteRedForeground1": "#bc2f32",
"colorPaletteRedForeground2": "#751d1f",
"colorPaletteRedForeground3": "#d13438",
"colorPaletteRedForegroundInverted": "#dc5e62",
"colorPaletteRoyalBlueBackground2": "#9abfdc",
"colorPaletteRoyalBlueBorderActive": "#004e8c",
"colorPaletteRoyalBlueForeground2": "#002c4e",
"colorPaletteSeafoamBackground2": "#a8f0cd",
"colorPaletteSeafoamBorderActive": "#00cc6a",
"colorPaletteSeafoamForeground2": "#00723b",
"colorPaletteSteelBackground2": "#94c8d4",
"colorPaletteSteelBorderActive": "#005b70",
"colorPaletteSteelForeground2": "#00333f",
"colorPaletteTealBackground2": "#9bd9db",
"colorPaletteTealBorderActive": "#038387",
"colorPaletteTealForeground2": "#02494c",
"colorPaletteYellowBackground1": "#fffef5",
"colorPaletteYellowBackground2": "#fef7b2",
"colorPaletteYellowBackground3": "#fde300",
"colorPaletteYellowBorder1": "#fef7b2",
"colorPaletteYellowBorder2": "#fde300",
"colorPaletteYellowBorderActive": "#fde300",
"colorPaletteYellowForeground1": "#817400",
"colorPaletteYellowForeground2": "#817400",
"colorPaletteYellowForeground3": "#fde300",
"colorPaletteYellowForegroundInverted": "#fef7b2",
"colorScrollbarOverlay": "rgba(0, 0, 0, 0.5)",
"colorStatusDangerBackground1": "#fdf3f4",
"colorStatusDangerBackground2": "#eeacb2",
"colorStatusDangerBackground3": "#c50f1f",
"colorStatusDangerBorder1": "#eeacb2",
"colorStatusDangerBorder2": "#c50f1f",
"colorStatusDangerBorderActive": "#c50f1f",
"colorStatusDangerForeground1": "#b10e1c",
"colorStatusDangerForeground2": "#6e0811",
"colorStatusDangerForeground3": "#c50f1f",
"colorStatusDangerForegroundInverted": "#dc626d",
"colorStatusSuccessBackground1": "#f1faf1",
"colorStatusSuccessBackground2": "#9fd89f",
"colorStatusSuccessBackground3": "#107c10",
"colorStatusSuccessBorder1": "#9fd89f",
"colorStatusSuccessBorder2": "#107c10",
"colorStatusSuccessBorderActive": "#107c10",
"colorStatusSuccessForeground1": "#0e700e",
"colorStatusSuccessForeground2": "#094509",
"colorStatusSuccessForeground3": "#107c10",
"colorStatusSuccessForegroundInverted": "#54b054",
"colorStatusWarningBackground1": "#fff9f5",
"colorStatusWarningBackground2": "#fdcfb4",
"colorStatusWarningBackground3": "#f7630c",
"colorStatusWarningBorder1": "#fdcfb4",
"colorStatusWarningBorder2": "#bc4b09",
"colorStatusWarningBorderActive": "#f7630c",
"colorStatusWarningForeground1": "#bc4b09",
"colorStatusWarningForeground2": "#8a3707",
"colorStatusWarningForeground3": "#bc4b09",
"colorStatusWarningForegroundInverted": "#faa06b",
"colorStrokeFocus1": "#ffffff",
"colorStrokeFocus2": "#000000",
"colorSubtleBackground": "transparent",
"colorSubtleBackgroundHover": "#f5f5f5",
"colorSubtleBackgroundInverted": "transparent",
"colorSubtleBackgroundInvertedHover": "rgba(0, 0, 0, 0.1)",
"colorSubtleBackgroundInvertedPressed": "rgba(0, 0, 0, 0.3)",
"colorSubtleBackgroundInvertedSelected": "rgba(0, 0, 0, 0.2)",
"colorSubtleBackgroundLightAlphaHover": "rgba(255, 255, 255, 0.7)",
"colorSubtleBackgroundLightAlphaPressed": "rgba(255, 255, 255, 0.5)",
"colorSubtleBackgroundLightAlphaSelected": "transparent",
"colorSubtleBackgroundPressed": "#e0e0e0",
"colorSubtleBackgroundSelected": "#ebebeb",
"colorTransparentBackground": "transparent",
"colorTransparentBackgroundHover": "transparent",
"colorTransparentBackgroundPressed": "transparent",
"colorTransparentBackgroundSelected": "transparent",
"colorTransparentStroke": "transparent",
"colorTransparentStrokeDisabled": "transparent",
"colorTransparentStrokeInteractive": "transparent",
"curveAccelerateMax": "cubic-bezier(0.9,0.1,1,0.2)",
"curveAccelerateMid": "cubic-bezier(1,0,1,1)",
"curveAccelerateMin": "cubic-bezier(0.8,0,0.78,1)",
"curveDecelerateMax": "cubic-bezier(0.1,0.9,0.2,1)",
"curveDecelerateMid": "cubic-bezier(0,0,0,1)",
"curveDecelerateMin": "cubic-bezier(0.33,0,0.1,1)",
"curveEasyEase": "cubic-bezier(0.33,0,0.67,1)",
"curveEasyEaseMax": "cubic-bezier(0.8,0,0.2,1)",
"curveLinear": "cubic-bezier(0,0,1,1)",
"durationFast": "150ms",
"durationFaster": "100ms",
"durationGentle": "250ms",
"durationNormal": "200ms",
"durationSlow": "300ms",
"durationSlower": "400ms",
"durationUltraFast": "50ms",
"durationUltraSlow": "500ms",
"fontFamilyBase": "'Segoe UI', 'Segoe UI Web (West European)', -apple-system, BlinkMacSystemFont, Roboto, 'Helvetica Neue', sans-serif",
"fontFamilyMonospace": "Consolas, 'Courier New', Courier, monospace",
"fontFamilyNumeric": "Bahnschrift, 'Segoe UI', 'Segoe UI Web (West European)', -apple-system, BlinkMacSystemFont, Roboto, 'Helvetica Neue', sans-serif",
"fontSizeBase100": "10px",
"fontSizeBase200": "12px",
"fontSizeBase300": "14px",
"fontSizeBase400": "16px",
"fontSizeBase500": "20px",
"fontSizeBase600": "24px",
"fontSizeHero1000": "68px",
"fontSizeHero700": "28px",
"fontSizeHero800": "32px",
"fontSizeHero900": "40px",
"fontWeightBold": 700,
"fontWeightMedium": 500,
"fontWeightRegular": 400,
"fontWeightSemibold": 600,
"lineHeightBase100": "14px",
"lineHeightBase200": "16px",
"lineHeightBase300": "20px",
"lineHeightBase400": "22px",
"lineHeightBase500": "28px",
"lineHeightBase600": "32px",
"lineHeightHero1000": "92px",
"lineHeightHero700": "36px",
"lineHeightHero800": "40px",
"lineHeightHero900": "52px",
"shadow16": "0 0 2px rgba(0,0,0,0.12), 0 8px 16px rgba(0,0,0,0.14)",
"shadow16Brand": "0 0 2px rgba(0,0,0,0.30), 0 8px 16px rgba(0,0,0,0.25)",
"shadow2": "0 0 2px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.14)",
"shadow28": "0 0 8px rgba(0,0,0,0.12), 0 14px 28px rgba(0,0,0,0.14)",
"shadow28Brand": "0 0 8px rgba(0,0,0,0.30), 0 14px 28px rgba(0,0,0,0.25)",
"shadow2Brand": "0 0 2px rgba(0,0,0,0.30), 0 1px 2px rgba(0,0,0,0.25)",
"shadow4": "0 0 2px rgba(0,0,0,0.12), 0 2px 4px rgba(0,0,0,0.14)",
"shadow4Brand": "0 0 2px rgba(0,0,0,0.30), 0 2px 4px rgba(0,0,0,0.25)",
"shadow64": "0 0 8px rgba(0,0,0,0.12), 0 32px 64px rgba(0,0,0,0.14)",
"shadow64Brand": "0 0 8px rgba(0,0,0,0.30), 0 32px 64px rgba(0,0,0,0.25)",
"shadow8": "0 0 2px rgba(0,0,0,0.12), 0 4px 8px rgba(0,0,0,0.14)",
"shadow8Brand": "0 0 2px rgba(0,0,0,0.30), 0 4px 8px rgba(0,0,0,0.25)",
"spacingHorizontalL": "16px",
"spacingHorizontalM": "12px",
"spacingHorizontalMNudge": "10px",
"spacingHorizontalNone": "0",
"spacingHorizontalS": "8px",
"spacingHorizontalSNudge": "6px",
"spacingHorizontalXL": "20px",
"spacingHorizontalXS": "4px",
"spacingHorizontalXXL": "24px",
"spacingHorizontalXXS": "2px",
"spacingHorizontalXXXL": "32px",
"spacingVerticalL": "16px",
"spacingVerticalM": "12px",
"spacingVerticalMNudge": "10px",
"spacingVerticalNone": "0",
"spacingVerticalS": "8px",
"spacingVerticalSNudge": "6px",
"spacingVerticalXL": "20px",
"spacingVerticalXS": "4px",
"spacingVerticalXXL": "24px",
"spacingVerticalXXS": "2px",
"spacingVerticalXXXL": "32px",
"strokeWidthThick": "2px",
"strokeWidthThicker": "3px",
"strokeWidthThickest": "4px",
"strokeWidthThin": "1px",
}
}
>
<div
className="tab-pane active documentsTab"
role="tabpanel"
style={
Object {
"display": "flex",
}
}
>
<div
className="filterdivs"
>
<div
className="filterDocCollapsed"
>
<span
className="selectQuery"
>
SELECT * FROM c
</span>
<span
className="appliedQuery"
/>
<Button
appearance="primary"
onClick={[Function]}
style={
Object {
"marginLeft": 8,
}
}
>
Edit Filter
</Button>
</div>
</div>
<div
style={
Object {
"height": "100%",
"overflow": "hidden",
}
}
>
<Split
mode="horizontal"
prefixCls="w-split"
visiable={true}
>
<div
style={
Object {
"minWidth": 120,
"overflow": "hidden",
"position": "relative",
"width": "35%",
}
}
>
<Button
appearance="transparent"
aria-label="Refresh"
icon={<ArrowClockwise16Filled />}
onClick={[Function]}
onKeyDown={[Function]}
size="small"
style={
Object {
"backgroundColor": "white",
"color": undefined,
"float": "right",
"position": "absolute",
"right": 0,
"top": 6,
"zIndex": 1,
}
}
/>
<div
style={
Object {
"height": "100%",
"width": "calc(100% - 50px)",
}
}
>
<DocumentsTableComponent
columnHeaders={
Object {
"idHeader": "id",
"partitionKeyHeaders": Array [],
}
}
isSelectionDisabled={true}
items={Array []}
onItemClicked={[Function]}
onSelectedRowsChange={[Function]}
selectedRows={
Set {
0,
}
}
/>
</div>
</div>
<div
style={
Object {
"minWidth": "20%",
"width": "100%",
}
}
/>
</Split>
</div>
</div>
</FluentProvider>
`;

View File

@@ -1,84 +0,0 @@
import { selectionHelper } from "Explorer/Tabs/DocumentsTabV2/SelectionHelper";
describe("Selection helper", () => {
describe("when shift:off", () => {
it("ctrl:off: should return clicked items and update selection start", () => {
const currentSelection = new Set<number>([1, 2, 3]);
const clickedIndex = 4;
const isShiftKey = false;
const isCtrlKey = false;
const selectionStartIndex = 1;
const result = selectionHelper(currentSelection, clickedIndex, isShiftKey, isCtrlKey, selectionStartIndex);
expect(result.selection).toEqual(new Set<number>([4]));
expect(result.selectionStartIndex).toEqual(4);
});
it("ctrl:on: should turn on selection and update selection start on not selected item", () => {
const currentSelection = new Set<number>([1, 3]);
const clickedIndex = 2;
const isShiftKey = false;
const isCtrlKey = true;
const selectionStartIndex = 1;
const result = selectionHelper(currentSelection, clickedIndex, isShiftKey, isCtrlKey, selectionStartIndex);
expect(result.selection).toEqual(new Set<number>([1, 2, 3]));
expect(result.selectionStartIndex).toEqual(2);
});
it("ctrl:on: should turn off selection and update selection start on selected item", () => {
const currentSelection = new Set<number>([1, 2, 3]);
const clickedIndex = 2;
const isShiftKey = false;
const isCtrlKey = true;
const selectionStartIndex = 1;
const result = selectionHelper(currentSelection, clickedIndex, isShiftKey, isCtrlKey, selectionStartIndex);
expect(result.selection).toEqual(new Set<number>([1, 3]));
expect(result.selectionStartIndex).toEqual(2);
});
});
describe("when shift:on", () => {
it("ctrl:off: should only select between selection start and clicked index (selection start < clicked index)", () => {
const currentSelection = new Set<number>([7, 8, 10]);
const clickedIndex = 9;
const isShiftKey = true;
const isCtrlKey = false;
const selectionStartIndex = 5;
const result = selectionHelper(currentSelection, clickedIndex, isShiftKey, isCtrlKey, selectionStartIndex);
expect(result.selection).toEqual(new Set<number>([5, 6, 7, 8, 9]));
expect(result.selectionStartIndex).toEqual(undefined);
});
it("ctrl:off: should only select between selection start and clicked index (selection start > clicked index)", () => {
const currentSelection = new Set<number>([4, 6, 8]);
const clickedIndex = 2;
const isShiftKey = true;
const isCtrlKey = false;
const selectionStartIndex = 5;
const result = selectionHelper(currentSelection, clickedIndex, isShiftKey, isCtrlKey, selectionStartIndex);
expect(result.selection).toEqual(new Set<number>([2, 3, 4, 5]));
expect(result.selectionStartIndex).toEqual(undefined);
});
it("ctrl:on: selection start on selected item should keep current selection and select range, and not update selection start", () => {
const currentSelection = new Set<number>([1, 4, 5, 7]);
const clickedIndex = 9;
const isShiftKey = true;
const isCtrlKey = true;
const selectionStartIndex = 5;
const result = selectionHelper(currentSelection, clickedIndex, isShiftKey, isCtrlKey, selectionStartIndex);
expect(result.selection).toEqual(new Set<number>([1, 4, 5, 6, 7, 8, 9]));
expect(result.selectionStartIndex).toEqual(undefined);
});
it("ctrl:on: selection start on deselected item should deselect range, and not update selection start", () => {
const currentSelection = new Set<number>([1, 4, 6, 7, 10]);
const clickedIndex = 9;
const isShiftKey = true;
const isCtrlKey = true;
const selectionStartIndex = 5;
const result = selectionHelper(currentSelection, clickedIndex, isShiftKey, isCtrlKey, selectionStartIndex);
expect(result.selection).toEqual(new Set<number>([1, 4, 10]));
expect(result.selectionStartIndex).toEqual(undefined);
});
});
});

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