diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 15c555e31..e285f87d1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -92,7 +92,7 @@ jobs: NODE_OPTIONS: "--max-old-space-size=4096" - run: cp -r ./Contracts ./dist/contracts - run: cp -r ./configs ./dist/configs - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: name: dist path: dist/ @@ -117,14 +117,14 @@ jobs: with: nuget-api-key: ${{ secrets.NUGET_API_KEY }} - name: Download Dist Folder - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: dist - run: cp ./configs/prod.json config.json - run: nuget sources add -Name "ADO" -Source "$NUGET_SOURCE" -UserName "jawelton@microsoft.com" -Password "$AZURE_DEVOPS_PAT" - run: nuget pack -Version "2.0.0-github-${GITHUB_SHA}" - run: nuget push -SkipDuplicate -Source "$NUGET_SOURCE" -ApiKey Az *.nupkg - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 name: packages with: path: "*.nupkg" @@ -141,7 +141,7 @@ jobs: with: nuget-api-key: ${{ secrets.NUGET_API_KEY }} - name: Download Dist Folder - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: dist - run: cp ./configs/mpac.json config.json @@ -149,7 +149,7 @@ jobs: - run: nuget sources add -Name "ADO" -Source "$NUGET_SOURCE" -UserName "jawelton@microsoft.com" -Password "$AZURE_DEVOPS_PAT" - run: nuget pack -Version "2.0.0-github-${GITHUB_SHA}" - run: nuget push -SkipDuplicate -Source "$NUGET_SOURCE" -ApiKey Az *.nupkg - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 name: packages with: path: "*.nupkg" diff --git a/jest.config.js b/jest.config.js index 5a1b31550..d265f685d 100644 --- a/jest.config.js +++ b/jest.config.js @@ -174,7 +174,11 @@ module.exports = { }, // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation - transformIgnorePatterns: ["/node_modules/(?!@fluentui/react-icons)", "/externals/"], + transformIgnorePatterns: [ + "/node_modules/(?!@fluentui/react-icons|(.*)/dist/browser)/", + "/node_modules/plotly.js-cartesian-dist-min", + "/externals/", + ], // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them // unmockedModulePathPatterns: undefined, diff --git a/less/documentDB.less b/less/documentDB.less index 1abbc9b30..acce65d6a 100644 --- a/less/documentDB.less +++ b/less/documentDB.less @@ -3117,3 +3117,7 @@ a:link { background: white; height: 100%; } + +.sidebarContainer { + height: 100%; +} diff --git a/less/documentDBFabric.less b/less/documentDBFabric.less index 5f89f72ea..900535700 100644 --- a/less/documentDBFabric.less +++ b/less/documentDBFabric.less @@ -20,6 +20,10 @@ a:focus { text-decoration: underline; } +.splashLoaderContainer { + background-color: #f5f5f5; +} + #divExplorer { background-color: #f5f5f5; } @@ -27,26 +31,24 @@ a:focus { .resourceTreeAndTabs { border-radius: 0px; box-shadow: @FabricBoxBorderShadow; - margin: @FabricBoxMargin; margin-top: 0px; margin-bottom: 0px; background-color: #ffffff; } .tabsManagerContainer { - background-color: #ffffff + background-color: #ffffff; } .nav-tabs-margin { padding-top: 5px; - background-color: #ffffff + background-color: #ffffff; } .commandBarContainer { background-color: #ffffff; border-radius: @FabricBoxBorderRadius @FabricBoxBorderRadius 0px 0px; box-shadow: @FabricBoxBorderShadow; - margin: @FabricBoxMargin; margin-top: 0px; margin-bottom: 0px; padding-top: 2px; @@ -65,17 +67,16 @@ a:focus { } } - -.nav-tabs>li>.tabNavContentContainer>.tab_Content:hover { +.nav-tabs > li > .tabNavContentContainer > .tab_Content:hover { border-bottom: 2px solid #e0e0e0; } -.nav-tabs>li.active>.tabNavContentContainer>.tab_Content, -.nav-tabs>li.active>.tabNavContentContainer>.tab_Content:hover { +.nav-tabs > li.active > .tabNavContentContainer > .tab_Content, +.nav-tabs > li.active > .tabNavContentContainer > .tab_Content:hover { border-bottom: 2px solid @FabricAccentMedium; } -.nav-tabs>li.active>.tabNavContentContainer>.tab_Content>.contentWrapper>.tabNavText { +.nav-tabs > li.active > .tabNavContentContainer > .tab_Content > .contentWrapper > .tabNavText { border-bottom: 0px none transparent; } @@ -94,10 +95,10 @@ a:focus { padding-bottom: @SmallSpace; .contentWrapper { - .statusIconContainer { - margin-left: 0px; - } + .statusIconContainer { + margin-left: 0px; } + } .tabIconSection { .cancelButton { @@ -119,7 +120,6 @@ a:focus { } } - .resourceTree { padding: 12px; } @@ -156,25 +156,21 @@ a:focus { } .selected { - &>.treeNodeHeader { + & > .treeNodeHeader { background-color: @FabricAccentExtra; } } } } - .dataExplorerErrorConsoleContainer { - border-radius: 0px 0px @FabricBoxBorderRadius @FabricBoxBorderRadius; + border-radius: 0px 0px @FabricBoxBorderRadius @FabricBoxBorderRadius; box-shadow: @FabricBoxBorderShadow; - margin: @FabricBoxMargin; margin-top: 0px; width: auto; align-self: auto; } - - .filterbtnstyle { background: #fff; color: #000; @@ -200,12 +196,10 @@ a:focus { border: solid 1px #d1d1d1; } - .gridRowSelected .tabdocumentsGridElement:hover { background-color: @FabricAccentLight !important; } - .refreshcol { filter: brightness(0) saturate(100%); } @@ -216,4 +210,4 @@ a:focus { .fileImportImg img { filter: brightness(0) saturate(100%); -} \ No newline at end of file +} diff --git a/package-lock.json b/package-lock.json index db0aa9b88..c05feec0a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "hasInstallScript": true, "dependencies": { "@azure/arm-cosmosdb": "9.1.0", - "@azure/cosmos": "4.0.1-beta.3", + "@azure/cosmos": "4.2.0-beta.1", "@azure/cosmos-language-service": "0.0.5", "@azure/identity": "1.5.2", "@azure/ms-rest-nodeauth": "3.1.1", @@ -228,18 +228,20 @@ } }, "node_modules/@azure/abort-controller": { - "version": "1.1.0", - "license": "MIT", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", "dependencies": { - "tslib": "^2.2.0" + "tslib": "^2.6.2" }, "engines": { - "node": ">=12.0.0" + "node": ">=18.0.0" } }, "node_modules/@azure/abort-controller/node_modules/tslib": { - "version": "2.6.2", - "license": "0BSD" + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" }, "node_modules/@azure/arm-cosmosdb": { "version": "9.1.0", @@ -251,15 +253,16 @@ } }, "node_modules/@azure/core-auth": { - "version": "1.5.0", - "license": "MIT", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.9.0.tgz", + "integrity": "sha512-FPwHpZywuyasDSLMqJ6fhbOK3TqUdviZNF8OqRGA4W5Ewib2lEEZ+pBsYcBa88B2NGO/SEnYPGhyBqNlE8ilSw==", "dependencies": { - "@azure/abort-controller": "^1.0.0", - "@azure/core-util": "^1.1.0", - "tslib": "^2.2.0" + "@azure/abort-controller": "^2.0.0", + "@azure/core-util": "^1.11.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=18.0.0" } }, "node_modules/@azure/core-auth/node_modules/tslib": { @@ -282,36 +285,61 @@ "node": ">=18.0.0" } }, - "node_modules/@azure/core-client/node_modules/@azure/abort-controller": { - "version": "2.1.2", - "license": "MIT", + "node_modules/@azure/core-client/node_modules/tslib": { + "version": "2.6.2", + "license": "0BSD" + }, + "node_modules/@azure/core-rest-pipeline": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.18.0.tgz", + "integrity": "sha512-QSoGUp4Eq/gohEFNJaUOwTN7BCc2nHTjjbm75JT0aD7W65PWM1H/tItz0GsABn22uaKyGxiMhWQLt2r+FGU89Q==", "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.8.0", + "@azure/core-tracing": "^1.0.1", + "@azure/core-util": "^1.11.0", + "@azure/logger": "^1.0.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@azure/core-client/node_modules/tslib": { - "version": "2.6.2", - "license": "0BSD" - }, - "node_modules/@azure/core-rest-pipeline": { - "version": "1.12.2", - "license": "MIT", + "node_modules/@azure/core-rest-pipeline/node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", "dependencies": { - "@azure/abort-controller": "^1.0.0", - "@azure/core-auth": "^1.4.0", - "@azure/core-tracing": "^1.0.1", - "@azure/core-util": "^1.3.0", - "@azure/logger": "^1.0.0", - "form-data": "^4.0.0", - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.0", - "tslib": "^2.2.0" + "debug": "^4.3.4" }, "engines": { - "node": ">=16.0.0" + "node": ">= 14" + } + }, + "node_modules/@azure/core-rest-pipeline/node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@azure/core-rest-pipeline/node_modules/https-proxy-agent": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" } }, "node_modules/@azure/core-rest-pipeline/node_modules/tslib": { @@ -319,13 +347,14 @@ "license": "0BSD" }, "node_modules/@azure/core-tracing": { - "version": "1.0.1", - "license": "MIT", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.2.0.tgz", + "integrity": "sha512-UKTiEJPkWcESPYJz3X5uKRYyOcJD+4nYph+KpfdPRnQJVrZfk0KJgdnaAWKfhsBBtAf/D58Az4AvCJEmWgIBAg==", "dependencies": { - "tslib": "^2.2.0" + "tslib": "^2.6.2" }, "engines": { - "node": ">=12.0.0" + "node": ">=18.0.0" } }, "node_modules/@azure/core-tracing/node_modules/tslib": { @@ -333,14 +362,15 @@ "license": "0BSD" }, "node_modules/@azure/core-util": { - "version": "1.6.1", - "license": "MIT", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.11.0.tgz", + "integrity": "sha512-DxOSLua+NdpWoSqULhjDyAZTXFdP/LKkqtYuxxz1SCN289zk3OG8UOpnCQAz/tygyACBtWp/BoO72ptK7msY8g==", "dependencies": { - "@azure/abort-controller": "^1.0.0", - "tslib": "^2.2.0" + "@azure/abort-controller": "^2.0.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, "node_modules/@azure/core-util/node_modules/tslib": { @@ -348,22 +378,20 @@ "license": "0BSD" }, "node_modules/@azure/cosmos": { - "version": "4.0.1-beta.3", - "license": "MIT", + "version": "4.2.0-beta.1", + "resolved": "https://registry.npmjs.org/@azure/cosmos/-/cosmos-4.2.0-beta.1.tgz", + "integrity": "sha512-mREONehm1DxjEKXGaNU6Wmpf9Ckb9IrhKFXhDFVs45pxmoEb3y2s/Ub0owuFmqlphpcS1zgtYQn5exn+lwnJuQ==", "dependencies": { - "@azure/abort-controller": "^1.0.0", - "@azure/core-auth": "^1.3.0", - "@azure/core-rest-pipeline": "^1.2.0", - "@azure/core-tracing": "^1.0.0", - "debug": "^4.1.1", + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.7.1", + "@azure/core-rest-pipeline": "^1.15.1", + "@azure/core-tracing": "^1.1.1", + "@azure/core-util": "^1.8.1", "fast-json-stable-stringify": "^2.1.0", - "jsbi": "^3.1.3", - "node-abort-controller": "^3.0.0", + "jsbi": "^4.3.0", "priorityqueuejs": "^2.0.0", - "semaphore": "^1.0.5", - "tslib": "^2.2.0", - "universal-user-agent": "^6.0.0", - "uuid": "^8.3.0" + "semaphore": "^1.1.0", + "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" @@ -11708,6 +11736,7 @@ }, "node_modules/@tootallnate/once": { "version": "2.0.0", + "dev": true, "license": "MIT", "engines": { "node": ">= 10" @@ -19895,6 +19924,7 @@ }, "node_modules/form-data": { "version": "4.0.0", + "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -21095,6 +21125,7 @@ }, "node_modules/http-proxy-agent": { "version": "5.0.0", + "dev": true, "license": "MIT", "dependencies": { "@tootallnate/once": "2", @@ -27067,8 +27098,9 @@ } }, "node_modules/jsbi": { - "version": "3.2.5", - "license": "Apache-2.0" + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/jsbi/-/jsbi-4.3.0.tgz", + "integrity": "sha512-SnZNcinB4RIcnEyZqFPdGPVgrg2AcnykiBy0sHVJQKHYeaLUvi3Exj+iaPpLnFVkDPZIV4U0yvgC9/R4uEAZ9g==" }, "node_modules/jsbn": { "version": "0.1.1", @@ -29737,7 +29769,9 @@ }, "node_modules/node-abort-controller": { "version": "3.1.1", - "license": "MIT" + "dev": true, + "license": "MIT", + "peer": true }, "node_modules/node-addon-api": { "version": "4.3.0", diff --git a/package.json b/package.json index d174a7674..755254b2b 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "main": "index.js", "dependencies": { "@azure/arm-cosmosdb": "9.1.0", - "@azure/cosmos": "4.0.1-beta.3", + "@azure/cosmos": "4.2.0-beta.1", "@azure/cosmos-language-service": "0.0.5", "@azure/identity": "1.5.2", "@azure/ms-rest-nodeauth": "3.1.1", @@ -247,4 +247,4 @@ "printWidth": 120, "endOfLine": "auto" } -} \ No newline at end of file +} diff --git a/src/Common/Constants.ts b/src/Common/Constants.ts index 23155cbdf..0c8252509 100644 --- a/src/Common/Constants.ts +++ b/src/Common/Constants.ts @@ -89,6 +89,7 @@ export class CapabilityNames { public static readonly EnableMongo: string = "EnableMongo"; public static readonly EnableServerless: string = "EnableServerless"; public static readonly EnableNoSQLVectorSearch: string = "EnableNoSQLVectorSearch"; + public static readonly EnableNoSQLFullTextSearch: string = "EnableNoSQLFullTextSearch"; } export enum CapacityMode { @@ -158,7 +159,7 @@ export class MongoProxyEndpoints { export class MongoProxyApi { public static readonly ResourceList: string = "ResourceList"; public static readonly QueryDocuments: string = "QueryDocuments"; - public static readonly CreateDocument: string = "CreateDocumen"; + public static readonly CreateDocument: string = "CreateDocument"; public static readonly ReadDocument: string = "ReadDocument"; public static readonly UpdateDocument: string = "UpdateDocument"; public static readonly DeleteDocument: string = "DeleteDocument"; diff --git a/src/Common/MongoProxyClient.test.ts b/src/Common/MongoProxyClient.test.ts index cda6eb882..a63190651 100644 --- a/src/Common/MongoProxyClient.test.ts +++ b/src/Common/MongoProxyClient.test.ts @@ -73,6 +73,7 @@ describe("MongoProxyClient", () => { }); updateConfigContext({ MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod, + globallyEnabledMongoAPIs: [], }); window.fetch = jest.fn().mockImplementation(fetchMock); }); @@ -89,7 +90,10 @@ describe("MongoProxyClient", () => { }); it("builds the correct proxy URL in development", () => { - updateConfigContext({ MONGO_PROXY_ENDPOINT: "https://localhost:1234" }); + updateConfigContext({ + MONGO_PROXY_ENDPOINT: "https://localhost:1234", + globallyEnabledMongoAPIs: [], + }); queryDocuments(databaseId, collection, true, "{}"); expect(window.fetch).toHaveBeenCalledWith( `${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer/resourcelist`, @@ -105,6 +109,7 @@ describe("MongoProxyClient", () => { }); updateConfigContext({ MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod, + globallyEnabledMongoAPIs: [], }); window.fetch = jest.fn().mockImplementation(fetchMock); }); @@ -121,7 +126,10 @@ describe("MongoProxyClient", () => { }); it("builds the correct proxy URL in development", () => { - updateConfigContext({ MONGO_PROXY_ENDPOINT: "https://localhost:1234" }); + updateConfigContext({ + MONGO_PROXY_ENDPOINT: "https://localhost:1234", + globallyEnabledMongoAPIs: [], + }); readDocument(databaseId, collection, documentId); expect(window.fetch).toHaveBeenCalledWith( `${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`, @@ -137,6 +145,7 @@ describe("MongoProxyClient", () => { }); updateConfigContext({ MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod, + globallyEnabledMongoAPIs: [], }); window.fetch = jest.fn().mockImplementation(fetchMock); }); @@ -153,7 +162,10 @@ describe("MongoProxyClient", () => { }); it("builds the correct proxy URL in development", () => { - updateConfigContext({ MONGO_PROXY_ENDPOINT: "https://localhost:1234" }); + updateConfigContext({ + MONGO_PROXY_ENDPOINT: "https://localhost:1234", + globallyEnabledMongoAPIs: [], + }); readDocument(databaseId, collection, documentId); expect(window.fetch).toHaveBeenCalledWith( `${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`, @@ -169,6 +181,7 @@ describe("MongoProxyClient", () => { }); updateConfigContext({ MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod, + globallyEnabledMongoAPIs: [], }); window.fetch = jest.fn().mockImplementation(fetchMock); }); @@ -185,7 +198,10 @@ describe("MongoProxyClient", () => { }); it("builds the correct proxy URL in development", () => { - updateConfigContext({ MONGO_BACKEND_ENDPOINT: "https://localhost:1234" }); + updateConfigContext({ + MONGO_BACKEND_ENDPOINT: "https://localhost:1234", + globallyEnabledMongoAPIs: [], + }); updateDocument(databaseId, collection, documentId, "{}"); expect(window.fetch).toHaveBeenCalledWith( `${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`, @@ -201,6 +217,7 @@ describe("MongoProxyClient", () => { }); updateConfigContext({ MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod, + globallyEnabledMongoAPIs: [], }); window.fetch = jest.fn().mockImplementation(fetchMock); }); @@ -217,7 +234,10 @@ describe("MongoProxyClient", () => { }); it("builds the correct proxy URL in development", () => { - updateConfigContext({ MONGO_PROXY_ENDPOINT: "https://localhost:1234" }); + updateConfigContext({ + MONGO_PROXY_ENDPOINT: "https://localhost:1234", + globallyEnabledMongoAPIs: [], + }); deleteDocument(databaseId, collection, documentId); expect(window.fetch).toHaveBeenCalledWith( `${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`, @@ -233,6 +253,7 @@ describe("MongoProxyClient", () => { }); updateConfigContext({ MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod, + globallyEnabledMongoAPIs: [], }); }); @@ -260,6 +281,7 @@ describe("MongoProxyClient", () => { resetConfigContext(); updateConfigContext({ MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod, + globallyEnabledMongoAPIs: [], }); const params = new URLSearchParams({ "feature.mongoProxyEndpoint": MongoProxyEndpoints.Prod, diff --git a/src/Common/MongoProxyClient.ts b/src/Common/MongoProxyClient.ts index c0e391e0f..0513d48cc 100644 --- a/src/Common/MongoProxyClient.ts +++ b/src/Common/MongoProxyClient.ts @@ -689,13 +689,13 @@ export function createMongoCollectionWithProxy_ToBeDeprecated( } export function getFeatureEndpointOrDefault(feature: string): string { let endpoint; - const allowedMongoProxyEndpoints = configContext.allowedMongoProxyEndpoints || [ - ...defaultAllowedMongoProxyEndpoints, - ...allowedMongoProxyEndpoints_ToBeDeprecated, - ]; if (useMongoProxyEndpoint(feature)) { endpoint = configContext.MONGO_PROXY_ENDPOINT; } else { + const allowedMongoProxyEndpoints = configContext.allowedMongoProxyEndpoints || [ + ...defaultAllowedMongoProxyEndpoints, + ...allowedMongoProxyEndpoints_ToBeDeprecated, + ]; endpoint = hasFlag(userContext.features.mongoProxyAPIs, feature) && validateEndpoint(userContext.features.mongoProxyEndpoint, allowedMongoProxyEndpoints) @@ -790,6 +790,10 @@ export function useMongoProxyEndpoint(mongoProxyApi: string): boolean { return false; } + if (configContext.globallyEnabledMongoAPIs.includes(mongoProxyApi)) { + return true; + } + return mongoProxyEnvironmentMap[mongoProxyApi].includes(configContext.MONGO_PROXY_ENDPOINT); } diff --git a/src/Common/QueryError.test.ts b/src/Common/QueryError.test.ts index 2eea29a62..7924c398d 100644 --- a/src/Common/QueryError.test.ts +++ b/src/Common/QueryError.test.ts @@ -36,7 +36,7 @@ describe("QueryError.tryParse", () => { code: "BadRequest", message: "Your query is bad, and you should feel bad", }; - const message = JSON.stringify(innerError); + const message = `Message: ${JSON.stringify(innerError)}\r\nActivity ID: 42`; const outerError = { code: "BadRequest", message, @@ -48,7 +48,7 @@ describe("QueryError.tryParse", () => { ]); }); - // Imitate the value coming from the backend, which has the syntax errors serialized as JSON in the message. + // Imitate the value coming from the backend, which has the syntax errors serialized as JSON in the message, along with a prefix and activity id. it("handles single-nested error", () => { const errors = [ { @@ -69,7 +69,7 @@ describe("QueryError.tryParse", () => { message: "Your query is bad, and you should feel bad", errors, }; - const message = JSON.stringify(innerError); + const message = `Message: ${JSON.stringify(innerError)}\r\nActivity ID: 42`; const outerError = { code: "BadRequest", message, @@ -91,4 +91,23 @@ describe("QueryError.tryParse", () => { ), ]); }); + + // Imitate another value we've gotten from the backend, which has a doubly-nested JSON payload. + it("handles double-nested error", () => { + const outerError = { + code: "BadRequest", + message: + '{"code":"BadRequest","message":"{\\"errors\\":[{\\"severity\\":\\"Error\\",\\"location\\":{\\"start\\":7,\\"end\\":18},\\"code\\":\\"SC2005\\",\\"message\\":\\"\'nonexistent\' is not a recognized built-in function name.\\"}]}\\r\\nActivityId: aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa, Windows/10.0.20348 cosmos-netstandard-sdk/3.18.0"}', + }; + + const result = QueryError.tryParse(outerError, testErrorLocationResolver); + expect(result).toEqual([ + new QueryError( + "'nonexistent' is not a recognized built-in function name.", + QueryErrorSeverity.Error, + "SC2005", + new QueryErrorLocation({ offset: 7, lineNumber: 7, column: 7 }, { offset: 18, lineNumber: 18, column: 18 }), + ), + ]); + }); }); diff --git a/src/Common/QueryError.ts b/src/Common/QueryError.ts index 51748d1a8..c1bab1c02 100644 --- a/src/Common/QueryError.ts +++ b/src/Common/QueryError.ts @@ -214,16 +214,28 @@ export default class QueryError { return null; } - // Assign to a new variable because of a TypeScript flow typing quirk, see below. - if (message.startsWith("Message: ")) { - // Reassigning this to 'error' restores the original type of 'error', which is 'unknown'. - // So we use a separate variable to avoid this. - message = message.substring("Message: ".length); + // Some newer backends produce a message that contains a doubly-nested JSON payload. + // In this case, the message we get is a fully-complete JSON object we can parse. + // So let's try that first + if (message.startsWith("{") && message.endsWith("}")) { + let outer: unknown = undefined; + try { + outer = JSON.parse(message); + if (typeof outer === "object" && "message" in outer && typeof outer.message === "string") { + message = outer.message; + } + } catch (e) { + // Just continue if the parsing fails. We'll use the fallback logic below. + } } const lines = message.split("\n"); message = lines[0].trim(); + if (message.startsWith("Message: ")) { + message = message.substring("Message: ".length); + } + let parsed: unknown; try { parsed = JSON.parse(message); diff --git a/src/Common/dataAccess/createCollection.ts b/src/Common/dataAccess/createCollection.ts index b64a3add5..b5afd70a3 100644 --- a/src/Common/dataAccess/createCollection.ts +++ b/src/Common/dataAccess/createCollection.ts @@ -99,6 +99,9 @@ const createSqlContainer = async (params: DataModels.CreateCollectionParams): Pr if (params.vectorEmbeddingPolicy) { resource.vectorEmbeddingPolicy = params.vectorEmbeddingPolicy; } + if (params.fullTextPolicy) { + resource.fullTextPolicy = params.fullTextPolicy; + } const rpPayload: ARMTypes.SqlDatabaseCreateUpdateParameters = { properties: { @@ -270,6 +273,7 @@ const createCollectionWithSDK = async (params: DataModels.CreateCollectionParams uniqueKeyPolicy: params.uniqueKeyPolicy || undefined, analyticalStorageTtl: params.analyticalStorageTtl, vectorEmbeddingPolicy: params.vectorEmbeddingPolicy, + fullTextPolicy: params.fullTextPolicy, } as ContainerRequest; // TODO: remove cast when https://github.com/Azure/azure-cosmos-js/issues/423 is fixed const collectionOptions: RequestOptions = {}; const createDatabaseBody: DatabaseRequest = { id: params.databaseId }; diff --git a/src/ConfigContext.ts b/src/ConfigContext.ts index 931893be4..113a6d5e5 100644 --- a/src/ConfigContext.ts +++ b/src/ConfigContext.ts @@ -67,6 +67,8 @@ export interface ConfigContext { hostedExplorerURL: string; armAPIVersion?: string; msalRedirectURI?: string; + globallyEnabledCassandraAPIs?: string[]; + globallyEnabledMongoAPIs?: string[]; } // Default configuration @@ -114,6 +116,8 @@ let configContext: Readonly = { NEW_CASSANDRA_APIS: ["postQuery", "createOrDelete", "getKeys", "getSchema"], isTerminalEnabled: false, isPhoenixEnabled: false, + globallyEnabledCassandraAPIs: [], + globallyEnabledMongoAPIs: [], }; export function resetConfigContext(): void { diff --git a/src/Contracts/DataModels.ts b/src/Contracts/DataModels.ts index 3f7e3a7db..da6ddd28d 100644 --- a/src/Contracts/DataModels.ts +++ b/src/Contracts/DataModels.ts @@ -159,6 +159,7 @@ export interface Collection extends Resource { analyticalStorageTtl?: number; geospatialConfig?: GeospatialConfig; vectorEmbeddingPolicy?: VectorEmbeddingPolicy; + fullTextPolicy?: FullTextPolicy; schema?: ISchema; requestSchema?: () => void; computedProperties?: ComputedProperties; @@ -199,11 +200,19 @@ export interface IndexingPolicy { compositeIndexes?: any[]; spatialIndexes?: any[]; vectorIndexes?: VectorIndex[]; + fullTextIndexes?: FullTextIndex[]; } export interface VectorIndex { path: string; type: "flat" | "diskANN" | "quantizedFlat"; + diskANNShardKey?: string; + indexingSearchListSize?: number; + quantizationByteSize?: number; +} + +export interface FullTextIndex { + path: string; } export interface ComputedProperty { @@ -342,6 +351,7 @@ export interface CreateCollectionParams { uniqueKeyPolicy?: UniqueKeyPolicy; createMongoWildcardIndex?: boolean; vectorEmbeddingPolicy?: VectorEmbeddingPolicy; + fullTextPolicy?: FullTextPolicy; } export interface VectorEmbeddingPolicy { @@ -355,6 +365,16 @@ export interface VectorEmbedding { path: string; } +export interface FullTextPolicy { + defaultLanguage: string; + fullTextPaths: FullTextPath[]; +} + +export interface FullTextPath { + path: string; + language: string; +} + export interface ReadDatabaseOfferParams { databaseId: string; databaseResourceId?: string; diff --git a/src/Contracts/ViewModels.ts b/src/Contracts/ViewModels.ts index f8c6d885c..b30435dc6 100644 --- a/src/Contracts/ViewModels.ts +++ b/src/Contracts/ViewModels.ts @@ -126,6 +126,8 @@ export interface Collection extends CollectionBase { analyticalStorageTtl: ko.Observable; schema?: DataModels.ISchema; requestSchema?: () => void; + vectorEmbeddingPolicy: ko.Observable; + fullTextPolicy: ko.Observable; indexingPolicy: ko.Observable; uniqueKeyPolicy: DataModels.UniqueKeyPolicy; usageSizeInKB: ko.Observable; @@ -381,8 +383,9 @@ export enum TerminalKind { export interface DataExplorerInputsFrame { databaseAccount: any; subscriptionId?: string; - tenantId?: string; resourceGroup?: string; + tenantId?: string; + userName?: string; masterKey?: string; hasWriteAccess?: boolean; authorizationToken?: string; diff --git a/src/Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent.tsx b/src/Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent.tsx index 80da43414..3f0fa6d2c 100644 --- a/src/Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent.tsx +++ b/src/Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent.tsx @@ -1,4 +1,4 @@ -import { DirectionalHint, Icon, Label, Stack, TooltipHost } from "@fluentui/react"; +import { DirectionalHint, Icon, IconButton, Label, Stack, TooltipHost } from "@fluentui/react"; import * as React from "react"; import { NormalizedEventKey } from "../../../Common/Constants"; import { accordionStackTokens } from "../Settings/SettingsRenderUtils"; @@ -9,6 +9,9 @@ export interface CollapsibleSectionProps { onExpand?: () => void; children: JSX.Element; tooltipContent?: string | JSX.Element | JSX.Element[]; + showDelete?: boolean; + onDelete?: () => void; + disabled?: boolean; } export interface CollapsibleSectionState { @@ -69,6 +72,20 @@ export class CollapsibleSectionComponent extends React.Component )} + {this.props.showDelete && ( + + { + event.stopPropagation(); + this.props.onDelete(); + }} + /> + + )} {this.state.isExpanded && this.props.children} diff --git a/src/Explorer/Controls/FullTextSeach/FullTextPoliciesComponent.test.tsx b/src/Explorer/Controls/FullTextSeach/FullTextPoliciesComponent.test.tsx new file mode 100644 index 000000000..91a2efd7f --- /dev/null +++ b/src/Explorer/Controls/FullTextSeach/FullTextPoliciesComponent.test.tsx @@ -0,0 +1,6 @@ +import "@testing-library/jest-dom"; + +describe("AddFullTextPolicyForm", () => { + //CTODO: add tests + it.skip("should render correctly", () => {}); +}); diff --git a/src/Explorer/Controls/FullTextSeach/FullTextPoliciesComponent.tsx b/src/Explorer/Controls/FullTextSeach/FullTextPoliciesComponent.tsx new file mode 100644 index 000000000..8972791ce --- /dev/null +++ b/src/Explorer/Controls/FullTextSeach/FullTextPoliciesComponent.tsx @@ -0,0 +1,239 @@ +import { + DefaultButton, + Dropdown, + IDropdownOption, + IStyleFunctionOrObject, + ITextFieldStyleProps, + ITextFieldStyles, + Label, + Stack, + TextField, +} from "@fluentui/react"; +import { FullTextIndex, FullTextPath, FullTextPolicy } from "Contracts/DataModels"; +import { CollapsibleSectionComponent } from "Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent"; +import * as React from "react"; + +export interface FullTextPoliciesComponentProps { + fullTextPolicy: FullTextPolicy; + onFullTextPathChange: ( + fullTextPolicy: FullTextPolicy, + fullTextIndexes: FullTextIndex[], + validationPassed: boolean, + ) => void; + discardChanges?: boolean; + onChangesDiscarded?: () => void; +} + +export interface FullTextPolicyData { + path: string; + language: string; + pathError: string; +} + +const labelStyles = { + root: { + fontSize: 12, + }, +}; + +const textFieldStyles: IStyleFunctionOrObject = { + 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 FullTextPoliciesComponent: React.FunctionComponent = ({ + fullTextPolicy, + onFullTextPathChange, + discardChanges, + onChangesDiscarded, +}): JSX.Element => { + const getFullTextPathError = (path: string, index?: number): string => { + let error = ""; + if (!path) { + error = "Full text path should not be empty"; + } + if ( + index >= 0 && + fullTextPathData?.find( + (fullTextPath: FullTextPolicyData, dataIndex: number) => dataIndex !== index && fullTextPath.path === path, + ) + ) { + error = "Full text path is already defined"; + } + return error; + }; + + const initializeData = (fullTextPolicy: FullTextPolicy): FullTextPolicyData[] => { + if (!fullTextPolicy) { + fullTextPolicy = { defaultLanguage: getFullTextLanguageOptions()[0].key as never, fullTextPaths: [] }; + } + return fullTextPolicy.fullTextPaths.map((fullTextPath: FullTextPath) => ({ + ...fullTextPath, + pathError: getFullTextPathError(fullTextPath.path), + })); + }; + + const [fullTextPathData, setFullTextPathData] = React.useState(initializeData(fullTextPolicy)); + const [defaultLanguage, setDefaultLanguage] = React.useState( + fullTextPolicy ? fullTextPolicy.defaultLanguage : (getFullTextLanguageOptions()[0].key as never), + ); + + React.useEffect(() => { + propagateData(); + }, [fullTextPathData, defaultLanguage]); + + React.useEffect(() => { + if (discardChanges) { + setFullTextPathData(initializeData(fullTextPolicy)); + setDefaultLanguage(fullTextPolicy.defaultLanguage); + onChangesDiscarded(); + } + }, [discardChanges]); + + const propagateData = () => { + const newFullTextPolicy: FullTextPolicy = { + defaultLanguage: defaultLanguage, + fullTextPaths: fullTextPathData.map((policy: FullTextPolicyData) => ({ + path: policy.path, + language: policy.language, + })), + }; + const fullTextIndexes: FullTextIndex[] = fullTextPathData.map((policy) => ({ + path: policy.path, + })); + const validationPassed = fullTextPathData.every((policy: FullTextPolicyData) => policy.pathError === ""); + onFullTextPathChange(newFullTextPolicy, fullTextIndexes, validationPassed); + }; + + const onFullTextPathValueChange = (index: number, event: React.ChangeEvent) => { + const value = event.target.value.trim(); + const fullTextPaths = [...fullTextPathData]; + if (!fullTextPaths[index]?.path && !value.startsWith("/")) { + fullTextPaths[index].path = "/" + value; + } else { + fullTextPaths[index].path = value; + } + fullTextPaths[index].pathError = getFullTextPathError(value, index); + setFullTextPathData(fullTextPaths); + }; + + const onFullTextPathPolicyChange = (index: number, option: IDropdownOption): void => { + const policies = [...fullTextPathData]; + policies[index].language = option.key as never; + setFullTextPathData(policies); + }; + + const onAdd = () => { + setFullTextPathData([ + ...fullTextPathData, + { + path: "", + language: defaultLanguage, + pathError: getFullTextPathError(""), + }, + ]); + }; + + const onDelete = (index: number) => { + const policies = fullTextPathData.filter((_uniqueKey, j) => index !== j); + setFullTextPathData(policies); + }; + + return ( + + + + , option: IDropdownOption) => + setDefaultLanguage(option.key as never) + } + > + + {fullTextPathData && + fullTextPathData.length > 0 && + fullTextPathData.map((fullTextPolicy: FullTextPolicyData, index: number) => ( + onDelete(index)} + > + + + + + ) => onFullTextPathValueChange(index, event)} + value={fullTextPolicy.path || ""} + errorMessage={fullTextPolicy.pathError} + /> + + + + , option: IDropdownOption) => + onFullTextPathPolicyChange(index, option) + } + > + + + + + ))} + + Add full text path + + + ); +}; + +export const getFullTextLanguageOptions = (): IDropdownOption[] => { + return [ + { + key: "en-US", + text: "English (US)", + }, + ]; +}; diff --git a/src/Explorer/Controls/InputDataList/InputDataList.tsx b/src/Explorer/Controls/InputDataList/InputDataList.tsx new file mode 100644 index 000000000..cd31db53b --- /dev/null +++ b/src/Explorer/Controls/InputDataList/InputDataList.tsx @@ -0,0 +1,314 @@ +// This component is used to create a dropdown list of options for the user to select from. +// The options are displayed in a dropdown list when the user clicks on the input field. +// The user can then select an option from the list. The selected option is then displayed in the input field. + +import { getTheme } from "@fluentui/react"; +import { + Button, + Divider, + Input, + Link, + makeStyles, + Popover, + PopoverProps, + PopoverSurface, + PositioningImperativeRef, +} from "@fluentui/react-components"; +import { ArrowDownRegular, DismissRegular } from "@fluentui/react-icons"; +import { NormalizedEventKey } from "Common/Constants"; +import { tokens } from "Explorer/Theme/ThemeUtil"; +import React, { FC, useEffect, useRef } from "react"; + +const useStyles = makeStyles({ + container: { + padding: 0, + }, + input: { + flexGrow: 1, + paddingRight: 0, + outline: "none", + "& input:focus": { + outline: "none", // Undo body :focus dashed outline + }, + }, + inputButton: { + border: 0, + }, + dropdownHeader: { + width: "100%", + fontSize: tokens.fontSizeBase300, + fontWeight: 600, + padding: `${tokens.spacingVerticalM} 0 0 ${tokens.spacingVerticalM}`, + }, + dropdownStack: { + display: "flex", + flexDirection: "column", + gap: tokens.spacingVerticalS, + marginTop: tokens.spacingVerticalS, + marginBottom: "1px", + }, + dropdownOption: { + fontSize: tokens.fontSizeBase300, + fontWeight: 400, + justifyContent: "left", + padding: `${tokens.spacingHorizontalXS} ${tokens.spacingHorizontalS} ${tokens.spacingHorizontalXS} ${tokens.spacingHorizontalL}`, + overflow: "hidden", + whiteSpace: "nowrap", + textOverflow: "ellipsis", + border: 0, + ":hover": { + outline: `1px dashed ${tokens.colorNeutralForeground1Hover}`, + backgroundColor: tokens.colorNeutralBackground2Hover, + color: tokens.colorNeutralForeground1, + }, + }, + bottomSection: { + fontSize: tokens.fontSizeBase300, + fontWeight: 400, + padding: `${tokens.spacingHorizontalXS} ${tokens.spacingHorizontalS} ${tokens.spacingHorizontalXS} ${tokens.spacingHorizontalL}`, + overflow: "hidden", + whiteSpace: "nowrap", + textOverflow: "ellipsis", + }, +}); + +export interface InputDatalistDropdownOptionSection { + label: string; + options: string[]; +} + +export interface InputDataListProps { + dropdownOptions: InputDatalistDropdownOptionSection[]; + placeholder?: string; + title?: string; + value: string; + onChange: (value: string) => void; + onKeyDown?: (e: React.KeyboardEvent) => void; + autofocus?: boolean; // true: acquire focus on first render + bottomLink?: { + text: string; + url: string; + }; +} + +export const InputDataList: FC = ({ + dropdownOptions, + placeholder, + title, + value, + onChange, + onKeyDown, + autofocus, + bottomLink, +}) => { + const styles = useStyles(); + const [showDropdown, setShowDropdown] = React.useState(false); + const inputRef = useRef(null); + const positioningRef = React.useRef(null); + const [isInputFocused, setIsInputFocused] = React.useState(autofocus); + const [autofocusFirstDropdownItem, setAutofocusFirstDropdownItem] = React.useState(false); + + const theme = getTheme(); + const itemRefs = useRef([]); + + useEffect(() => { + if (inputRef.current) { + positioningRef.current?.setTarget(inputRef.current); + } + }, [inputRef, positioningRef]); + + useEffect(() => { + if (isInputFocused) { + inputRef.current?.focus(); + } + }, [isInputFocused]); + + useEffect(() => { + if (autofocusFirstDropdownItem && showDropdown) { + // Autofocus on first item if input isn't focused + itemRefs.current[0]?.focus(); + setAutofocusFirstDropdownItem(false); + } + }, [autofocusFirstDropdownItem, showDropdown]); + + const handleOpenChange: PopoverProps["onOpenChange"] = (e, data) => { + if (isInputFocused && !data.open) { + // Don't close if input is focused and we're opening the dropdown (which will steal the focus) + return; + } + + setShowDropdown(data.open || false); + if (data.open) { + setIsInputFocused(true); + } + }; + + const handleInputKeyDown = (e: React.KeyboardEvent) => { + if (e.key === NormalizedEventKey.Escape) { + setShowDropdown(false); + } else if (e.key === NormalizedEventKey.DownArrow) { + setShowDropdown(true); + setAutofocusFirstDropdownItem(true); + } + onKeyDown(e); + }; + + const handleDownDropdownItemKeyDown = ( + e: React.KeyboardEvent, + index: number, + ) => { + if (e.key === NormalizedEventKey.Enter) { + e.currentTarget.click(); + } else if (e.key === NormalizedEventKey.Escape) { + setShowDropdown(false); + inputRef.current?.focus(); + } else if (e.key === NormalizedEventKey.DownArrow) { + if (index + 1 < itemRefs.current.length) { + itemRefs.current[index + 1].focus(); + } else { + setIsInputFocused(true); + } + } else if (e.key === NormalizedEventKey.UpArrow) { + if (index - 1 >= 0) { + itemRefs.current[index - 1].focus(); + } else { + // Last item, focus back to input + setIsInputFocused(true); + } + } + }; + + // Flatten dropdownOptions to better manage refs and focus + let flatIndex = 0; + const indexMap = new Map(); + for (let sectionIndex = 0; sectionIndex < dropdownOptions.length; sectionIndex++) { + const section = dropdownOptions[sectionIndex]; + for (let optionIndex = 0; optionIndex < section.options.length; optionIndex++) { + indexMap.set(`${sectionIndex}-${optionIndex}`, flatIndex); + flatIndex++; + } + } + + return ( + <> + { + const newValue = e.target.value; + // Don't show dropdown if there is already a value in the input field (when user is typing) + setShowDropdown(!(newValue.length > 0)); + onChange(newValue); + }} + onClick={(e) => { + e.stopPropagation(); + }} + onFocus={() => { + // Don't show dropdown if there is already a value in the input field + // or isInputFocused is undefined which means component is mounting + setShowDropdown(!(value.length > 0) && isInputFocused !== undefined); + + setIsInputFocused(true); + }} + onBlur={() => { + setIsInputFocused(false); + }} + contentAfter={ + value.length > 0 ? ( + + ))} + + + ))} + {bottomLink && ( + <> + +
+ (itemRefs.current[flatIndex] = el)} + href={bottomLink.url} + target="_blank" + onBlur={() => setShowDropdown(false)} + onKeyDown={(e: React.KeyboardEvent) => handleDownDropdownItemKeyDown(e, flatIndex)} + > + {bottomLink.text} + +
+ + )} + + + + ); +}; diff --git a/src/Explorer/Controls/Settings/SettingsComponent.tsx b/src/Explorer/Controls/Settings/SettingsComponent.tsx index 74f3fb4f6..57bf5b13e 100644 --- a/src/Explorer/Controls/Settings/SettingsComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsComponent.tsx @@ -4,11 +4,11 @@ import { ComputedPropertiesComponentProps, } from "Explorer/Controls/Settings/SettingsSubComponents/ComputedPropertiesComponent"; import { - ContainerVectorPolicyComponent, - ContainerVectorPolicyComponentProps, -} from "Explorer/Controls/Settings/SettingsSubComponents/ContainerVectorPolicyComponent"; + ContainerPolicyComponent, + ContainerPolicyComponentProps, +} from "Explorer/Controls/Settings/SettingsSubComponents/ContainerPolicyComponent"; import { useDatabases } from "Explorer/useDatabases"; -import { isVectorSearchEnabled } from "Utils/CapabilityUtils"; +import { isFullTextSearchEnabled, isVectorSearchEnabled } from "Utils/CapabilityUtils"; import { isRunningOnPublicCloud } from "Utils/CloudUtils"; import * as React from "react"; import DiscardIcon from "../../../../images/discard.svg"; @@ -105,6 +105,13 @@ export interface SettingsComponentState { isSubSettingsSaveable: boolean; isSubSettingsDiscardable: boolean; + vectorEmbeddingPolicy: DataModels.VectorEmbeddingPolicy; + vectorEmbeddingPolicyBaseline: DataModels.VectorEmbeddingPolicy; + fullTextPolicy: DataModels.FullTextPolicy; + fullTextPolicyBaseline: DataModels.FullTextPolicy; + shouldDiscardContainerPolicies: boolean; + isContainerPolicyDirty: boolean; + indexingPolicyContent: DataModels.IndexingPolicy; indexingPolicyContentBaseline: DataModels.IndexingPolicy; shouldDiscardIndexingPolicy: boolean; @@ -149,6 +156,7 @@ export class SettingsComponent extends React.Component this.setState({ isScaleDiscardable: isScaleDiscardable }); + private onVectorEmbeddingPolicyChange = (newVectorEmbeddingPolicy: DataModels.VectorEmbeddingPolicy): void => + this.setState({ vectorEmbeddingPolicy: newVectorEmbeddingPolicy }); + + private onFullTextPolicyChange = (newFullTextPolicy: DataModels.FullTextPolicy): void => + this.setState({ fullTextPolicy: newFullTextPolicy }); + private onIndexingPolicyContentChange = (newIndexingPolicy: DataModels.IndexingPolicy): void => this.setState({ indexingPolicyContent: newIndexingPolicy }); + private resetShouldDiscardContainerPolicies = (): void => this.setState({ shouldDiscardContainerPolicies: false }); + private resetShouldDiscardIndexingPolicy = (): void => this.setState({ shouldDiscardIndexingPolicy: false }); private logIndexingPolicySuccessMessage = (): void => { @@ -538,6 +568,12 @@ export class SettingsComponent extends React.Component this.setState({ isSubSettingsDiscardable: isSubSettingsDiscardable }); + private onVectorEmbeddingPolicyDirtyChange = (isVectorEmbeddingPolicyDirty: boolean): void => + this.setState({ isContainerPolicyDirty: isVectorEmbeddingPolicyDirty }); + + private onFullTextPolicyDirtyChange = (isFullTextPolicyDirty: boolean): void => + this.setState({ isContainerPolicyDirty: isFullTextPolicyDirty }); + private onIndexingPolicyDirtyChange = (isIndexingPolicyDirty: boolean): void => this.setState({ isIndexingPolicyDirty: isIndexingPolicyDirty }); @@ -691,6 +727,10 @@ export class SettingsComponent extends React.Component, }); - if (this.isVectorSearchEnabled) { + if (this.isVectorSearchEnabled || this.isFullTextSearchEnabled) { tabs.push({ tab: SettingsV2TabTypes.ContainerVectorPolicyTab, - content: , + content: , }); } diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ContainerPolicyComponent.text.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/ContainerPolicyComponent.text.tsx new file mode 100644 index 000000000..0ef5250c6 --- /dev/null +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ContainerPolicyComponent.text.tsx @@ -0,0 +1,6 @@ +import "@testing-library/jest-dom"; + +describe("ContainerPolicyComponent", () => { + //CTODO: add tests + it.skip("should render correctly", () => {}); +}); diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ContainerPolicyComponent.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/ContainerPolicyComponent.tsx new file mode 100644 index 000000000..f4db62ec6 --- /dev/null +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ContainerPolicyComponent.tsx @@ -0,0 +1,163 @@ +import { DefaultButton, Pivot, PivotItem, Stack } from "@fluentui/react"; +import { FullTextPolicy, VectorEmbedding, VectorEmbeddingPolicy } from "Contracts/DataModels"; +import { + FullTextPoliciesComponent, + getFullTextLanguageOptions, +} from "Explorer/Controls/FullTextSeach/FullTextPoliciesComponent"; +import { titleAndInputStackProps } from "Explorer/Controls/Settings/SettingsRenderUtils"; +import { ContainerPolicyTabTypes, isDirty } from "Explorer/Controls/Settings/SettingsUtils"; +import { VectorEmbeddingPoliciesComponent } from "Explorer/Controls/VectorSearch/VectorEmbeddingPoliciesComponent"; +import React from "react"; + +export interface ContainerPolicyComponentProps { + vectorEmbeddingPolicy: VectorEmbeddingPolicy; + vectorEmbeddingPolicyBaseline: VectorEmbeddingPolicy; + onVectorEmbeddingPolicyChange: (newVectorEmbeddingPolicy: VectorEmbeddingPolicy) => void; + onVectorEmbeddingPolicyDirtyChange: (isVectorEmbeddingPolicyDirty: boolean) => void; + isVectorSearchEnabled: boolean; + fullTextPolicy: FullTextPolicy; + fullTextPolicyBaseline: FullTextPolicy; + onFullTextPolicyChange: (newFullTextPolicy: FullTextPolicy) => void; + onFullTextPolicyDirtyChange: (isFullTextPolicyDirty: boolean) => void; + isFullTextSearchEnabled: boolean; + shouldDiscardContainerPolicies: boolean; + resetShouldDiscardContainerPolicyChange: () => void; +} + +export const ContainerPolicyComponent: React.FC = ({ + vectorEmbeddingPolicy, + vectorEmbeddingPolicyBaseline, + onVectorEmbeddingPolicyChange, + onVectorEmbeddingPolicyDirtyChange, + isVectorSearchEnabled, + fullTextPolicy, + fullTextPolicyBaseline, + onFullTextPolicyChange, + onFullTextPolicyDirtyChange, + isFullTextSearchEnabled, + shouldDiscardContainerPolicies, + resetShouldDiscardContainerPolicyChange, +}) => { + const [selectedTab, setSelectedTab] = React.useState( + ContainerPolicyTabTypes.VectorPolicyTab, + ); + const [vectorEmbeddings, setVectorEmbeddings] = React.useState(); + const [vectorEmbeddingsBaseline, setVectorEmbeddingsBaseline] = React.useState(); + const [discardVectorChanges, setDiscardVectorChanges] = React.useState(false); + const [fullTextSearchPolicy, setFullTextSearchPolicy] = React.useState(); + const [fullTextSearchPolicyBaseline, setFullTextSearchPolicyBaseline] = React.useState(); + const [discardFullTextChanges, setDiscardFullTextChanges] = React.useState(false); + + React.useEffect(() => { + setVectorEmbeddings(vectorEmbeddingPolicy?.vectorEmbeddings); + setVectorEmbeddingsBaseline(vectorEmbeddingPolicyBaseline?.vectorEmbeddings); + }, [vectorEmbeddingPolicy]); + + React.useEffect(() => { + setFullTextSearchPolicy(fullTextPolicy); + setFullTextSearchPolicyBaseline(fullTextPolicyBaseline); + }, [fullTextPolicy, fullTextPolicyBaseline]); + + React.useEffect(() => { + if (shouldDiscardContainerPolicies) { + setVectorEmbeddings(vectorEmbeddingPolicyBaseline?.vectorEmbeddings); + setDiscardVectorChanges(true); + setFullTextSearchPolicy(fullTextPolicyBaseline); + setDiscardFullTextChanges(true); + resetShouldDiscardContainerPolicyChange(); + } + }); + + const checkAndSendVectorEmbeddingPoliciesToSettings = (newVectorEmbeddings: VectorEmbedding[]): void => { + if (isDirty(newVectorEmbeddings, vectorEmbeddingsBaseline)) { + onVectorEmbeddingPolicyDirtyChange(true); + onVectorEmbeddingPolicyChange({ vectorEmbeddings: newVectorEmbeddings }); + } else { + resetShouldDiscardContainerPolicyChange(); + } + }; + + const checkAndSendFullTextPolicyToSettings = (newFullTextPolicy: FullTextPolicy): void => { + if (isDirty(newFullTextPolicy, fullTextSearchPolicyBaseline)) { + onFullTextPolicyDirtyChange(true); + onFullTextPolicyChange(newFullTextPolicy); + } else { + resetShouldDiscardContainerPolicyChange(); + } + }; + + const onVectorChangesDiscarded = (): void => { + setDiscardVectorChanges(false); + }; + + const onFullTextChangesDiscarded = (): void => { + setDiscardFullTextChanges(false); + }; + + const onPivotChange = (item: PivotItem): void => { + const selectedTab = ContainerPolicyTabTypes[item.props.itemKey as keyof typeof ContainerPolicyTabTypes]; + setSelectedTab(selectedTab); + }; + + return ( +
+ + {isVectorSearchEnabled && ( + + + {vectorEmbeddings && ( + + checkAndSendVectorEmbeddingPoliciesToSettings(vectorEmbeddings) + } + discardChanges={discardVectorChanges} + onChangesDiscarded={onVectorChangesDiscarded} + /> + )} + + + )} + {isFullTextSearchEnabled && ( + + + {fullTextSearchPolicy ? ( + + checkAndSendFullTextPolicyToSettings(newFullTextPolicy) + } + discardChanges={discardFullTextChanges} + onChangesDiscarded={onFullTextChangesDiscarded} + /> + ) : ( + { + checkAndSendFullTextPolicyToSettings({ + defaultLanguage: getFullTextLanguageOptions()[0].key as never, + fullTextPaths: [], + }); + }} + > + Create new full text search policy + + )} + + + )} + +
+ ); +}; diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ContainerVectorPolicyComponent.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/ContainerVectorPolicyComponent.tsx deleted file mode 100644 index ca4d63a04..000000000 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/ContainerVectorPolicyComponent.tsx +++ /dev/null @@ -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 = ({ - vectorEmbeddingPolicy, -}) => { - return ( - - - - ); -}; diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/IndexingPolicyComponent.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/IndexingPolicyComponent.tsx index bc5de93a4..b2e7e12c2 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/IndexingPolicyComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/IndexingPolicyComponent.tsx @@ -120,11 +120,6 @@ export class IndexingPolicyComponent extends React.Component< indexTransformationProgress={this.props.indexTransformationProgress} refreshIndexTransformationProgress={this.props.refreshIndexTransformationProgress} /> - {this.props.isVectorSearchEnabled && ( - - Container vector policies and vector indexes are not modifiable after container creation - - )} {isDirty(this.props.indexingPolicyContent, this.props.indexingPolicyContentBaseline) && ( {unsavedEditorWarningMessage("indexPolicy")} )} diff --git a/src/Explorer/Controls/Settings/SettingsUtils.tsx b/src/Explorer/Controls/Settings/SettingsUtils.tsx index cff7d1f74..fa8360b9b 100644 --- a/src/Explorer/Controls/Settings/SettingsUtils.tsx +++ b/src/Explorer/Controls/Settings/SettingsUtils.tsx @@ -4,7 +4,14 @@ import * as ViewModels from "../../../Contracts/ViewModels"; import { MongoIndex } from "../../../Utils/arm/generatedClients/cosmos/types"; const zeroValue = 0; -export type isDirtyTypes = boolean | string | number | DataModels.IndexingPolicy | DataModels.ComputedProperties; +export type isDirtyTypes = + | boolean + | string + | number + | DataModels.IndexingPolicy + | DataModels.ComputedProperties + | DataModels.VectorEmbedding[] + | DataModels.FullTextPolicy; export const TtlOff = "off"; export const TtlOn = "on"; export const TtlOnNoDefault = "on-nodefault"; @@ -50,6 +57,11 @@ export enum SettingsV2TabTypes { ContainerVectorPolicyTab, } +export enum ContainerPolicyTabTypes { + VectorPolicyTab, + FullTextPolicyTab, +} + export interface IsComponentDirtyResult { isSaveable: boolean; isDiscardable: boolean; @@ -154,7 +166,7 @@ export const getTabTitle = (tab: SettingsV2TabTypes): string => { case SettingsV2TabTypes.ComputedPropertiesTab: return "Computed Properties"; case SettingsV2TabTypes.ContainerVectorPolicyTab: - return "Container Vector Policy (preview)"; + return "Container Policies"; default: throw new Error(`Unknown tab ${tab}`); } diff --git a/src/Explorer/Controls/Settings/TestUtils.tsx b/src/Explorer/Controls/Settings/TestUtils.tsx index d0c794025..c158e5cba 100644 --- a/src/Explorer/Controls/Settings/TestUtils.tsx +++ b/src/Explorer/Controls/Settings/TestUtils.tsx @@ -46,6 +46,8 @@ export const collection = { query: "query", }, ]), + vectorEmbeddingPolicy: ko.observable({} as DataModels.VectorEmbeddingPolicy), + fullTextPolicy: ko.observable({} as DataModels.FullTextPolicy), readSettings: () => { return; }, diff --git a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap index c1c725a6f..ea6fe2864 100644 --- a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap +++ b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap @@ -55,6 +55,7 @@ exports[`SettingsComponent renders 1`] = ` }, "databaseId": "test", "defaultTtl": [Function], + "fullTextPolicy": [Function], "geospatialConfig": [Function], "getDatabase": [Function], "id": [Function], @@ -71,6 +72,7 @@ exports[`SettingsComponent renders 1`] = ` "readSettings": [Function], "uniqueKeyPolicy": {}, "usageSizeInKB": [Function], + "vectorEmbeddingPolicy": [Function], } } isAutoPilotSelected={false} @@ -132,6 +134,7 @@ exports[`SettingsComponent renders 1`] = ` }, "databaseId": "test", "defaultTtl": [Function], + "fullTextPolicy": [Function], "geospatialConfig": [Function], "getDatabase": [Function], "id": [Function], @@ -148,6 +151,7 @@ exports[`SettingsComponent renders 1`] = ` "readSettings": [Function], "uniqueKeyPolicy": {}, "usageSizeInKB": [Function], + "vectorEmbeddingPolicy": [Function], } } displayedTtlSeconds="5" @@ -249,6 +253,7 @@ exports[`SettingsComponent renders 1`] = ` }, "databaseId": "test", "defaultTtl": [Function], + "fullTextPolicy": [Function], "geospatialConfig": [Function], "getDatabase": [Function], "id": [Function], @@ -265,6 +270,7 @@ exports[`SettingsComponent renders 1`] = ` "readSettings": [Function], "uniqueKeyPolicy": {}, "usageSizeInKB": [Function], + "vectorEmbeddingPolicy": [Function], } } explorer={ diff --git a/src/Explorer/Panes/VectorSearchPanel/AddVectorEmbeddingPolicyForm.test.tsx b/src/Explorer/Controls/VectorSearch/VectorEmbeddingPoliciesComponent.test.tsx similarity index 81% rename from src/Explorer/Panes/VectorSearchPanel/AddVectorEmbeddingPolicyForm.test.tsx rename to src/Explorer/Controls/VectorSearch/VectorEmbeddingPoliciesComponent.test.tsx index b7aff69ae..dc3d53cbd 100644 --- a/src/Explorer/Panes/VectorSearchPanel/AddVectorEmbeddingPolicyForm.test.tsx +++ b/src/Explorer/Controls/VectorSearch/VectorEmbeddingPoliciesComponent.test.tsx @@ -2,7 +2,7 @@ import "@testing-library/jest-dom"; import { RenderResult, fireEvent, render, screen, waitFor } from "@testing-library/react"; import { VectorEmbedding, VectorIndex } from "Contracts/DataModels"; import React from "react"; -import { AddVectorEmbeddingPolicyForm } from "./AddVectorEmbeddingPolicyForm"; +import { VectorEmbeddingPoliciesComponent } from "./VectorEmbeddingPoliciesComponent"; const mockVectorEmbedding: VectorEmbedding[] = [ { path: "/vector1", dataType: "float32", distanceFunction: "euclidean", dimensions: 0 }, @@ -17,9 +17,9 @@ describe("AddVectorEmbeddingPolicyForm", () => { beforeEach(() => { component = render( - , ); @@ -36,7 +36,7 @@ describe("AddVectorEmbeddingPolicyForm", () => { }); test("calls onDelete when delete button is clicked", async () => { - const deleteButton = component.container.querySelector("#delete-vector-policy-1"); + const deleteButton = component.container.querySelector("#delete-Vector-embedding-1"); fireEvent.click(deleteButton); expect(mockOnVectorEmbeddingChange).toHaveBeenCalled(); expect(screen.queryByText("Vector embedding 1")).toBeNull(); @@ -49,21 +49,19 @@ describe("AddVectorEmbeddingPolicyForm", () => { 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(), { + await waitFor(() => expect(screen.getByText("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(), + expect(screen.getByText("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(), { + await waitFor(() => expect(screen.queryByText("Path should not be empty")).toBeNull(), { timeout: 1500, }); await waitFor( diff --git a/src/Explorer/Controls/VectorSearch/VectorEmbeddingPoliciesComponent.tsx b/src/Explorer/Controls/VectorSearch/VectorEmbeddingPoliciesComponent.tsx new file mode 100644 index 000000000..03d1ab1e6 --- /dev/null +++ b/src/Explorer/Controls/VectorSearch/VectorEmbeddingPoliciesComponent.tsx @@ -0,0 +1,470 @@ +import { + DefaultButton, + Dropdown, + IDropdownOption, + IStyleFunctionOrObject, + ITextFieldStyleProps, + ITextFieldStyles, + 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/Controls/VectorSearch/VectorSearchUtils"; +import React, { FunctionComponent, useState } from "react"; + +export interface IVectorEmbeddingPoliciesComponentProps { + vectorEmbeddings: VectorEmbedding[]; + onVectorEmbeddingChange: ( + vectorEmbeddings: VectorEmbedding[], + vectorIndexingPolicies: VectorIndex[], + validationPassed: boolean, + ) => void; + vectorIndexes?: VectorIndex[]; + discardChanges?: boolean; + onChangesDiscarded?: () => void; + disabled?: boolean; +} + +export interface VectorEmbeddingPolicyData { + path: string; + dataType: VectorEmbedding["dataType"]; + distanceFunction: VectorEmbedding["distanceFunction"]; + dimensions: number; + indexType: VectorIndex["type"] | "none"; + pathError: string; + dimensionsError: string; + diskANNShardKey?: string; + diskANNShardKeyError?: string; + indexingSearchListSize?: number; + indexingSearchListSizeError?: string; + quantizationByteSize?: number; + quantizationByteSizeError?: string; +} + +type VectorEmbeddingPolicyProperty = "dataType" | "distanceFunction" | "indexType"; + +const labelStyles = { + root: { + fontSize: 12, + }, +}; + +const textFieldStyles: IStyleFunctionOrObject = { + 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 VectorEmbeddingPoliciesComponent: FunctionComponent = ({ + vectorEmbeddings, + vectorIndexes, + onVectorEmbeddingChange, + discardChanges, + onChangesDiscarded, + disabled, +}): JSX.Element => { + const onVectorEmbeddingPathError = (path: string, index?: number): string => { + let error = ""; + if (!path) { + error = "Path should not be empty"; + } + if ( + index >= 0 && + vectorEmbeddingPolicyData?.find( + (vectorEmbedding: VectorEmbeddingPolicyData, dataIndex: number) => + dataIndex !== index && vectorEmbedding.path === path, + ) + ) { + error = "Path is already defined"; + } + return error; + }; + + const onVectorEmbeddingDimensionError = (dimension: number, indexType: VectorIndex["type"] | "none"): string => { + let error = ""; + if (dimension <= 0 || dimension > 4096) { + error = "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 onQuantizationByteSizeError = (size: number): string => { + let error = ""; + if (size < 1 || size > 512) { + error = "Quantization byte size must be greater than 0 and less than or equal to 512"; + } + return error; + }; + + const onIndexingSearchListSizeError = (size: number): string => { + let error = ""; + if (size < 25 || size > 500) { + error = "Indexing search list size must be greater than or equal to 25 and less than or equal to 500"; + } + return error; + }; + + //TODO: no restrictions yet due to this field being removed for now. + // Uncomment and replace with validation code when field is reinstated + // const onDiskANNShardKeyError = (shardKey: string): string => { + // return ""; + // }; + + const initializeData = (vectorEmbeddings: VectorEmbedding[], vectorIndexes: VectorIndex[]) => { + const mergedData: VectorEmbeddingPolicyData[] = []; + vectorEmbeddings.forEach((embedding) => { + const matchingIndex = displayIndexes ? vectorIndexes.find((index) => index.path === embedding.path) : undefined; + mergedData.push({ + ...embedding, + indexType: matchingIndex?.type || "none", + indexingSearchListSize: matchingIndex?.indexingSearchListSize || undefined, + quantizationByteSize: matchingIndex?.quantizationByteSize || undefined, + pathError: onVectorEmbeddingPathError(embedding.path), + dimensionsError: onVectorEmbeddingDimensionError(embedding.dimensions, matchingIndex?.type || "none"), + }); + }); + return mergedData; + }; + + const [displayIndexes] = useState(!!vectorIndexes); + const [vectorEmbeddingPolicyData, setVectorEmbeddingPolicyData] = useState( + initializeData(vectorEmbeddings, vectorIndexes), + ); + + React.useEffect(() => { + propagateData(); + }, [vectorEmbeddingPolicyData]); + + React.useEffect(() => { + if (discardChanges) { + setVectorEmbeddingPolicyData(initializeData(vectorEmbeddings, vectorIndexes)); + onChangesDiscarded(); + } + }, [discardChanges]); + + const propagateData = () => { + const vectorEmbeddings: VectorEmbedding[] = vectorEmbeddingPolicyData.map((policy: VectorEmbeddingPolicyData) => ({ + path: policy.path, + dataType: policy.dataType, + dimensions: policy.dimensions, + distanceFunction: policy.distanceFunction, + })); + const vectorIndexes: VectorIndex[] = vectorEmbeddingPolicyData + .filter((policy: VectorEmbeddingPolicyData) => policy.indexType !== "none") + .map( + (policy) => + ({ + path: policy.path, + type: policy.indexType, + indexingSearchListSize: policy.indexingSearchListSize, + quantizationByteSize: policy.quantizationByteSize, + }) as VectorIndex, + ); + const validationPassed = vectorEmbeddingPolicyData.every( + (policy: VectorEmbeddingPolicyData) => policy.pathError === "" && policy.dimensionsError === "", + ); + onVectorEmbeddingChange(vectorEmbeddings, vectorIndexes, validationPassed); + }; + + const onVectorEmbeddingPathChange = (index: number, event: React.ChangeEvent) => { + 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) => { + 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; + if (vectorEmbedding.indexType === "diskANN") { + vectorEmbedding.indexingSearchListSize = 100; + } else { + vectorEmbedding.indexingSearchListSize = undefined; + } + setVectorEmbeddingPolicyData(vectorEmbeddings); + }; + + const onQuantizationByteSizeChange = (index: number, event: React.ChangeEvent) => { + const value = parseInt(event.target.value.trim()) || 0; + const vectorEmbeddings = [...vectorEmbeddingPolicyData]; + vectorEmbeddings[index].quantizationByteSize = value; + vectorEmbeddings[index].quantizationByteSizeError = onQuantizationByteSizeError(value); + setVectorEmbeddingPolicyData(vectorEmbeddings); + }; + + const onIndexingSearchListSizeChange = (index: number, event: React.ChangeEvent) => { + const value = parseInt(event.target.value.trim()) || 0; + const vectorEmbeddings = [...vectorEmbeddingPolicyData]; + vectorEmbeddings[index].indexingSearchListSize = value; + vectorEmbeddings[index].indexingSearchListSizeError = onIndexingSearchListSizeError(value); + setVectorEmbeddingPolicyData(vectorEmbeddings); + }; + + // TODO: uncomment after Ignite + // DiskANNShardKey was removed for Ignite due to backend problems. Leaving this here as it will be reinstated immediately after Ignite + // const onDiskANNShardKeyChange = (index: number, event: React.ChangeEvent) => { + // const value = event.target.value.trim(); + // const vectorEmbeddings = [...vectorEmbeddingPolicyData]; + // if (!vectorEmbeddings[index]?.diskANNShardKey && !value.startsWith("/")) { + // vectorEmbeddings[index].diskANNShardKey = "/" + value; + // } else { + // vectorEmbeddings[index].diskANNShardKey = value; + // } + // const error = onDiskANNShardKeyError(value); + // vectorEmbeddings[index].diskANNShardKeyError = 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 ( + + {vectorEmbeddingPolicyData && + vectorEmbeddingPolicyData.length > 0 && + vectorEmbeddingPolicyData.map((vectorEmbeddingPolicy: VectorEmbeddingPolicyData, index: number) => ( + onDelete(index)} + > + + + + + ) => onVectorEmbeddingPathChange(index, event)} + value={vectorEmbeddingPolicy.path || ""} + errorMessage={vectorEmbeddingPolicy.pathError} + /> + + + + , option: IDropdownOption) => + onVectorEmbeddingPolicyChange(index, option, "dataType") + } + > + + + + , option: IDropdownOption) => + onVectorEmbeddingPolicyChange(index, option, "distanceFunction") + } + > + + + + ) => + onVectorEmbeddingDimensionsChange(index, event) + } + errorMessage={vectorEmbeddingPolicy.dimensionsError} + /> + + {displayIndexes && ( + + + , option: IDropdownOption) => + onVectorEmbeddingIndexTypeChange(index, option) + } + > + + + ) => + onQuantizationByteSizeChange(index, event) + } + /> + + + + ) => + onIndexingSearchListSizeChange(index, event) + } + /> + + {/*TODO: uncomment after Ignite */} + {/* DiskANNShardKey was removed for Ignite due to backend problems. Leaving this here as it will be reinstated immediately after Ignite + + + ) => + onDiskANNShardKeyChange(index, event) + } + /> + + */} + + )} + + + + ))} + + Add vector embedding + + + ); +}; diff --git a/src/Explorer/Panes/VectorSearchPanel/VectorSearchUtils.ts b/src/Explorer/Controls/VectorSearch/VectorSearchUtils.ts similarity index 100% rename from src/Explorer/Panes/VectorSearchPanel/VectorSearchUtils.ts rename to src/Explorer/Controls/VectorSearch/VectorSearchUtils.ts diff --git a/src/Explorer/Panes/AddCollectionPanel.tsx b/src/Explorer/Panes/AddCollectionPanel.tsx index d007f7734..8a34545d2 100644 --- a/src/Explorer/Panes/AddCollectionPanel.tsx +++ b/src/Explorer/Panes/AddCollectionPanel.tsx @@ -21,7 +21,11 @@ import { getNewDatabaseSharedThroughputDefault } from "Common/DatabaseUtility"; import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils"; import { configContext, Platform } from "ConfigContext"; import * as DataModels from "Contracts/DataModels"; -import { AddVectorEmbeddingPolicyForm } from "Explorer/Panes/VectorSearchPanel/AddVectorEmbeddingPolicyForm"; +import { + FullTextPoliciesComponent, + getFullTextLanguageOptions, +} from "Explorer/Controls/FullTextSeach/FullTextPoliciesComponent"; +import { VectorEmbeddingPoliciesComponent } from "Explorer/Controls/VectorSearch/VectorEmbeddingPoliciesComponent"; import { useSidePanel } from "hooks/useSidePanel"; import { useTeachingBubble } from "hooks/useTeachingBubble"; import React from "react"; @@ -30,7 +34,12 @@ 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, + isFullTextSearchEnabled, + isServerlessAccount, + isVectorSearchEnabled, +} from "Utils/CapabilityUtils"; import { getUpsellMessage } from "Utils/PricingUtils"; import { CollapsibleSectionComponent } from "../Controls/CollapsiblePanel/CollapsibleSectionComponent"; import { ThroughputInput } from "../Controls/ThroughputInput/ThroughputInput"; @@ -109,6 +118,9 @@ export interface AddCollectionPanelState { vectorIndexingPolicy: DataModels.VectorIndex[]; vectorEmbeddingPolicy: DataModels.VectorEmbedding[]; vectorPolicyValidated: boolean; + fullTextPolicy: DataModels.FullTextPolicy; + fullTextIndexes: DataModels.FullTextIndex[]; + fullTextPolicyValidated: boolean; } export class AddCollectionPanel extends React.Component { @@ -147,6 +159,9 @@ export class AddCollectionPanel extends React.Component - )} + {this.shouldShowFullTextSearchParameters() && ( + + { + this.scrollToSection("collapsibleFullTextPolicySectionContent"); + }} + //TODO: uncomment when learn more text becomes available + // tooltipContent={this.getContainerFullTextPolicyTooltipContent()} + > + + + { + this.setState({ fullTextPolicy, fullTextIndexes, fullTextPolicyValidated }); + }} + /> + + + + + )} {userContext.apiType !== "Tables" && ( + // Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore + // magna aliqua.{" "} + // + // Learn more + // + // + // ); + // } + private shouldShowCollectionThroughputInput(): boolean { if (isServerlessAccount()) { return false; @@ -1274,6 +1330,10 @@ export class AddCollectionPanel extends React.Component 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 = { - 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 = ({ - 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( - 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) => { - 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) => { - 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 ( - - {vectorEmbeddingPolicyData.length > 0 && - vectorEmbeddingPolicyData.map((vectorEmbeddingPolicy: VectorEmbeddingPolicyData, index: number) => ( - - - - - - ) => onVectorEmbeddingPathChange(index, event)} - value={vectorEmbeddingPolicy.path || ""} - errorMessage={vectorEmbeddingPolicy.pathError} - /> - - - - , option: IDropdownOption) => - onVectorEmbeddingPolicyChange(index, option, "dataType") - } - > - - - - , option: IDropdownOption) => - onVectorEmbeddingPolicyChange(index, option, "distanceFunction") - } - > - - - - ) => - onVectorEmbeddingDimensionsChange(index, event) - } - errorMessage={vectorEmbeddingPolicy.dimensionsError} - /> - - - - , option: IDropdownOption) => - onVectorEmbeddingIndexTypeChange(index, option) - } - > - - - onDelete(index)} - /> - - - ))} - - Add vector embedding - - - ); -}; diff --git a/src/Explorer/QueryCopilot/Modal/QueryCopilotFeedbackModal.tsx b/src/Explorer/QueryCopilot/Modal/QueryCopilotFeedbackModal.tsx index c96f8d179..7485b5515 100644 --- a/src/Explorer/QueryCopilot/Modal/QueryCopilotFeedbackModal.tsx +++ b/src/Explorer/QueryCopilot/Modal/QueryCopilotFeedbackModal.tsx @@ -79,9 +79,13 @@ export const QueryCopilotFeedbackModal = ({ readOnly /> - By pressing submit, your feedback will be used to improve Microsoft products and services. Please see the{" "} + Microsoft will process the feedback you submit pursuant to your organization’s instructions in order to + improve your and your organization’s experience with this product. If you have any questions about the use + of feedback data, please contact your tenant administrator. Processing of feedback data is governed by the + Microsoft Products and Services Data Protection Addendum between your organization and Microsoft, and the + feedback you submit is considered Personal Data under that addendum. Please see the{" "} { - + Privacy statement }{" "} diff --git a/src/Explorer/QueryCopilot/Modal/__snapshots__/QueryCopilotFeedbackModal.test.tsx.snap b/src/Explorer/QueryCopilot/Modal/__snapshots__/QueryCopilotFeedbackModal.test.tsx.snap index 4c39f7032..5f4830030 100644 --- a/src/Explorer/QueryCopilot/Modal/__snapshots__/QueryCopilotFeedbackModal.test.tsx.snap +++ b/src/Explorer/QueryCopilot/Modal/__snapshots__/QueryCopilotFeedbackModal.test.tsx.snap @@ -99,10 +99,10 @@ exports[`Query Copilot Feedback Modal snapshot test shoud render and match snaps } } > - By pressing submit, your feedback will be used to improve Microsoft products and services. Please see the + Microsoft will process the feedback you submit pursuant to your organization’s instructions in order to improve your and your organization’s experience with this product. If you have any questions about the use of feedback data, please contact your tenant administrator. Processing of feedback data is governed by the Microsoft Products and Services Data Protection Addendum between your organization and Microsoft, and the feedback you submit is considered Personal Data under that addendum. Please see the Privacy statement @@ -236,10 +236,10 @@ exports[`Query Copilot Feedback Modal snapshot test should cancel submission 1`] } } > - By pressing submit, your feedback will be used to improve Microsoft products and services. Please see the + Microsoft will process the feedback you submit pursuant to your organization’s instructions in order to improve your and your organization’s experience with this product. If you have any questions about the use of feedback data, please contact your tenant administrator. Processing of feedback data is governed by the Microsoft Products and Services Data Protection Addendum between your organization and Microsoft, and the feedback you submit is considered Personal Data under that addendum. Please see the Privacy statement @@ -373,10 +373,10 @@ exports[`Query Copilot Feedback Modal snapshot test should close on cancel click } } > - By pressing submit, your feedback will be used to improve Microsoft products and services. Please see the + Microsoft will process the feedback you submit pursuant to your organization’s instructions in order to improve your and your organization’s experience with this product. If you have any questions about the use of feedback data, please contact your tenant administrator. Processing of feedback data is governed by the Microsoft Products and Services Data Protection Addendum between your organization and Microsoft, and the feedback you submit is considered Personal Data under that addendum. Please see the Privacy statement @@ -510,10 +510,10 @@ exports[`Query Copilot Feedback Modal snapshot test should get user unput 1`] = } } > - By pressing submit, your feedback will be used to improve Microsoft products and services. Please see the + Microsoft will process the feedback you submit pursuant to your organization’s instructions in order to improve your and your organization’s experience with this product. If you have any questions about the use of feedback data, please contact your tenant administrator. Processing of feedback data is governed by the Microsoft Products and Services Data Protection Addendum between your organization and Microsoft, and the feedback you submit is considered Personal Data under that addendum. Please see the Privacy statement @@ -647,10 +647,10 @@ exports[`Query Copilot Feedback Modal snapshot test should not render dont show } } > - By pressing submit, your feedback will be used to improve Microsoft products and services. Please see the + Microsoft will process the feedback you submit pursuant to your organization’s instructions in order to improve your and your organization’s experience with this product. If you have any questions about the use of feedback data, please contact your tenant administrator. Processing of feedback data is governed by the Microsoft Products and Services Data Protection Addendum between your organization and Microsoft, and the feedback you submit is considered Personal Data under that addendum. Please see the Privacy statement @@ -784,10 +784,10 @@ exports[`Query Copilot Feedback Modal snapshot test should render dont show agai } } > - By pressing submit, your feedback will be used to improve Microsoft products and services. Please see the + Microsoft will process the feedback you submit pursuant to your organization’s instructions in order to improve your and your organization’s experience with this product. If you have any questions about the use of feedback data, please contact your tenant administrator. Processing of feedback data is governed by the Microsoft Products and Services Data Protection Addendum between your organization and Microsoft, and the feedback you submit is considered Personal Data under that addendum. Please see the Privacy statement @@ -936,10 +936,10 @@ exports[`Query Copilot Feedback Modal snapshot test should submit submission 1`] } } > - By pressing submit, your feedback will be used to improve Microsoft products and services. Please see the + Microsoft will process the feedback you submit pursuant to your organization’s instructions in order to improve your and your organization’s experience with this product. If you have any questions about the use of feedback data, please contact your tenant administrator. Processing of feedback data is governed by the Microsoft Products and Services Data Protection Addendum between your organization and Microsoft, and the feedback you submit is considered Personal Data under that addendum. Please see the Privacy statement diff --git a/src/Explorer/Sidebar.tsx b/src/Explorer/Sidebar.tsx index b0a0db7e2..a6a0e6a3e 100644 --- a/src/Explorer/Sidebar.tsx +++ b/src/Explorer/Sidebar.tsx @@ -282,67 +282,69 @@ export const SidebarContainer: React.FC = ({ explorer }) => { ); return ( - - {/* Collections Tree - Start */} - {hasSidebar && ( - // When collapsed, we force the pane to 24 pixels wide and make it non-resizable. - - -
- {loading && ( - // The Fluent UI progress bar has some issues in reduced-motion environments so we use a simple CSS animation here. - // https://github.com/microsoft/fluentui/issues/29076 -
- )} - {expanded ? ( - <> -
-
- - +
+ + {/* Collections Tree - Start */} + {hasSidebar && ( + // When collapsed, we force the pane to 24 pixels wide and make it non-resizable. + + +
+ {loading && ( + // The Fluent UI progress bar has some issues in reduced-motion environments so we use a simple CSS animation here. + // https://github.com/microsoft/fluentui/issues/29076 +
+ )} + {expanded ? ( + <> +
+
+ + +
-
-
+ {hasGlobalCommands && } + +
+ + ) : ( +
- - ) : ( - - )} -
- + + + )} +
+ + + )} + + - )} - - - - + +
); }; diff --git a/src/Explorer/Tables/TableDataClient.ts b/src/Explorer/Tables/TableDataClient.ts index 4e0859f95..8e652d6b0 100644 --- a/src/Explorer/Tables/TableDataClient.ts +++ b/src/Explorer/Tables/TableDataClient.ts @@ -757,6 +757,10 @@ export class CassandraAPIDataClient extends TableDataClient { CassandraProxyEndpoints.Mooncake, ]; + if (configContext.globallyEnabledCassandraAPIs.includes(api)) { + return true; + } + return ( configContext.NEW_CASSANDRA_APIS?.includes(api) && activeCassandraProxyEndpoints.includes(configContext.CASSANDRA_PROXY_ENDPOINT) diff --git a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.test.tsx b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.test.tsx index 14c1f16a8..b0c6514ef 100644 --- a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.test.tsx +++ b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.test.tsx @@ -49,6 +49,7 @@ jest.mock("Common/dataAccess/queryDocuments", () => ({ requestCharge: 1, activityId: "activityId", indexMetrics: "indexMetrics", + correlatedActivityId: undefined, }), })), })); @@ -385,22 +386,6 @@ describe("Documents tab (noSql API)", () => { 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("Input.filterInput").exists()).toBeTruthy(); - }); }); describe("Command bar buttons", () => { diff --git a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx index 8bcb35aa1..83501336d 100644 --- a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx +++ b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx @@ -1,7 +1,6 @@ import { Item, ItemDefinition, PartitionKey, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos"; import { Button, - Input, Link, MessageBar, MessageBarBody, @@ -10,8 +9,7 @@ import { makeStyles, shorthands, } from "@fluentui/react-components"; -import { Dismiss16Filled } from "@fluentui/react-icons"; -import { KeyCodes, QueryCopilotSampleContainerId, QueryCopilotSampleDatabaseId } from "Common/Constants"; +import { QueryCopilotSampleContainerId, QueryCopilotSampleDatabaseId } from "Common/Constants"; import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils"; import MongoUtility from "Common/MongoUtility"; import { createDocument } from "Common/dataAccess/createDocument"; @@ -26,6 +24,7 @@ import { Platform, configContext } from "ConfigContext"; import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent"; import { useDialog } from "Explorer/Controls/Dialog"; import { EditorReact } from "Explorer/Controls/Editor/EditorReact"; +import { InputDataList, InputDatalistDropdownOptionSection } from "Explorer/Controls/InputDataList/InputDataList"; import { ProgressModalDialog } from "Explorer/Controls/ProgressModalDialog"; import Explorer from "Explorer/Explorer"; import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter"; @@ -74,6 +73,7 @@ const MAX_FILTER_HISTORY_COUNT = 100; // Datalist will become scrollable, so we const NO_SQL_THROTTLING_DOC_URL = "https://learn.microsoft.com/azure/cosmos-db/nosql/troubleshoot-request-rate-too-large"; const MONGO_THROTTLING_DOC_URL = "https://learn.microsoft.com/azure/cosmos-db/mongodb/prevent-rate-limiting-errors"; +const DATA_EXPLORER_DOC_URL = "https://learn.microsoft.com/en-us/azure/cosmos-db/data-explorer"; const loadMoreHeight = LayoutConstants.rowHeight; export const useDocumentsTabStyles = makeStyles({ @@ -90,12 +90,6 @@ export const useDocumentsTabStyles = makeStyles({ alignItems: "center", ...cosmosShorthands.borderBottom(), }, - filterInput: { - flexGrow: 1, - }, - appliedFilter: { - flexGrow: 1, - }, tableContainer: { marginRight: tokens.spacingHorizontalXXXL, }, @@ -556,8 +550,6 @@ export interface IDocumentsTabComponentProps { isTabActive: boolean; } -const getUniqueId = (collection: ViewModels.CollectionBase): string => `${collection.databaseId}-${collection.id()}`; - const getDefaultSqlFilters = (partitionKeys: string[]) => ['WHERE c.id = "foo"', "ORDER BY c._ts DESC", 'WHERE c.id = "foo" ORDER BY c._ts DESC', "ORDER BY c._ts ASC"].concat( partitionKeys.map((partitionKey) => `WHERE c.${partitionKey} = "foo"`), @@ -583,14 +575,9 @@ export const DocumentsTabComponent: React.FunctionComponent { - const [isFilterCreated, setIsFilterCreated] = useState(true); - const [isFilterExpanded, setIsFilterExpanded] = useState(false); - const [isFilterFocused, setIsFilterFocused] = useState(false); - const [appliedFilter, setAppliedFilter] = useState(""); const [filterContent, setFilterContent] = useState(""); const [documentIds, setDocumentIds] = useState([]); const [isExecuting, setIsExecuting] = useState(false); - const filterInput = useRef(null); const styles = useDocumentsTabStyles(); // Query @@ -610,7 +597,7 @@ export const DocumentsTabComponent: React.FunctionComponent(RESET_INDEX); // Table multiple selection - const [selectedRows, setSelectedRows] = React.useState>(() => new Set([0])); + const [selectedRows, setSelectedRows] = React.useState>(() => new Set()); // Command buttons const [editorState, setEditorState] = useState( @@ -657,29 +644,6 @@ export const DocumentsTabComponent: React.FunctionComponent { - if (isFilterFocused) { - filterInput.current?.focus(); - } - }, [isFilterFocused]); - - // Clicked row must be defined - useEffect(() => { - if (documentIds.length > 0) { - let currentClickedRowIndex = clickedRowIndex; - if ( - (currentClickedRowIndex === RESET_INDEX && - editorState === ViewModels.DocumentExplorerState.noDocumentSelected) || - currentClickedRowIndex > documentIds.length - 1 - ) { - // reset clicked row or the current clicked row is out of bounds - currentClickedRowIndex = INITIAL_SELECTED_ROW_INDEX; - setSelectedRows(new Set([INITIAL_SELECTED_ROW_INDEX])); - onDocumentClicked(currentClickedRowIndex, documentIds); - } - } - }, [documentIds, clickedRowIndex, editorState]); - /** * Recursively delete all documents by retrying throttled requests (429). * This only works for NoSQL, because the bulk response includes status for each delete document request. @@ -773,11 +737,6 @@ export const DocumentsTabComponent: React.FunctionComponent _partitionKey || (_collection && _collection.partitionKey), [_collection, _partitionKey], @@ -848,10 +807,6 @@ export const DocumentsTabComponent: React.FunctionComponent { setKeyboardActions({ - [KeyboardAction.SEARCH]: () => { - onShowFilterClick(); - return true; - }, [KeyboardAction.CLEAR_SEARCH]: () => { setFilterContent(""); refreshDocumentsGrid(true); @@ -1334,12 +1289,6 @@ export const DocumentsTabComponent: React.FunctionComponent { - setIsFilterCreated(true); - setIsFilterExpanded(true); - setIsFilterFocused(true); - }; - const queryTimeoutEnabled = useCallback( (): boolean => !isPreferredApiMongoDB && LocalStorageUtility.getEntryBoolean(StorageKey.QueryTimeoutEnabled), [isPreferredApiMongoDB], @@ -1381,19 +1330,6 @@ export const DocumentsTabComponent: React.FunctionComponent { - setIsFilterExpanded(false); - }; - - const onCloseButtonKeyDown: KeyboardEventHandler = (event) => { - if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) { - onHideFilterClick(); - event.stopPropagation(); - return false; - } - return true; - }; - const updateDocumentIds = (newDocumentsIds: DocumentId[]): void => { setDocumentIds(newDocumentsIds); @@ -1535,14 +1471,9 @@ export const DocumentsTabComponent: React.FunctionComponent): void => { - if (e.key === "Enter") { + if (e.key === Constants.NormalizedEventKey.Enter) { onApplyFilterClick(); - // Suppress the default behavior of the key - e.preventDefault(); - } else if (e.key === "Escape") { - onHideFilterClick(); - // Suppress the default behavior of the key e.preventDefault(); } @@ -2040,10 +1971,6 @@ export const DocumentsTabComponent: React.FunctionComponent { + const options: InputDatalistDropdownOptionSection[] = []; + const nonBlankLastFilters = lastFilterContents.filter((filter) => filter.trim() !== ""); + if (nonBlankLastFilters.length > 0) { + options.push({ + label: "Saved filters", + options: nonBlankLastFilters, + }); + } + options.push({ + label: "Default filters", + options: isPreferredApiMongoDB ? defaultMongoFilters : getDefaultSqlFilters(partitionKeyProperties), + }); + return options; + }; + return (
- {isFilterCreated && ( - <> - {!isFilterExpanded && !isPreferredApiMongoDB && ( -
- SELECT * FROM c - {appliedFilter} - -
- )} - {!isFilterExpanded && isPreferredApiMongoDB && ( -
- {appliedFilter.length > 0 && Filter :} - {!(appliedFilter.length > 0) && No filter applied} - {appliedFilter} - -
- )} - {isFilterExpanded && ( -
- {!isPreferredApiMongoDB && SELECT * FROM c } - setFilterContent(e.target.value)} - onBlur={() => setIsFilterFocused(false)} - /> - - - {addStringsNoDuplicate( - lastFilterContents, - isPreferredApiMongoDB ? defaultMongoFilters : getDefaultSqlFilters(partitionKeyProperties), - ).map((filter) => ( - - - - {!isPreferredApiMongoDB && isExecuting && ( - - )} -
- )} - - )} - {/* doesn't like to be a flex child */} -
- { - tabStateData.leftPaneWidthPercent = (100 * sizes[0]) / (sizes[0] + sizes[1]); - saveSubComponentState(SubComponentName.MainTabDivider, _collection, tabStateData); - setTabStateData(tabStateData); +
+ {!isPreferredApiMongoDB && SELECT * FROM c } + setFilterContent(value)} + onKeyDown={onFilterKeyDown} + bottomLink={{ text: "Learn more", url: DATA_EXPLORER_DOC_URL }} + /> +
+ { + tabStateData.leftPaneWidthPercent = (100 * sizes[0]) / (sizes[0] + sizes[1]); + saveSubComponentState(SubComponentName.MainTabDivider, _collection, tabStateData); + setTabStateData(tabStateData); + }} + > + +
+
+
+ refreshDocumentsGrid(false)} + items={tableItems} + onSelectedRowsChange={onSelectedRowsChange} + selectedRows={selectedRows} + size={tableContainerSizePx} + selectedColumnIds={selectedColumnIds} + columnDefinitions={columnDefinitions} + isRowSelectionDisabled={ + isBulkDeleteDisabled || + (configContext.platform === Platform.Fabric && userContext.fabricContext?.isReadOnly) + } + onColumnSelectionChange={onColumnSelectionChange} + defaultColumnSelection={getInitialColumnSelection()} + collection={_collection} + isColumnSelectionDisabled={isPreferredApiMongoDB} + /> +
+
+ {tableItems.length > 0 && ( + loadNextPage(documentsIterator.iterator, false)} + onKeyDown={onLoadMoreKeyInput} + > + Load more + + )} +
+
+ +
+ {isTabActive && selectedDocumentContent && selectedRows.size <= 1 && ( + + )} + {selectedRows.size > 1 && ( + Number of selected documents: {selectedRows.size} + )} +
+
+
{bulkDeleteOperation && ( ({ }, })); +// Added as recent change to @azure/core-util would cause randomUUID() to throw an error during jest tests. +// TODO: when not using beta version of @azure/cosmos sdk try removing this +jest.mock("@azure/core-util", () => ({ + ...jest.requireActual("@azure/core-util"), + randomUUID: jest.fn(), +})); + async function waitForComponentToPaint

(wrapper: ReactWrapper

| ShallowWrapper

, amount = 0) { let newWrapper; await act(async () => { diff --git a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTableComponent.test.tsx b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTableComponent.test.tsx index 4a5439d5e..a7fc6e32e 100644 --- a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTableComponent.test.tsx +++ b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTableComponent.test.tsx @@ -14,7 +14,6 @@ describe("DocumentsTableComponent", () => { { [ID_HEADER]: "2", [PARTITION_KEY_HEADER]: "pk2" }, { [ID_HEADER]: "3", [PARTITION_KEY_HEADER]: "pk3" }, ], - onItemClicked: (): void => {}, onSelectedRowsChange: (): void => {}, selectedRows: new Set(), size: { diff --git a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTableComponent.tsx b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTableComponent.tsx index 730e2c1a6..fc762dec4 100644 --- a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTableComponent.tsx +++ b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTableComponent.tsx @@ -68,7 +68,6 @@ export type ColumnDefinition = { export interface IDocumentsTableComponentProps { onRefreshTable: () => void; items: DocumentsTableComponentItem[]; - onItemClicked: (index: number) => void; onSelectedRowsChange: (selectedItemsIndices: Set) => void; selectedRows: Set; size: { height: number; width: number }; @@ -98,6 +97,7 @@ const defaultSize = { idealWidth: 200, minWidth: 50, }; + export const DocumentsTableComponent: React.FC = ({ onRefreshTable, items, @@ -115,6 +115,8 @@ export const DocumentsTableComponent: React.FC = }: IDocumentsTableComponentProps) => { const styles = useDocumentsTabStyles(); + const sortedRowsRef = React.useRef(null); + const [columnSizingOptions, setColumnSizingOptions] = React.useState(() => { const columnSizesMap: ColumnSizesMap = readSubComponentState(SubComponentName.ColumnSizes, collection, {}); const columnSizesPx: TableColumnSizingOptions = {}; @@ -291,22 +293,42 @@ export const DocumentsTableComponent: React.FC = const [selectionStartIndex, setSelectionStartIndex] = React.useState(INITIAL_SELECTED_ROW_INDEX); const onTableCellClicked = useCallback( - (e: React.MouseEvent, index: number) => { + (e: React.MouseEvent | undefined, index: number, rowId: TableRowId) => { if (isSelectionDisabled) { // Only allow click - onSelectedRowsChange(new Set([index])); + onSelectedRowsChange(new Set([rowId])); setSelectionStartIndex(index); return; } + // The selection helper computes in the index space (what's visible to the user in the table, ie the sorted array). + // selectedRows is in the rowId space (the index of the original unsorted array), so it must be converted to the index space. + const selectedRowsIndex = new Set(); + selectedRows.forEach((rowId) => { + const index = sortedRowsRef.current.findIndex((row: TableRowData) => row.rowId === rowId); + if (index !== -1) { + selectedRowsIndex.add(index); + } else { + // This should never happen + console.error(`Row with rowId ${rowId} not found in sorted rows`); + } + }); + const result = selectionHelper( - selectedRows as Set, + selectedRowsIndex, index, - isEnvironmentShiftPressed(e), - isEnvironmentCtrlPressed(e), + e && isEnvironmentShiftPressed(e), + e && isEnvironmentCtrlPressed(e), selectionStartIndex, ); - onSelectedRowsChange(result.selection); + + // Convert selectionHelper result from index space back to rowId space + const selectedRowIds = new Set(); + result.selection.forEach((index) => { + selectedRowIds.add(sortedRowsRef.current[index].rowId); + }); + onSelectedRowsChange(selectedRowIds); + if (result.selectionStartIndex !== undefined) { setSelectionStartIndex(result.selectionStartIndex); } @@ -320,16 +342,20 @@ export const DocumentsTableComponent: React.FC = * - a key is down and the cell is clicked by the mouse */ const onIdClicked = useCallback( - (e: React.KeyboardEvent, index: number) => { + (e: React.KeyboardEvent, rowId: TableRowId) => { if (e.key === NormalizedEventKey.Enter || e.key === NormalizedEventKey.Space) { - onSelectedRowsChange(new Set([index])); + onSelectedRowsChange(new Set([rowId])); } }, [onSelectedRowsChange], ); const RenderRow = ({ index, style, data }: ReactWindowRenderFnProps) => { - const { item, selected, appearance, onClick, onKeyDown } = data[index]; + // WARNING: because the table sorts the data, 'index' is not the same as 'rowId' + // The rowId is the index of the item in the original array, + // while the index is the index of the item in the sorted array + const { item, selected, appearance, onClick, onKeyDown, rowId } = data[index]; + return ( = key={column.columnId} className={styles.tableCell} // When clicking on a cell with shift/ctrl key, onKeyDown is called instead of onClick. - onClick={(e: React.MouseEvent) => onTableCellClicked(e, index)} - onKeyPress={(e: React.KeyboardEvent) => onIdClicked(e, index)} + onClick={(e: React.MouseEvent) => onTableCellClicked(e, index, rowId)} + onKeyPress={(e: React.KeyboardEvent) => onIdClicked(e, rowId)} {...columnSizing.getTableCellProps(column.columnId)} tabIndex={column.columnId === "id" ? 0 : -1} > @@ -420,6 +446,19 @@ export const DocumentsTableComponent: React.FC = }), ); + // Store the sorted rows in a ref which won't trigger a re-render (as opposed to a state) + sortedRowsRef.current = rows; + + // If there are no selected rows, auto select the first row + const [autoSelectFirstDoc, setAutoSelectFirstDoc] = React.useState(true); + React.useEffect(() => { + if (autoSelectFirstDoc && sortedRowsRef.current?.length > 0 && selectedRows.size === 0) { + setAutoSelectFirstDoc(false); + const DOC_INDEX_TO_SELECT = 0; + onTableCellClicked(undefined, DOC_INDEX_TO_SELECT, sortedRowsRef.current[DOC_INDEX_TO_SELECT].rowId); + } + }, [selectedRows, onTableCellClicked, autoSelectFirstDoc]); + const toggleAllKeydown = React.useCallback( (e: React.KeyboardEvent) => { if (e.key === " ") { diff --git a/src/Explorer/Tabs/DocumentsTabV2/__snapshots__/DocumentsTabV2.test.tsx.snap b/src/Explorer/Tabs/DocumentsTabV2/__snapshots__/DocumentsTabV2.test.tsx.snap index 794d609b8..0c97def4e 100644 --- a/src/Explorer/Tabs/DocumentsTabV2/__snapshots__/DocumentsTabV2.test.tsx.snap +++ b/src/Explorer/Tabs/DocumentsTabV2/__snapshots__/DocumentsTabV2.test.tsx.snap @@ -17,111 +17,124 @@ exports[`Documents tab (noSql API) when rendered should render the page 1`] = ` className="___11ktxfv_0000000 f1o614cb fy9rknc f22iagw fsnqrgy f1f5gg8d fjodcmx f122n59 f1f09k3d fg706s2 frpde29" > - SELECT * FROM c + SELECT * FROM c -

-
- -
-
- -
+ "id": "id", + "isPartitionKey": false, + "label": "id", + }, + ] + } + defaultColumnSelection={ + [ + "id", + ] + } + isColumnSelectionDisabled={false} + isRowSelectionDisabled={true} + items={[]} + onColumnSelectionChange={[Function]} + onRefreshTable={[Function]} + onSelectedRowsChange={[Function]} + selectedColumnIds={ + [ + "id", + ] + } + selectedRows={Set {}} + />
-
- -
+ + +
- - -
+ } + /> +
+
`; diff --git a/src/Explorer/Tabs/DocumentsTabV2/__snapshots__/DocumentsTableComponent.test.tsx.snap b/src/Explorer/Tabs/DocumentsTabV2/__snapshots__/DocumentsTableComponent.test.tsx.snap index 0976393b2..4f9fa580f 100644 --- a/src/Explorer/Tabs/DocumentsTabV2/__snapshots__/DocumentsTableComponent.test.tsx.snap +++ b/src/Explorer/Tabs/DocumentsTabV2/__snapshots__/DocumentsTableComponent.test.tsx.snap @@ -39,7 +39,6 @@ exports[`DocumentsTableComponent should not render selection column when isSelec }, ] } - onItemClicked={[Function]} onRefreshTable={[Function]} onSelectedRowsChange={[Function]} selectedColumnIds={[]} @@ -504,7 +503,6 @@ exports[`DocumentsTableComponent should render documents and partition keys in h }, ] } - onItemClicked={[Function]} onRefreshTable={[Function]} onSelectedRowsChange={[Function]} selectedColumnIds={[]} diff --git a/src/Explorer/Tree/Collection.ts b/src/Explorer/Tree/Collection.ts index 26246f1b1..8015f7643 100644 --- a/src/Explorer/Tree/Collection.ts +++ b/src/Explorer/Tree/Collection.ts @@ -52,6 +52,8 @@ export default class Collection implements ViewModels.Collection { public partitionKeyProperties: string[]; public id: ko.Observable; public defaultTtl: ko.Observable; + public vectorEmbeddingPolicy: ko.Observable; + public fullTextPolicy: ko.Observable; public indexingPolicy: ko.Observable; public uniqueKeyPolicy: DataModels.UniqueKeyPolicy; public usageSizeInKB: ko.Observable; @@ -110,6 +112,8 @@ export default class Collection implements ViewModels.Collection { this.id = ko.observable(data.id); this.defaultTtl = ko.observable(data.defaultTtl); + this.vectorEmbeddingPolicy = ko.observable(data.vectorEmbeddingPolicy); + this.fullTextPolicy = ko.observable(data.fullTextPolicy); this.indexingPolicy = ko.observable(data.indexingPolicy); this.usageSizeInKB = ko.observable(); this.offer = ko.observable(); diff --git a/src/UserContext.ts b/src/UserContext.ts index f4b43a2f3..955452d3a 100644 --- a/src/UserContext.ts +++ b/src/UserContext.ts @@ -75,6 +75,7 @@ export interface UserContext { readonly masterKey?: string; readonly subscriptionId?: string; readonly tenantId?: string; + readonly userName?: string; readonly resourceGroup?: string; readonly databaseAccount?: DatabaseAccount; readonly endpoint?: string; diff --git a/src/Utils/AuthorizationUtils.ts b/src/Utils/AuthorizationUtils.ts index 8cb5580ba..d2ef4e8ff 100644 --- a/src/Utils/AuthorizationUtils.ts +++ b/src/Utils/AuthorizationUtils.ts @@ -91,7 +91,8 @@ export async function acquireMsalTokenForAccount( // This will eventually throw InteractionRequiredAuthError if silent is true, we won't handle it here. const loginRequest = { scopes: [hrefEndpoint], - loginHint: user_hint, + loginHint: user_hint ?? userContext.userName, + authority: userContext.tenantId ? `${configContext.AAD_ENDPOINT}${userContext.tenantId}` : undefined, }; try { if (silent) { @@ -132,7 +133,8 @@ export async function acquireMsalTokenForAccount( account: msalAccount || null, forceRefresh: true, scopes: [hrefEndpoint], - authority: `${configContext.AAD_ENDPOINT}${msalAccount.tenantId}`, + loginHint: user_hint ?? userContext.userName, + authority: `${configContext.AAD_ENDPOINT}${userContext.tenantId ?? msalAccount.tenantId}`, }; return acquireTokenWithMsal(msalInstance, tokenRequest, silent); } diff --git a/src/Utils/CapabilityUtils.ts b/src/Utils/CapabilityUtils.ts index 8b6976666..fdfac1bd4 100644 --- a/src/Utils/CapabilityUtils.ts +++ b/src/Utils/CapabilityUtils.ts @@ -20,3 +20,7 @@ export const isServerlessAccount = (): boolean => { export const isVectorSearchEnabled = (): boolean => { return userContext.apiType === "SQL" && isCapabilityEnabled(Constants.CapabilityNames.EnableNoSQLVectorSearch); }; + +export const isFullTextSearchEnabled = (): boolean => { + return userContext.apiType === "SQL" && isCapabilityEnabled(Constants.CapabilityNames.EnableNoSQLFullTextSearch); +}; diff --git a/src/Utils/arm/generatedClients/cosmos/types.ts b/src/Utils/arm/generatedClients/cosmos/types.ts index 5272f215b..1871884e0 100644 --- a/src/Utils/arm/generatedClients/cosmos/types.ts +++ b/src/Utils/arm/generatedClients/cosmos/types.ts @@ -1237,6 +1237,7 @@ export interface SqlContainerResource { id: string; vectorEmbeddingPolicy?: VectorEmbeddingPolicy; + fullTextPolicy?: FullTextPolicy; /* The configuration of the indexing policy. By default, the indexing is automatic for all document paths within the container */ indexingPolicy?: IndexingPolicy; @@ -1281,6 +1282,28 @@ export interface VectorEmbedding { distanceFunction?: string; } +export interface FullTextPolicy { + /** + * The default language for the full text . + */ + defaultLanguage: string; + /** + * The paths to be indexed for full text search. + */ + fullTextPaths: FullTextPath[]; +} + +export interface FullTextPath { + /** + * The path to be indexed for full text search. + */ + path: string; + /** + * The language for the full text path. + */ + language: string; +} + /* Cosmos DB indexing policy */ export interface IndexingPolicy { /* Indicates if the indexing policy is automatic */ @@ -1301,6 +1324,8 @@ export interface IndexingPolicy { spatialIndexes?: SpatialSpec[]; vectorIndexes?: VectorIndex[]; + + fullTextIndexes?: FullTextIndex[]; } export interface VectorIndex { @@ -1308,6 +1333,11 @@ export interface VectorIndex { type?: string; } +export interface FullTextIndex { + /** The path in the JSON document to index. */ + path: string; +} + /* undocumented */ export interface ExcludedPath { /* The path for which the indexing behavior applies to. Index paths typically start with root and end with wildcard (/path/*) */ diff --git a/src/hooks/useKnockoutExplorer.ts b/src/hooks/useKnockoutExplorer.ts index f86e380a1..eacc7f4c5 100644 --- a/src/hooks/useKnockoutExplorer.ts +++ b/src/hooks/useKnockoutExplorer.ts @@ -695,6 +695,7 @@ function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) { subscriptionId: inputs.subscriptionId, tenantId: inputs.tenantId, subscriptionType: inputs.subscriptionType, + userName: inputs.userName, quotaId: inputs.quotaId, portalEnv: inputs.serverId as PortalEnv, hasWriteAccess: inputs.hasWriteAccess ?? true,