mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-01-08 03:57:31 +00:00
Compare commits
39 Commits
ashleyst/p
...
users/rbac
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
72eca5ed79 | ||
|
|
473a6d34bd | ||
|
|
805d72c55b | ||
|
|
eb7c737066 | ||
|
|
478467bda5 | ||
|
|
fd3a83dcd8 | ||
|
|
713df1869a | ||
|
|
192a275139 | ||
|
|
77ee359adb | ||
|
|
a50108c375 | ||
|
|
d3fb5eabdb | ||
|
|
4792e2d1c7 | ||
|
|
8849526fab | ||
|
|
24af64a66d | ||
|
|
be871737ad | ||
|
|
4d8bb5c3ea | ||
|
|
10a8505b9a | ||
|
|
e66c8a1b5c | ||
|
|
7e1a738f8e | ||
|
|
dabb91e9e9 | ||
|
|
7570d6b91d | ||
|
|
b8d6a0188a | ||
|
|
8c25742304 | ||
|
|
1ba3a6c761 | ||
|
|
c680481fe0 | ||
|
|
ef7c2fe2f7 | ||
|
|
4c7aca95e1 | ||
|
|
2243ad895a | ||
|
|
b2d5f91fe1 | ||
|
|
a712193477 | ||
|
|
5ee411693c | ||
|
|
16c7b2567b | ||
|
|
78d9a0cd8d | ||
|
|
c6ad538559 | ||
|
|
2bc09a6efe | ||
|
|
d3a3033b25 | ||
|
|
6bdc714e11 | ||
|
|
5042f28229 | ||
|
|
e1430fd06f |
9
images/EntraID.svg
Normal file
9
images/EntraID.svg
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg id="uuid-f8d4d392-7c12-4bd9-baff-66fbf7814b91" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18">
|
||||||
|
<path d="m3.802,14.032c.388.242,1.033.511,1.715.511.621,0,1.198-.18,1.676-.487,0,0,.001,0,.002-.001l1.805-1.128v4.073c-.286,0-.574-.078-.824-.234l-4.374-2.734Z" fill="#225086"/>
|
||||||
|
<path d="m7.853,1.507L.353,9.967c-.579.654-.428,1.642.323,2.111,0,0,2.776,1.735,3.126,1.954.388.242,1.033.511,1.715.511.621,0,1.198-.18,1.676-.487,0,0,.001,0,.002-.001l1.805-1.128-4.364-2.728,4.365-4.924V1s0,0,0,0c-.424,0-.847.169-1.147.507Z" fill="#6df"/>
|
||||||
|
<polygon points="4.636 10.199 4.688 10.231 9 12.927 9.001 12.927 9.001 12.927 9.001 5.276 9 5.275 4.636 10.199" fill="#cbf8ff"/>
|
||||||
|
<path d="m17.324,12.078c.751-.469.902-1.457.323-2.111l-4.921-5.551c-.397-.185-.842-.291-1.313-.291-.925,0-1.752.399-2.302,1.026l-.109.123h0s4.364,4.924,4.364,4.924h0s0,0,0,0l-4.365,2.728v4.073c.287,0,.573-.078.823-.234l7.5-4.688Z" fill="#074793"/>
|
||||||
|
<path d="m9.001,1v4.275s.109-.123.109-.123c.55-.627,1.377-1.026,2.302-1.026.472,0,.916.107,1.313.291l-2.579-2.909c-.299-.338-.723-.507-1.146-.507Z" fill="#0294e4"/>
|
||||||
|
<polygon points="13.365 10.199 13.365 10.199 13.365 10.199 9.001 5.276 9.001 12.926 13.365 10.199" fill="#96bcc2"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -2366,9 +2366,9 @@ a:link {
|
|||||||
.tabsManagerContainer {
|
.tabsManagerContainer {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
overflow: hidden;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
min-height: 300px;
|
min-height: 300px;
|
||||||
overflow-y: scroll;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabs {
|
.tabs {
|
||||||
@@ -2671,7 +2671,7 @@ a:link {
|
|||||||
width: @ActiveTabWidth;
|
width: @ActiveTabWidth;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-tabs > li.active > .tabNavContentContainer > .tab_Content > .contentWrapper> .tabNavText {
|
.nav-tabs > li.active > .tabNavContentContainer > .tab_Content > .contentWrapper > .tabNavText {
|
||||||
font-weight: bolder;
|
font-weight: bolder;
|
||||||
border-bottom: 2px solid rgba(0, 120, 212, 1);
|
border-bottom: 2px solid rgba(0, 120, 212, 1);
|
||||||
}
|
}
|
||||||
@@ -2708,68 +2708,68 @@ a:link {
|
|||||||
border-right: @ButtonBorderWidth solid @BaseMedium;
|
border-right: @ButtonBorderWidth solid @BaseMedium;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
.contentWrapper {
|
.contentWrapper {
|
||||||
.flex-display();
|
.flex-display();
|
||||||
width: @ContentWrapper;
|
width: @ContentWrapper;
|
||||||
|
|
||||||
.statusIconContainer {
|
.statusIconContainer {
|
||||||
width: @StatusIconContainerSize;
|
width: @StatusIconContainerSize;
|
||||||
height: @StatusIconContainerSize;
|
height: @StatusIconContainerSize;
|
||||||
margin-left: @SmallSpace;
|
margin-left: @SmallSpace;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
|
||||||
.errorIconContainer {
|
.errorIconContainer {
|
||||||
width: @ErrorIconContainer;
|
width: @ErrorIconContainer;
|
||||||
height: @ErrorIconContainer;
|
height: @ErrorIconContainer;
|
||||||
margin-top: 1px;
|
margin-top: 1px;
|
||||||
|
|
||||||
.errorIcon {
|
.errorIcon {
|
||||||
width: @ErrorIconWidth;
|
width: @ErrorIconWidth;
|
||||||
height: @LoadingErrorIconSize;
|
|
||||||
background-image: url(../images/error_no_outline.svg);
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-position: center;
|
|
||||||
background-size: 3px;
|
|
||||||
display: block;
|
|
||||||
margin: 1px 0px 0px 6px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.errorIconContainer.actionsEnabled {
|
|
||||||
&:hover {
|
|
||||||
.hover();
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
.active();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.errorIconContainer[tabindex]:active {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loadingIcon {
|
|
||||||
width: @LoadingErrorIconSize;
|
|
||||||
height: @LoadingErrorIconSize;
|
height: @LoadingErrorIconSize;
|
||||||
margin: 0px 0px @SmallSpace @SmallSpace;
|
background-image: url(../images/error_no_outline.svg);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
background-size: 3px;
|
||||||
|
display: block;
|
||||||
|
margin: 1px 0px 0px 6px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabNavText {
|
.errorIconContainer.actionsEnabled {
|
||||||
margin-left: @SmallSpace;
|
&:hover {
|
||||||
margin-right: 2px;
|
.hover();
|
||||||
color: @BaseDark;
|
}
|
||||||
text-overflow: ellipsis;
|
|
||||||
overflow: hidden;
|
&:focus {
|
||||||
white-space: nowrap;
|
.focus();
|
||||||
flex-grow: 1;
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
.active();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.errorIconContainer[tabindex]:active {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loadingIcon {
|
||||||
|
width: @LoadingErrorIconSize;
|
||||||
|
height: @LoadingErrorIconSize;
|
||||||
|
margin: 0px 0px @SmallSpace @SmallSpace;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tabNavText {
|
||||||
|
margin-left: @SmallSpace;
|
||||||
|
margin-right: 2px;
|
||||||
|
color: @BaseDark;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.tabIconSection {
|
.tabIconSection {
|
||||||
width: 29px;
|
width: 29px;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|||||||
@@ -209,6 +209,7 @@
|
|||||||
"test": "rimraf coverage && jest",
|
"test": "rimraf coverage && jest",
|
||||||
"test:debug": "jest --runInBand",
|
"test:debug": "jest --runInBand",
|
||||||
"test:e2e": "jest -c ./jest.config.playwright.js --detectOpenHandles",
|
"test:e2e": "jest -c ./jest.config.playwright.js --detectOpenHandles",
|
||||||
|
"test:file": "jest --coverage=false",
|
||||||
"watch": "npm run start",
|
"watch": "npm run start",
|
||||||
"wait-for-server": "wait-on -t 240000 -i 5000 -v https-get://0.0.0.0:1234/",
|
"wait-for-server": "wait-on -t 240000 -i 5000 -v https-get://0.0.0.0:1234/",
|
||||||
"build:ase": "gulp build:ase",
|
"build:ase": "gulp build:ase",
|
||||||
|
|||||||
@@ -196,6 +196,12 @@ export class Queries {
|
|||||||
public static readonly DefaultMaxWaitTimeInSeconds = 30;
|
public static readonly DefaultMaxWaitTimeInSeconds = 30;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class RBACOptions {
|
||||||
|
public static setAutomaticRBACOption: string = "Automatic";
|
||||||
|
public static setTrueRBACOption: string = "True";
|
||||||
|
public static setFalseRBACOption: string = "False";
|
||||||
|
}
|
||||||
|
|
||||||
export class SavedQueries {
|
export class SavedQueries {
|
||||||
public static readonly CollectionName: string = "___Query";
|
public static readonly CollectionName: string = "___Query";
|
||||||
public static readonly DatabaseName: string = "___Cosmos";
|
public static readonly DatabaseName: string = "___Cosmos";
|
||||||
@@ -255,6 +261,7 @@ export class HttpHeaders {
|
|||||||
public static partitionKey: string = "x-ms-documentdb-partitionkey";
|
public static partitionKey: string = "x-ms-documentdb-partitionkey";
|
||||||
public static migrateOfferToManualThroughput: string = "x-ms-cosmos-migrate-offer-to-manual-throughput";
|
public static migrateOfferToManualThroughput: string = "x-ms-cosmos-migrate-offer-to-manual-throughput";
|
||||||
public static migrateOfferToAutopilot: string = "x-ms-cosmos-migrate-offer-to-autopilot";
|
public static migrateOfferToAutopilot: string = "x-ms-cosmos-migrate-offer-to-autopilot";
|
||||||
|
public static xAPIKey: string = "X-API-Key";
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ContentType {
|
export class ContentType {
|
||||||
|
|||||||
@@ -28,19 +28,6 @@ describe("tokenProvider", () => {
|
|||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.restoreAllMocks();
|
jest.restoreAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("calls the auth token service if no master key is set", async () => {
|
|
||||||
await tokenProvider(options);
|
|
||||||
expect((window.fetch as any).mock.calls.length).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("does not call the auth service if a master key is set", async () => {
|
|
||||||
updateUserContext({
|
|
||||||
masterKey: "foo",
|
|
||||||
});
|
|
||||||
await tokenProvider(options);
|
|
||||||
expect((window.fetch as any).mock.calls.length).toBe(0);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getTokenFromAuthService", () => {
|
describe("getTokenFromAuthService", () => {
|
||||||
|
|||||||
@@ -3,10 +3,12 @@ import { getAuthorizationTokenUsingResourceTokens } from "Common/getAuthorizatio
|
|||||||
import { AuthorizationToken } from "Contracts/FabricMessageTypes";
|
import { AuthorizationToken } from "Contracts/FabricMessageTypes";
|
||||||
import { checkDatabaseResourceTokensValidity } from "Platform/Fabric/FabricUtil";
|
import { checkDatabaseResourceTokensValidity } from "Platform/Fabric/FabricUtil";
|
||||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||||
|
import { listKeys } from "Utils/arm/generatedClients/cosmos/databaseAccounts";
|
||||||
|
import { DatabaseAccountListKeysResult } from "Utils/arm/generatedClients/cosmos/types";
|
||||||
import { AuthType } from "../AuthType";
|
import { AuthType } from "../AuthType";
|
||||||
import { PriorityLevel } from "../Common/Constants";
|
import { PriorityLevel } from "../Common/Constants";
|
||||||
import { Platform, configContext } from "../ConfigContext";
|
import { Platform, configContext } from "../ConfigContext";
|
||||||
import { userContext } from "../UserContext";
|
import { updateUserContext, userContext } from "../UserContext";
|
||||||
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
||||||
import * as PriorityBasedExecutionUtils from "../Utils/PriorityBasedExecutionUtils";
|
import * as PriorityBasedExecutionUtils from "../Utils/PriorityBasedExecutionUtils";
|
||||||
import { EmulatorMasterKey, HttpHeaders } from "./Constants";
|
import { EmulatorMasterKey, HttpHeaders } from "./Constants";
|
||||||
@@ -17,7 +19,16 @@ const _global = typeof self === "undefined" ? window : self;
|
|||||||
export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
|
export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
|
||||||
const { verb, resourceId, resourceType, headers } = requestInfo;
|
const { verb, resourceId, resourceType, headers } = requestInfo;
|
||||||
|
|
||||||
if (userContext.features.enableAadDataPlane && userContext.aadToken) {
|
const aadDataPlaneFeatureEnabled =
|
||||||
|
userContext.features.enableAadDataPlane && userContext.databaseAccount.properties.disableLocalAuth;
|
||||||
|
const dataPlaneRBACOptionEnabled = userContext.dataPlaneRbacEnabled && userContext.apiType === "SQL";
|
||||||
|
if (aadDataPlaneFeatureEnabled || (!userContext.features.enableAadDataPlane && dataPlaneRBACOptionEnabled)) {
|
||||||
|
if (!userContext.aadToken) {
|
||||||
|
logConsoleError(
|
||||||
|
`AAD token does not exist. Please use "Login for Entra ID" prior to performing Entra ID RBAC operations`,
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const AUTH_PREFIX = `type=aad&ver=1.0&sig=`;
|
const AUTH_PREFIX = `type=aad&ver=1.0&sig=`;
|
||||||
const authorizationToken = `${AUTH_PREFIX}${userContext.aadToken}`;
|
const authorizationToken = `${AUTH_PREFIX}${userContext.aadToken}`;
|
||||||
return authorizationToken;
|
return authorizationToken;
|
||||||
@@ -72,8 +83,30 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
|
|||||||
|
|
||||||
if (userContext.masterKey) {
|
if (userContext.masterKey) {
|
||||||
// TODO This SDK method mutates the headers object. Find a better one or fix the SDK.
|
// TODO This SDK method mutates the headers object. Find a better one or fix the SDK.
|
||||||
await Cosmos.setAuthorizationTokenHeaderUsingMasterKey(verb, resourceId, resourceType, headers, EmulatorMasterKey);
|
await Cosmos.setAuthorizationTokenHeaderUsingMasterKey(
|
||||||
|
verb,
|
||||||
|
resourceId,
|
||||||
|
resourceType,
|
||||||
|
headers,
|
||||||
|
userContext.masterKey,
|
||||||
|
);
|
||||||
return decodeURIComponent(headers.authorization);
|
return decodeURIComponent(headers.authorization);
|
||||||
|
} else if (userContext.dataPlaneRbacEnabled == false) {
|
||||||
|
const { databaseAccount: account, subscriptionId, resourceGroup } = userContext;
|
||||||
|
const keys: DatabaseAccountListKeysResult = await listKeys(subscriptionId, resourceGroup, account.name);
|
||||||
|
|
||||||
|
if (keys.primaryMasterKey) {
|
||||||
|
updateUserContext({ masterKey: keys.primaryMasterKey });
|
||||||
|
// TODO This SDK method mutates the headers object. Find a better one or fix the SDK.
|
||||||
|
await Cosmos.setAuthorizationTokenHeaderUsingMasterKey(
|
||||||
|
verb,
|
||||||
|
resourceId,
|
||||||
|
resourceType,
|
||||||
|
headers,
|
||||||
|
keys.primaryMasterKey,
|
||||||
|
);
|
||||||
|
return decodeURIComponent(headers.authorization);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userContext.resourceToken) {
|
if (userContext.resourceToken) {
|
||||||
@@ -157,7 +190,6 @@ export function client(): Cosmos.CosmosClient {
|
|||||||
|
|
||||||
const options: Cosmos.CosmosClientOptions = {
|
const options: Cosmos.CosmosClientOptions = {
|
||||||
endpoint: endpoint() || "https://cosmos.azure.com", // CosmosClient gets upset if we pass a bad URL. This should never actually get called
|
endpoint: endpoint() || "https://cosmos.azure.com", // CosmosClient gets upset if we pass a bad URL. This should never actually get called
|
||||||
key: userContext.masterKey,
|
|
||||||
tokenProvider,
|
tokenProvider,
|
||||||
userAgentSuffix: "Azure Portal",
|
userAgentSuffix: "Azure Portal",
|
||||||
defaultHeaders: _defaultHeaders,
|
defaultHeaders: _defaultHeaders,
|
||||||
|
|||||||
@@ -42,6 +42,10 @@ export interface ConfigContext {
|
|||||||
ARM_API_VERSION: string;
|
ARM_API_VERSION: string;
|
||||||
GRAPH_ENDPOINT: string;
|
GRAPH_ENDPOINT: string;
|
||||||
GRAPH_API_VERSION: string;
|
GRAPH_API_VERSION: string;
|
||||||
|
// This is the endpoint to get offering Ids to be used to fetch prices. Refer to this doc: https://learn.microsoft.com/en-us/rest/api/marketplacecatalog/dataplane/skus/list?view=rest-marketplacecatalog-dataplane-2023-05-01-preview&tabs=HTTP
|
||||||
|
CATALOG_ENDPOINT: string;
|
||||||
|
CATALOG_API_VERSION: string;
|
||||||
|
CATALOG_API_KEY: string;
|
||||||
ARCADIA_ENDPOINT: string;
|
ARCADIA_ENDPOINT: string;
|
||||||
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: string;
|
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: string;
|
||||||
BACKEND_ENDPOINT?: string;
|
BACKEND_ENDPOINT?: string;
|
||||||
@@ -93,6 +97,9 @@ let configContext: Readonly<ConfigContext> = {
|
|||||||
ARM_API_VERSION: "2016-06-01",
|
ARM_API_VERSION: "2016-06-01",
|
||||||
GRAPH_ENDPOINT: "https://graph.microsoft.com",
|
GRAPH_ENDPOINT: "https://graph.microsoft.com",
|
||||||
GRAPH_API_VERSION: "1.6",
|
GRAPH_API_VERSION: "1.6",
|
||||||
|
CATALOG_ENDPOINT: "https://catalogapi.azure.com/",
|
||||||
|
CATALOG_API_VERSION: "2023-05-01-preview",
|
||||||
|
CATALOG_API_KEY: "",
|
||||||
ARCADIA_ENDPOINT: "https://workspaceartifacts.projectarcadia.net",
|
ARCADIA_ENDPOINT: "https://workspaceartifacts.projectarcadia.net",
|
||||||
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: "dev.azuresynapse.net",
|
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: "dev.azuresynapse.net",
|
||||||
GITHUB_CLIENT_ID: "6cb2f63cf6f7b5cbdeca", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1189306
|
GITHUB_CLIENT_ID: "6cb2f63cf6f7b5cbdeca", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1189306
|
||||||
@@ -192,6 +199,9 @@ if (process.env.NODE_ENV === "development") {
|
|||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
PROXY_PATH: "/proxy",
|
PROXY_PATH: "/proxy",
|
||||||
EMULATOR_ENDPOINT: "https://localhost:8081",
|
EMULATOR_ENDPOINT: "https://localhost:8081",
|
||||||
|
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Mpac,
|
||||||
|
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Mpac,
|
||||||
|
CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Mpac,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -425,6 +425,7 @@ export interface SelfServeFrameInputs {
|
|||||||
authorizationToken: string;
|
authorizationToken: string;
|
||||||
csmEndpoint: string;
|
csmEndpoint: string;
|
||||||
flights?: readonly string[];
|
flights?: readonly string[];
|
||||||
|
catalogAPIKey: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MonacoEditorSettings {
|
export class MonacoEditorSettings {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Icon, Label, Stack } from "@fluentui/react";
|
import { DirectionalHint, Icon, Label, Stack, TooltipHost } from "@fluentui/react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { NormalizedEventKey } from "../../../Common/Constants";
|
import { NormalizedEventKey } from "../../../Common/Constants";
|
||||||
import { accordionStackTokens } from "../Settings/SettingsRenderUtils";
|
import { accordionStackTokens } from "../Settings/SettingsRenderUtils";
|
||||||
@@ -8,6 +8,7 @@ export interface CollapsibleSectionProps {
|
|||||||
isExpandedByDefault: boolean;
|
isExpandedByDefault: boolean;
|
||||||
onExpand?: () => void;
|
onExpand?: () => void;
|
||||||
children: JSX.Element;
|
children: JSX.Element;
|
||||||
|
tooltipContent?: string | JSX.Element | JSX.Element[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CollapsibleSectionState {
|
export interface CollapsibleSectionState {
|
||||||
@@ -55,6 +56,19 @@ export class CollapsibleSectionComponent extends React.Component<CollapsibleSect
|
|||||||
>
|
>
|
||||||
<Icon iconName={this.state.isExpanded ? "ChevronDown" : "ChevronRight"} />
|
<Icon iconName={this.state.isExpanded ? "ChevronDown" : "ChevronRight"} />
|
||||||
<Label>{this.props.title}</Label>
|
<Label>{this.props.title}</Label>
|
||||||
|
{this.props.tooltipContent && (
|
||||||
|
<TooltipHost
|
||||||
|
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||||
|
content={this.props.tooltipContent}
|
||||||
|
styles={{
|
||||||
|
root: {
|
||||||
|
marginLeft: "0 !important",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
||||||
|
</TooltipHost>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
{this.state.isExpanded && this.props.children}
|
{this.state.isExpanded && this.props.children}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -20,6 +20,10 @@
|
|||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.outlineNone{
|
||||||
|
outline: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
.copyQuery:focus::after,
|
.copyQuery:focus::after,
|
||||||
.deleteQuery:focus::after {
|
.deleteQuery:focus::after {
|
||||||
outline: none !important;
|
outline: none !important;
|
||||||
|
|||||||
@@ -223,7 +223,7 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
|||||||
<Text variant="small" aria-label="capacity calculator of azure cosmos db">
|
<Text variant="small" aria-label="capacity calculator of azure cosmos db">
|
||||||
Estimate your required RU/s with{" "}
|
Estimate your required RU/s with{" "}
|
||||||
<Link
|
<Link
|
||||||
className="underlinedLink"
|
className="underlinedLink outlineNone"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
href="https://cosmos.azure.com/capacitycalculator/"
|
href="https://cosmos.azure.com/capacitycalculator/"
|
||||||
aria-label="capacity calculator of azure cosmos db"
|
aria-label="capacity calculator of azure cosmos db"
|
||||||
|
|||||||
@@ -733,13 +733,13 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
|||||||
|
|
||||||
<StyledLinkBase
|
<StyledLinkBase
|
||||||
aria-label="capacity calculator of azure cosmos db"
|
aria-label="capacity calculator of azure cosmos db"
|
||||||
className="underlinedLink"
|
className="underlinedLink outlineNone"
|
||||||
href="https://cosmos.azure.com/capacitycalculator/"
|
href="https://cosmos.azure.com/capacitycalculator/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<LinkBase
|
<LinkBase
|
||||||
aria-label="capacity calculator of azure cosmos db"
|
aria-label="capacity calculator of azure cosmos db"
|
||||||
className="underlinedLink"
|
className="underlinedLink outlineNone"
|
||||||
href="https://cosmos.azure.com/capacitycalculator/"
|
href="https://cosmos.azure.com/capacitycalculator/"
|
||||||
styles={[Function]}
|
styles={[Function]}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -1019,7 +1019,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
|||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
aria-label="capacity calculator of azure cosmos db"
|
aria-label="capacity calculator of azure cosmos db"
|
||||||
className="ms-Link underlinedLink root-117"
|
className="ms-Link underlinedLink outlineNone root-117"
|
||||||
href="https://cosmos.azure.com/capacitycalculator/"
|
href="https://cosmos.azure.com/capacitycalculator/"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
|||||||
@@ -7,11 +7,13 @@ import { getCopilotEnabled, isCopilotFeatureRegistered } from "Explorer/QueryCop
|
|||||||
import { IGalleryItem } from "Juno/JunoClient";
|
import { IGalleryItem } from "Juno/JunoClient";
|
||||||
import { scheduleRefreshDatabaseResourceToken } from "Platform/Fabric/FabricUtil";
|
import { scheduleRefreshDatabaseResourceToken } from "Platform/Fabric/FabricUtil";
|
||||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||||
|
import { acquireTokenWithMsal, getMsalInstance } from "Utils/AuthorizationUtils";
|
||||||
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointUtils";
|
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointUtils";
|
||||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import _ from "underscore";
|
import _ from "underscore";
|
||||||
|
import * as msal from "@azure/msal-browser";
|
||||||
import shallow from "zustand/shallow";
|
import shallow from "zustand/shallow";
|
||||||
import { AuthType } from "../AuthType";
|
import { AuthType } from "../AuthType";
|
||||||
import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer";
|
import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer";
|
||||||
@@ -30,14 +32,13 @@ import { PhoenixClient } from "../Phoenix/PhoenixClient";
|
|||||||
import * as ExplorerSettings from "../Shared/ExplorerSettings";
|
import * as ExplorerSettings from "../Shared/ExplorerSettings";
|
||||||
import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants";
|
import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants";
|
||||||
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
|
||||||
import { isAccountNewerThanThresholdInMs, userContext } from "../UserContext";
|
import { isAccountNewerThanThresholdInMs, updateUserContext, userContext } from "../UserContext";
|
||||||
import { getCollectionName, getUploadName } from "../Utils/APITypeUtils";
|
import { getCollectionName, getUploadName } from "../Utils/APITypeUtils";
|
||||||
import { stringToBlob } from "../Utils/BlobUtils";
|
import { stringToBlob } from "../Utils/BlobUtils";
|
||||||
import { isCapabilityEnabled } from "../Utils/CapabilityUtils";
|
import { isCapabilityEnabled } from "../Utils/CapabilityUtils";
|
||||||
import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils";
|
import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils";
|
||||||
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
|
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
|
||||||
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils";
|
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils";
|
||||||
import { update } from "../Utils/arm/generatedClients/cosmos/databaseAccounts";
|
|
||||||
import { useSidePanel } from "../hooks/useSidePanel";
|
import { useSidePanel } from "../hooks/useSidePanel";
|
||||||
import { useTabs } from "../hooks/useTabs";
|
import { useTabs } from "../hooks/useTabs";
|
||||||
import "./ComponentRegisterer";
|
import "./ComponentRegisterer";
|
||||||
@@ -66,6 +67,8 @@ import { ResourceTreeAdapter } from "./Tree/ResourceTreeAdapter";
|
|||||||
import StoredProcedure from "./Tree/StoredProcedure";
|
import StoredProcedure from "./Tree/StoredProcedure";
|
||||||
import { useDatabases } from "./useDatabases";
|
import { useDatabases } from "./useDatabases";
|
||||||
import { useSelectedNode } from "./useSelectedNode";
|
import { useSelectedNode } from "./useSelectedNode";
|
||||||
|
import { update } from "Utils/arm/generatedClients/cosmos/databaseAccounts";
|
||||||
|
import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane";
|
||||||
|
|
||||||
BindingHandlersRegisterer.registerBindingHandlers();
|
BindingHandlersRegisterer.registerBindingHandlers();
|
||||||
|
|
||||||
@@ -251,8 +254,44 @@ export default class Explorer {
|
|||||||
};
|
};
|
||||||
useDialog.getState().openDialog(addSynapseLinkDialogProps);
|
useDialog.getState().openDialog(addSynapseLinkDialogProps);
|
||||||
TelemetryProcessor.traceStart(Action.EnableAzureSynapseLink);
|
TelemetryProcessor.traceStart(Action.EnableAzureSynapseLink);
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: return result
|
public async openLoginForEntraIDPopUp(): Promise<void> {
|
||||||
|
if (userContext.databaseAccount.properties?.documentEndpoint) {
|
||||||
|
const hrefEndpoint = new URL(userContext.databaseAccount.properties.documentEndpoint).href.replace(
|
||||||
|
/\/$/,
|
||||||
|
"/.default",
|
||||||
|
);
|
||||||
|
const msalInstance = await getMsalInstance();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await msalInstance.loginPopup({
|
||||||
|
redirectUri: configContext.msalRedirectURI,
|
||||||
|
scopes: [],
|
||||||
|
});
|
||||||
|
localStorage.setItem("cachedTenantId", response.tenantId);
|
||||||
|
const cachedAccount = msalInstance.getAllAccounts()?.[0];
|
||||||
|
msalInstance.setActiveAccount(cachedAccount);
|
||||||
|
const aadToken = await acquireTokenWithMsal(msalInstance, {
|
||||||
|
forceRefresh: true,
|
||||||
|
scopes: [hrefEndpoint],
|
||||||
|
authority: `${configContext.AAD_ENDPOINT}${localStorage.getItem("cachedTenantId")}`,
|
||||||
|
});
|
||||||
|
updateUserContext({ aadToken: aadToken });
|
||||||
|
useDataPlaneRbac.setState({ aadTokenUpdated: true });
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof msal.AuthError && error.errorCode === msal.BrowserAuthErrorMessage.popUpWindowError.code) {
|
||||||
|
logConsoleError(
|
||||||
|
"We were unable to establish authorization for this account, due to pop-ups being disabled in the browser.\nPlease enable pop-ups for this site and try again",
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const errorJson = JSON.stringify(error);
|
||||||
|
logConsoleError(
|
||||||
|
`Failed to perform authorization for this account, due to the following error: \n${errorJson}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public openNPSSurveyDialog(): void {
|
public openNPSSurveyDialog(): void {
|
||||||
@@ -1157,21 +1196,25 @@ export default class Explorer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async refreshSampleData(): Promise<void> {
|
public async refreshSampleData(): Promise<void> {
|
||||||
if (!userContext.sampleDataConnectionInfo) {
|
try {
|
||||||
|
if (!userContext.sampleDataConnectionInfo) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const collection: DataModels.Collection = await readSampleCollection();
|
||||||
|
if (!collection) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const databaseId = userContext.sampleDataConnectionInfo?.databaseId;
|
||||||
|
if (!databaseId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sampleDataResourceTokenCollection = new ResourceTokenCollection(this, databaseId, collection, true);
|
||||||
|
useDatabases.setState({ sampleDataResourceTokenCollection });
|
||||||
|
} catch (error) {
|
||||||
|
Logger.logError(getErrorMessage(error), "Explorer");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const collection: DataModels.Collection = await readSampleCollection();
|
|
||||||
if (!collection) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const databaseId = userContext.sampleDataConnectionInfo?.databaseId;
|
|
||||||
if (!databaseId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const sampleDataResourceTokenCollection = new ResourceTokenCollection(this, databaseId, collection, true);
|
|
||||||
useDatabases.setState({ sampleDataResourceTokenCollection });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import OpenQueryFromDiskIcon from "../../../../images/OpenQueryFromDisk.svg";
|
|||||||
import OpenInTabIcon from "../../../../images/open-in-tab.svg";
|
import OpenInTabIcon from "../../../../images/open-in-tab.svg";
|
||||||
import SettingsIcon from "../../../../images/settings_15x15.svg";
|
import SettingsIcon from "../../../../images/settings_15x15.svg";
|
||||||
import SynapseIcon from "../../../../images/synapse-link.svg";
|
import SynapseIcon from "../../../../images/synapse-link.svg";
|
||||||
|
import EntraIDIcon from "../../../../images/EntraID.svg";
|
||||||
import { AuthType } from "../../../AuthType";
|
import { AuthType } from "../../../AuthType";
|
||||||
import * as Constants from "../../../Common/Constants";
|
import * as Constants from "../../../Common/Constants";
|
||||||
import { Platform, configContext } from "../../../ConfigContext";
|
import { Platform, configContext } from "../../../ConfigContext";
|
||||||
@@ -30,9 +31,10 @@ import { OpenFullScreen } from "../../OpenFullScreen";
|
|||||||
import { AddDatabasePanel } from "../../Panes/AddDatabasePanel/AddDatabasePanel";
|
import { AddDatabasePanel } from "../../Panes/AddDatabasePanel/AddDatabasePanel";
|
||||||
import { BrowseQueriesPane } from "../../Panes/BrowseQueriesPane/BrowseQueriesPane";
|
import { BrowseQueriesPane } from "../../Panes/BrowseQueriesPane/BrowseQueriesPane";
|
||||||
import { LoadQueryPane } from "../../Panes/LoadQueryPane/LoadQueryPane";
|
import { LoadQueryPane } from "../../Panes/LoadQueryPane/LoadQueryPane";
|
||||||
import { SettingsPane } from "../../Panes/SettingsPane/SettingsPane";
|
import { SettingsPane, useDataPlaneRbac } from "../../Panes/SettingsPane/SettingsPane";
|
||||||
import { useDatabases } from "../../useDatabases";
|
import { useDatabases } from "../../useDatabases";
|
||||||
import { SelectedNodeState, useSelectedNode } from "../../useSelectedNode";
|
import { SelectedNodeState, useSelectedNode } from "../../useSelectedNode";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
let counter = 0;
|
let counter = 0;
|
||||||
|
|
||||||
@@ -69,6 +71,22 @@ export function createStaticCommandBarButtons(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (userContext.apiType === "SQL") {
|
||||||
|
const [loginButtonProps, setLoginButtonProps] = useState<CommandButtonComponentProps | undefined>(undefined);
|
||||||
|
const dataPlaneRbacEnabled = useDataPlaneRbac((state) => state.dataPlaneRbacEnabled);
|
||||||
|
const aadTokenUpdated = useDataPlaneRbac((state) => state.aadTokenUpdated);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const buttonProps = createLoginForEntraIDButton(container);
|
||||||
|
setLoginButtonProps(buttonProps);
|
||||||
|
}, [dataPlaneRbacEnabled, aadTokenUpdated, container]);
|
||||||
|
|
||||||
|
if (loginButtonProps) {
|
||||||
|
addDivider();
|
||||||
|
buttons.push(loginButtonProps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (userContext.apiType !== "Tables") {
|
if (userContext.apiType !== "Tables") {
|
||||||
newCollectionBtn.children = [createNewCollectionGroup(container)];
|
newCollectionBtn.children = [createNewCollectionGroup(container)];
|
||||||
const newDatabaseBtn = createNewDatabase(container);
|
const newDatabaseBtn = createNewDatabase(container);
|
||||||
@@ -275,6 +293,31 @@ function createOpenSynapseLinkDialogButton(container: Explorer): CommandButtonCo
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createLoginForEntraIDButton(container: Explorer): CommandButtonComponentProps {
|
||||||
|
if (configContext.platform !== Platform.Portal) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCommandClick = async () => {
|
||||||
|
await container.openLoginForEntraIDPopUp();
|
||||||
|
useDataPlaneRbac.setState({ dataPlaneRbacEnabled: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!userContext.dataPlaneRbacEnabled || userContext.aadToken) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const label = "Login for Entra ID RBAC";
|
||||||
|
return {
|
||||||
|
iconSrc: EntraIDIcon,
|
||||||
|
iconAlt: label,
|
||||||
|
onCommandClick: handleCommandClick,
|
||||||
|
commandButtonLabel: label,
|
||||||
|
hasPopup: true,
|
||||||
|
ariaLabel: label,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function createNewDatabase(container: Explorer): CommandButtonComponentProps {
|
function createNewDatabase(container: Explorer): CommandButtonComponentProps {
|
||||||
const label = "New " + getDatabaseName();
|
const label = "New " + getDatabaseName();
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
|
|||||||
import { configContext, Platform } from "ConfigContext";
|
import { configContext, Platform } from "ConfigContext";
|
||||||
import * as DataModels from "Contracts/DataModels";
|
import * as DataModels from "Contracts/DataModels";
|
||||||
import { SubscriptionType } from "Contracts/SubscriptionType";
|
import { SubscriptionType } from "Contracts/SubscriptionType";
|
||||||
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
|
import { AddVectorEmbeddingPolicyForm } from "Explorer/Panes/VectorSearchPanel/AddVectorEmbeddingPolicyForm";
|
||||||
import { useSidePanel } from "hooks/useSidePanel";
|
import { useSidePanel } from "hooks/useSidePanel";
|
||||||
import { useTeachingBubble } from "hooks/useTeachingBubble";
|
import { useTeachingBubble } from "hooks/useTeachingBubble";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
@@ -82,22 +82,6 @@ export const AllPropertiesIndexed: DataModels.IndexingPolicy = {
|
|||||||
excludedPaths: [],
|
excludedPaths: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
const DefaultDatabaseVectorIndex: DataModels.IndexingPolicy = {
|
|
||||||
indexingMode: "consistent",
|
|
||||||
automatic: true,
|
|
||||||
includedPaths: [
|
|
||||||
{
|
|
||||||
path: "/*",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
excludedPaths: [
|
|
||||||
{
|
|
||||||
path: '/"_etag"/?',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
vectorIndexes: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DefaultVectorEmbeddingPolicy: DataModels.VectorEmbeddingPolicy = {
|
export const DefaultVectorEmbeddingPolicy: DataModels.VectorEmbeddingPolicy = {
|
||||||
vectorEmbeddings: [],
|
vectorEmbeddings: [],
|
||||||
};
|
};
|
||||||
@@ -122,8 +106,9 @@ export interface AddCollectionPanelState {
|
|||||||
isExecuting: boolean;
|
isExecuting: boolean;
|
||||||
isThroughputCapExceeded: boolean;
|
isThroughputCapExceeded: boolean;
|
||||||
teachingBubbleStep: number;
|
teachingBubbleStep: number;
|
||||||
vectorIndexingPolicy: string;
|
vectorIndexingPolicy: DataModels.VectorIndex[];
|
||||||
vectorEmbeddingPolicy: string;
|
vectorEmbeddingPolicy: DataModels.VectorEmbedding[];
|
||||||
|
vectorPolicyValidated: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AddCollectionPanel extends React.Component<AddCollectionPanelProps, AddCollectionPanelState> {
|
export class AddCollectionPanel extends React.Component<AddCollectionPanelProps, AddCollectionPanelState> {
|
||||||
@@ -159,8 +144,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
isExecuting: false,
|
isExecuting: false,
|
||||||
isThroughputCapExceeded: false,
|
isThroughputCapExceeded: false,
|
||||||
teachingBubbleStep: 0,
|
teachingBubbleStep: 0,
|
||||||
vectorIndexingPolicy: JSON.stringify(DefaultDatabaseVectorIndex, null, 2),
|
vectorEmbeddingPolicy: [],
|
||||||
vectorEmbeddingPolicy: JSON.stringify(DefaultVectorEmbeddingPolicy, null, 2),
|
vectorIndexingPolicy: [],
|
||||||
|
vectorPolicyValidated: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -896,60 +882,28 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
)}
|
)}
|
||||||
{this.shouldShowVectorSearchParameters() && (
|
{this.shouldShowVectorSearchParameters() && (
|
||||||
<Stack>
|
<Stack>
|
||||||
<CollapsibleSectionComponent
|
|
||||||
title="Indexing Policy"
|
|
||||||
isExpandedByDefault={false}
|
|
||||||
onExpand={() => {
|
|
||||||
this.scrollToSection("collapsibleVectorPolicySectionContent");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Stack id="collapsibleVectorPolicySectionContent" styles={{ root: { position: "relative" } }}>
|
|
||||||
<Link href="https://aka.ms/CosmosDBVectorSetup" target="_blank">
|
|
||||||
Learn more
|
|
||||||
</Link>
|
|
||||||
<EditorReact
|
|
||||||
language={"json"}
|
|
||||||
content={this.state.vectorIndexingPolicy}
|
|
||||||
isReadOnly={false}
|
|
||||||
wordWrap={"on"}
|
|
||||||
ariaLabel={"Editing indexing policy"}
|
|
||||||
lineNumbers={"on"}
|
|
||||||
scrollBeyondLastLine={false}
|
|
||||||
spinnerClassName="panelSectionSpinner"
|
|
||||||
monacoContainerStyles={{
|
|
||||||
minHeight: 200,
|
|
||||||
}}
|
|
||||||
onContentChanged={(newIndexingPolicy: string) => this.setVectorIndexingPolicy(newIndexingPolicy)}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
</CollapsibleSectionComponent>
|
|
||||||
<CollapsibleSectionComponent
|
<CollapsibleSectionComponent
|
||||||
title="Container Vector Policy"
|
title="Container Vector Policy"
|
||||||
isExpandedByDefault={false}
|
isExpandedByDefault={false}
|
||||||
onExpand={() => {
|
onExpand={() => {
|
||||||
this.scrollToSection("collapsibleVectorPolicySectionContent");
|
this.scrollToSection("collapsibleVectorPolicySectionContent");
|
||||||
}}
|
}}
|
||||||
|
tooltipContent={this.getContainerVectorPolicyTooltipContent()}
|
||||||
>
|
>
|
||||||
<Stack id="collapsibleVectorPolicySectionContent" styles={{ root: { position: "relative" } }}>
|
<Stack id="collapsibleVectorPolicySectionContent" styles={{ root: { position: "relative" } }}>
|
||||||
<Link href="https://aka.ms/CosmosDBVectorSetup" target="_blank">
|
<Stack styles={{ root: { paddingLeft: 40 } }}>
|
||||||
Learn more
|
<AddVectorEmbeddingPolicyForm
|
||||||
</Link>
|
vectorEmbedding={this.state.vectorEmbeddingPolicy}
|
||||||
<EditorReact
|
vectorIndex={this.state.vectorIndexingPolicy}
|
||||||
language={"json"}
|
onVectorEmbeddingChange={(
|
||||||
content={this.state.vectorEmbeddingPolicy}
|
vectorEmbeddingPolicy: DataModels.VectorEmbedding[],
|
||||||
isReadOnly={false}
|
vectorIndexingPolicy: DataModels.VectorIndex[],
|
||||||
wordWrap={"on"}
|
vectorPolicyValidated: boolean,
|
||||||
ariaLabel={"Editing container vector policy"}
|
) => {
|
||||||
lineNumbers={"on"}
|
this.setState({ vectorEmbeddingPolicy, vectorIndexingPolicy, vectorPolicyValidated });
|
||||||
scrollBeyondLastLine={false}
|
}}
|
||||||
spinnerClassName="panelSectionSpinner"
|
/>
|
||||||
monacoContainerStyles={{
|
</Stack>
|
||||||
minHeight: 200,
|
|
||||||
}}
|
|
||||||
onContentChanged={(newVectorEmbeddingPolicy: string) =>
|
|
||||||
this.setVectorEmbeddingPolicy(newVectorEmbeddingPolicy)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</CollapsibleSectionComponent>
|
</CollapsibleSectionComponent>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -1159,13 +1113,13 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private setVectorEmbeddingPolicy(vectorEmbeddingPolicy: string): void {
|
private setVectorEmbeddingPolicy(vectorEmbeddingPolicy: DataModels.VectorEmbedding[]): void {
|
||||||
this.setState({
|
this.setState({
|
||||||
vectorEmbeddingPolicy,
|
vectorEmbeddingPolicy,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private setVectorIndexingPolicy(vectorIndexingPolicy: string): void {
|
private setVectorIndexingPolicy(vectorIndexingPolicy: DataModels.VectorIndex[]): void {
|
||||||
this.setState({
|
this.setState({
|
||||||
vectorIndexingPolicy,
|
vectorIndexingPolicy,
|
||||||
});
|
});
|
||||||
@@ -1251,6 +1205,18 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getContainerVectorPolicyTooltipContent(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<Text variant="small">
|
||||||
|
Describe any properties in your data that contain vectors, so that they can be made available for similarity
|
||||||
|
queries.{" "}
|
||||||
|
<Link target="_blank" href="https://aka.ms/CosmosDBVectorSetup">
|
||||||
|
Learn more
|
||||||
|
</Link>
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private shouldShowCollectionThroughputInput(): boolean {
|
private shouldShowCollectionThroughputInput(): boolean {
|
||||||
if (isServerlessAccount()) {
|
if (isServerlessAccount()) {
|
||||||
return false;
|
return false;
|
||||||
@@ -1370,20 +1336,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.shouldShowVectorSearchParameters()) {
|
if (this.shouldShowVectorSearchParameters() && !this.state.vectorPolicyValidated) {
|
||||||
try {
|
this.setState({ errorMessage: "Please fix errors in container vector policy" });
|
||||||
JSON.parse(this.state.vectorIndexingPolicy) as DataModels.IndexingPolicy;
|
return false;
|
||||||
} catch (e) {
|
|
||||||
this.setState({ errorMessage: "Invalid JSON format for indexingPolicy" });
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
JSON.parse(this.state.vectorEmbeddingPolicy) as DataModels.VectorEmbeddingPolicy;
|
|
||||||
} catch (e) {
|
|
||||||
this.setState({ errorMessage: "Invalid JSON format for vectorEmbeddingPolicy" });
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -1461,15 +1416,17 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
}
|
}
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
let indexingPolicy: DataModels.IndexingPolicy = this.state.enableIndexing
|
const indexingPolicy: DataModels.IndexingPolicy = this.state.enableIndexing
|
||||||
? AllPropertiesIndexed
|
? AllPropertiesIndexed
|
||||||
: SharedDatabaseDefault;
|
: SharedDatabaseDefault;
|
||||||
|
|
||||||
let vectorEmbeddingPolicy: DataModels.VectorEmbeddingPolicy;
|
let vectorEmbeddingPolicy: DataModels.VectorEmbeddingPolicy;
|
||||||
|
|
||||||
if (this.shouldShowVectorSearchParameters()) {
|
if (this.shouldShowVectorSearchParameters()) {
|
||||||
indexingPolicy = JSON.parse(this.state.vectorIndexingPolicy);
|
indexingPolicy.vectorIndexes = this.state.vectorIndexingPolicy;
|
||||||
vectorEmbeddingPolicy = JSON.parse(this.state.vectorEmbeddingPolicy);
|
vectorEmbeddingPolicy = {
|
||||||
|
vectorEmbeddings: this.state.vectorEmbeddingPolicy,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const telemetryData = {
|
const telemetryData = {
|
||||||
|
|||||||
@@ -4,14 +4,19 @@ import {
|
|||||||
IChoiceGroupOption,
|
IChoiceGroupOption,
|
||||||
ISpinButtonStyles,
|
ISpinButtonStyles,
|
||||||
IToggleStyles,
|
IToggleStyles,
|
||||||
|
Icon,
|
||||||
|
MessageBar,
|
||||||
|
MessageBarType,
|
||||||
Position,
|
Position,
|
||||||
SpinButton,
|
SpinButton,
|
||||||
Toggle,
|
Toggle,
|
||||||
|
TooltipHost,
|
||||||
} from "@fluentui/react";
|
} from "@fluentui/react";
|
||||||
import * as Constants from "Common/Constants";
|
import * as Constants from "Common/Constants";
|
||||||
import { SplitterDirection } from "Common/Splitter";
|
import { SplitterDirection } from "Common/Splitter";
|
||||||
import { InfoTooltip } from "Common/Tooltip/InfoTooltip";
|
import { InfoTooltip } from "Common/Tooltip/InfoTooltip";
|
||||||
import { configContext } from "ConfigContext";
|
import { Platform, configContext } from "ConfigContext";
|
||||||
|
import { useDatabases } from "Explorer/useDatabases";
|
||||||
import {
|
import {
|
||||||
DefaultRUThreshold,
|
DefaultRUThreshold,
|
||||||
LocalStorageUtility,
|
LocalStorageUtility,
|
||||||
@@ -21,7 +26,7 @@ import {
|
|||||||
ruThresholdEnabled as isRUThresholdEnabled,
|
ruThresholdEnabled as isRUThresholdEnabled,
|
||||||
} from "Shared/StorageUtility";
|
} from "Shared/StorageUtility";
|
||||||
import * as StringUtility from "Shared/StringUtility";
|
import * as StringUtility from "Shared/StringUtility";
|
||||||
import { userContext } from "UserContext";
|
import { updateUserContext, userContext } from "UserContext";
|
||||||
import { logConsoleInfo } from "Utils/NotificationConsoleUtils";
|
import { logConsoleInfo } from "Utils/NotificationConsoleUtils";
|
||||||
import * as PriorityBasedExecutionUtils from "Utils/PriorityBasedExecutionUtils";
|
import * as PriorityBasedExecutionUtils from "Utils/PriorityBasedExecutionUtils";
|
||||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
@@ -29,6 +34,24 @@ import { useSidePanel } from "hooks/useSidePanel";
|
|||||||
import React, { FunctionComponent, useState } from "react";
|
import React, { FunctionComponent, useState } from "react";
|
||||||
import Explorer from "../../Explorer";
|
import Explorer from "../../Explorer";
|
||||||
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
|
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
|
||||||
|
import { AuthType } from "AuthType";
|
||||||
|
import create, { UseStore } from "zustand";
|
||||||
|
|
||||||
|
export interface DataPlaneRbacState {
|
||||||
|
dataPlaneRbacEnabled: boolean;
|
||||||
|
aadTokenUpdated: boolean;
|
||||||
|
|
||||||
|
getState?: () => DataPlaneRbacState;
|
||||||
|
|
||||||
|
setDataPlaneRbacEnabled: (dataPlaneRbacEnabled: boolean) => void;
|
||||||
|
setAadDataPlaneUpdated: (aadTokenUpdated: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
type DataPlaneRbacStore = UseStore<Partial<DataPlaneRbacState>>;
|
||||||
|
|
||||||
|
export const useDataPlaneRbac: DataPlaneRbacStore = create(() => ({
|
||||||
|
dataPlaneRbacEnabled: false,
|
||||||
|
}));
|
||||||
|
|
||||||
export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||||
explorer,
|
explorer,
|
||||||
@@ -43,6 +66,14 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
? Constants.Queries.UnlimitedPageOption
|
? Constants.Queries.UnlimitedPageOption
|
||||||
: Constants.Queries.CustomPageOption,
|
: Constants.Queries.CustomPageOption,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [enableDataPlaneRBACOption, setEnableDataPlaneRBACOption] = useState<string>(
|
||||||
|
LocalStorageUtility.hasItem(StorageKey.DataPlaneRbacEnabled)
|
||||||
|
? LocalStorageUtility.getEntryString(StorageKey.DataPlaneRbacEnabled)
|
||||||
|
: Constants.RBACOptions.setAutomaticRBACOption,
|
||||||
|
);
|
||||||
|
const [showDataPlaneRBACWarning, setShowDataPlaneRBACWarning] = useState<boolean>(false);
|
||||||
|
|
||||||
const [ruThresholdEnabled, setRUThresholdEnabled] = useState<boolean>(isRUThresholdEnabled());
|
const [ruThresholdEnabled, setRUThresholdEnabled] = useState<boolean>(isRUThresholdEnabled());
|
||||||
const [ruThreshold, setRUThreshold] = useState<number>(getRUThreshold());
|
const [ruThreshold, setRUThreshold] = useState<number>(getRUThreshold());
|
||||||
const [queryTimeoutEnabled, setQueryTimeoutEnabled] = useState<boolean>(
|
const [queryTimeoutEnabled, setQueryTimeoutEnabled] = useState<boolean>(
|
||||||
@@ -107,7 +138,10 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
const shouldShowCrossPartitionOption = userContext.apiType !== "Gremlin";
|
const shouldShowCrossPartitionOption = userContext.apiType !== "Gremlin";
|
||||||
const shouldShowParallelismOption = userContext.apiType !== "Gremlin";
|
const shouldShowParallelismOption = userContext.apiType !== "Gremlin";
|
||||||
const shouldShowPriorityLevelOption = PriorityBasedExecutionUtils.isFeatureEnabled();
|
const shouldShowPriorityLevelOption = PriorityBasedExecutionUtils.isFeatureEnabled();
|
||||||
const shouldShowCopilotSampleDBOption = userContext.apiType === "SQL" && useQueryCopilot.getState().copilotEnabled;
|
const shouldShowCopilotSampleDBOption =
|
||||||
|
userContext.apiType === "SQL" &&
|
||||||
|
useQueryCopilot.getState().copilotEnabled &&
|
||||||
|
useDatabases.getState().sampleDataResourceTokenCollection;
|
||||||
const handlerOnSubmit = async () => {
|
const handlerOnSubmit = async () => {
|
||||||
setIsExecuting(true);
|
setIsExecuting(true);
|
||||||
|
|
||||||
@@ -115,7 +149,26 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
StorageKey.ActualItemPerPage,
|
StorageKey.ActualItemPerPage,
|
||||||
isCustomPageOptionSelected() ? customItemPerPage : Constants.Queries.unlimitedItemsPerPage,
|
isCustomPageOptionSelected() ? customItemPerPage : Constants.Queries.unlimitedItemsPerPage,
|
||||||
);
|
);
|
||||||
|
|
||||||
LocalStorageUtility.setEntryNumber(StorageKey.CustomItemPerPage, customItemPerPage);
|
LocalStorageUtility.setEntryNumber(StorageKey.CustomItemPerPage, customItemPerPage);
|
||||||
|
|
||||||
|
LocalStorageUtility.setEntryString(StorageKey.DataPlaneRbacEnabled, enableDataPlaneRBACOption);
|
||||||
|
if (
|
||||||
|
enableDataPlaneRBACOption === Constants.RBACOptions.setTrueRBACOption ||
|
||||||
|
(enableDataPlaneRBACOption === Constants.RBACOptions.setAutomaticRBACOption &&
|
||||||
|
userContext.databaseAccount.properties.disableLocalAuth)
|
||||||
|
) {
|
||||||
|
updateUserContext({
|
||||||
|
dataPlaneRbacEnabled: true,
|
||||||
|
});
|
||||||
|
useDataPlaneRbac.setState({ dataPlaneRbacEnabled: true });
|
||||||
|
} else {
|
||||||
|
updateUserContext({
|
||||||
|
dataPlaneRbacEnabled: false,
|
||||||
|
});
|
||||||
|
useDataPlaneRbac.setState({ dataPlaneRbacEnabled: false });
|
||||||
|
}
|
||||||
|
|
||||||
LocalStorageUtility.setEntryBoolean(StorageKey.RUThresholdEnabled, ruThresholdEnabled);
|
LocalStorageUtility.setEntryBoolean(StorageKey.RUThresholdEnabled, ruThresholdEnabled);
|
||||||
LocalStorageUtility.setEntryBoolean(StorageKey.QueryTimeoutEnabled, queryTimeoutEnabled);
|
LocalStorageUtility.setEntryBoolean(StorageKey.QueryTimeoutEnabled, queryTimeoutEnabled);
|
||||||
LocalStorageUtility.setEntryNumber(StorageKey.RetryAttempts, retryAttempts);
|
LocalStorageUtility.setEntryNumber(StorageKey.RetryAttempts, retryAttempts);
|
||||||
@@ -203,6 +256,12 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
{ key: Constants.PriorityLevel.High, text: "High" },
|
{ key: Constants.PriorityLevel.High, text: "High" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const dataPlaneRBACOptionsList: IChoiceGroupOption[] = [
|
||||||
|
{ key: Constants.RBACOptions.setAutomaticRBACOption, text: "Automatic" },
|
||||||
|
{ key: Constants.RBACOptions.setTrueRBACOption, text: "True" },
|
||||||
|
{ key: Constants.RBACOptions.setFalseRBACOption, text: "False" },
|
||||||
|
];
|
||||||
|
|
||||||
const defaultQueryResultsViewOptionList: IChoiceGroupOption[] = [
|
const defaultQueryResultsViewOptionList: IChoiceGroupOption[] = [
|
||||||
{ key: SplitterDirection.Vertical, text: "Vertical" },
|
{ key: SplitterDirection.Vertical, text: "Vertical" },
|
||||||
{ key: SplitterDirection.Horizontal, text: "Horizontal" },
|
{ key: SplitterDirection.Horizontal, text: "Horizontal" },
|
||||||
@@ -219,6 +278,20 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
setPageOption(option.key);
|
setPageOption(option.key);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleOnDataPlaneRBACOptionChange = (
|
||||||
|
ev: React.FormEvent<HTMLInputElement>,
|
||||||
|
option: IChoiceGroupOption,
|
||||||
|
): void => {
|
||||||
|
setEnableDataPlaneRBACOption(option.key);
|
||||||
|
|
||||||
|
const shouldShowWarning =
|
||||||
|
(option.key === Constants.RBACOptions.setTrueRBACOption ||
|
||||||
|
(option.key === Constants.RBACOptions.setAutomaticRBACOption &&
|
||||||
|
userContext.databaseAccount.properties.disableLocalAuth === true)) &&
|
||||||
|
!useDataPlaneRbac.getState().aadTokenUpdated;
|
||||||
|
setShowDataPlaneRBACWarning(shouldShowWarning);
|
||||||
|
};
|
||||||
|
|
||||||
const handleOnRUThresholdToggleChange = (ev: React.MouseEvent<HTMLElement>, checked?: boolean): void => {
|
const handleOnRUThresholdToggleChange = (ev: React.MouseEvent<HTMLElement>, checked?: boolean): void => {
|
||||||
setRUThresholdEnabled(checked);
|
setRUThresholdEnabled(checked);
|
||||||
};
|
};
|
||||||
@@ -379,6 +452,54 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{userContext.apiType === "SQL" && userContext.authType === AuthType.AAD && (
|
||||||
|
<>
|
||||||
|
<div className="settingsSection">
|
||||||
|
<div className="settingsSectionPart">
|
||||||
|
<fieldset>
|
||||||
|
<legend id="enableDataPlaneRBACOptions" className="settingsSectionLabel legendLabel">
|
||||||
|
Enable Entra ID RBAC
|
||||||
|
</legend>
|
||||||
|
<TooltipHost
|
||||||
|
content={
|
||||||
|
<>
|
||||||
|
Choose Automatic to enable Entra ID RBAC automatically. True/False to force enable/disable Entra
|
||||||
|
ID RBAC.
|
||||||
|
<a
|
||||||
|
href="https://learn.microsoft.com/en-us/azure/cosmos-db/how-to-setup-rbac#use-data-explorer"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
{" "}
|
||||||
|
Learn more{" "}
|
||||||
|
</a>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Icon iconName="Info" ariaLabel="Info tooltip" className="panelInfoIcon" tabIndex={0} />
|
||||||
|
</TooltipHost>
|
||||||
|
{showDataPlaneRBACWarning && configContext.platform === Platform.Portal && (
|
||||||
|
<MessageBar
|
||||||
|
messageBarType={MessageBarType.warning}
|
||||||
|
isMultiline={true}
|
||||||
|
onDismiss={() => setShowDataPlaneRBACWarning(false)}
|
||||||
|
dismissButtonAriaLabel="Close"
|
||||||
|
>
|
||||||
|
Please click on "Login for Entra ID RBAC" prior to performing Entra ID RBAC operations
|
||||||
|
</MessageBar>
|
||||||
|
)}
|
||||||
|
<ChoiceGroup
|
||||||
|
ariaLabelledBy="enableDataPlaneRBACOptions"
|
||||||
|
options={dataPlaneRBACOptionsList}
|
||||||
|
styles={choiceButtonStyles}
|
||||||
|
selectedKey={enableDataPlaneRBACOption}
|
||||||
|
onChange={handleOnDataPlaneRBACOptionChange}
|
||||||
|
/>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
{userContext.apiType === "SQL" && (
|
{userContext.apiType === "SQL" && (
|
||||||
<>
|
<>
|
||||||
<div className="settingsSection">
|
<div className="settingsSection">
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import "@testing-library/jest-dom/extend-expect";
|
||||||
|
import { RenderResult, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||||
|
import { VectorEmbedding, VectorIndex } from "Contracts/DataModels";
|
||||||
|
import React from "react";
|
||||||
|
import { AddVectorEmbeddingPolicyForm } from "./AddVectorEmbeddingPolicyForm";
|
||||||
|
|
||||||
|
const mockVectorEmbedding: VectorEmbedding[] = [
|
||||||
|
{ path: "/vector1", dataType: "float32", distanceFunction: "euclidean", dimensions: 0 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const mockVectorIndex: VectorIndex[] = [{ path: "/vector1", type: "flat" }];
|
||||||
|
|
||||||
|
const mockOnVectorEmbeddingChange = jest.fn();
|
||||||
|
|
||||||
|
describe("AddVectorEmbeddingPolicyForm", () => {
|
||||||
|
let component: RenderResult;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
component = render(
|
||||||
|
<AddVectorEmbeddingPolicyForm
|
||||||
|
vectorEmbedding={mockVectorEmbedding}
|
||||||
|
vectorIndex={mockVectorIndex}
|
||||||
|
onVectorEmbeddingChange={mockOnVectorEmbeddingChange}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("renders correctly", () => {
|
||||||
|
expect(screen.getByText("Vector embedding 1")).toBeInTheDocument();
|
||||||
|
expect(screen.getByPlaceholderText("/vector1")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("calls onVectorEmbeddingChange on adding a new vector embedding", () => {
|
||||||
|
fireEvent.click(screen.getByText("Add vector embedding"));
|
||||||
|
expect(mockOnVectorEmbeddingChange).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("calls onDelete when delete button is clicked", async () => {
|
||||||
|
const deleteButton = component.container.querySelector("#delete-vector-policy-1");
|
||||||
|
fireEvent.click(deleteButton);
|
||||||
|
expect(mockOnVectorEmbeddingChange).toHaveBeenCalled();
|
||||||
|
expect(screen.queryByText("Vector embedding 1")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("calls onVectorEmbeddingPathChange on input change", () => {
|
||||||
|
fireEvent.change(screen.getByPlaceholderText("/vector1"), { target: { value: "/newPath" } });
|
||||||
|
expect(mockOnVectorEmbeddingChange).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("validates input correctly", async () => {
|
||||||
|
fireEvent.change(screen.getByPlaceholderText("/vector1"), { target: { value: "" } });
|
||||||
|
await waitFor(() => expect(screen.getByText("Vector embedding path should not be empty")).toBeInTheDocument(), {
|
||||||
|
timeout: 1500,
|
||||||
|
});
|
||||||
|
await waitFor(
|
||||||
|
() =>
|
||||||
|
expect(
|
||||||
|
screen.getByText("Vector embedding dimension must be greater than 0 and less than or equal 4096"),
|
||||||
|
).toBeInTheDocument(),
|
||||||
|
{
|
||||||
|
timeout: 1500,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
fireEvent.change(component.container.querySelector("#vector-policy-dimension-1"), { target: { value: "4096" } });
|
||||||
|
fireEvent.change(screen.getByPlaceholderText("/vector1"), { target: { value: "/vector1" } });
|
||||||
|
await waitFor(() => expect(screen.queryByText("Vector embedding path should not be empty")).toBeNull(), {
|
||||||
|
timeout: 1500,
|
||||||
|
});
|
||||||
|
await waitFor(
|
||||||
|
() => expect(screen.queryByText("Maximum allowed dimension for flat index is 505")).toBeInTheDocument(),
|
||||||
|
{
|
||||||
|
timeout: 1500,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("duplicate vector path is not allowed", async () => {
|
||||||
|
fireEvent.click(screen.getByText("Add vector embedding"));
|
||||||
|
fireEvent.change(component.container.querySelector("#vector-policy-path-2"), { target: { value: "/vector1" } });
|
||||||
|
await waitFor(() => expect(screen.queryByText("Vector embedding path is already defined")).toBeNull(), {
|
||||||
|
timeout: 1500,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,300 @@
|
|||||||
|
import {
|
||||||
|
DefaultButton,
|
||||||
|
Dropdown,
|
||||||
|
IDropdownOption,
|
||||||
|
IStyleFunctionOrObject,
|
||||||
|
ITextFieldStyleProps,
|
||||||
|
ITextFieldStyles,
|
||||||
|
IconButton,
|
||||||
|
Label,
|
||||||
|
Stack,
|
||||||
|
TextField,
|
||||||
|
} from "@fluentui/react";
|
||||||
|
import { VectorEmbedding, VectorIndex } from "Contracts/DataModels";
|
||||||
|
import { CollapsibleSectionComponent } from "Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent";
|
||||||
|
import {
|
||||||
|
getDataTypeOptions,
|
||||||
|
getDistanceFunctionOptions,
|
||||||
|
getIndexTypeOptions,
|
||||||
|
} from "Explorer/Panes/VectorSearchPanel/VectorSearchUtils";
|
||||||
|
import React, { FunctionComponent, useState } from "react";
|
||||||
|
|
||||||
|
export interface IAddVectorEmbeddingPolicyFormProps {
|
||||||
|
vectorEmbedding: VectorEmbedding[];
|
||||||
|
vectorIndex: VectorIndex[];
|
||||||
|
onVectorEmbeddingChange: (
|
||||||
|
vectorEmbeddings: VectorEmbedding[],
|
||||||
|
vectorIndexingPolicies: VectorIndex[],
|
||||||
|
validationPassed: boolean,
|
||||||
|
) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VectorEmbeddingPolicyData {
|
||||||
|
path: string;
|
||||||
|
dataType: VectorEmbedding["dataType"];
|
||||||
|
distanceFunction: VectorEmbedding["distanceFunction"];
|
||||||
|
dimensions: number;
|
||||||
|
indexType: VectorIndex["type"] | "none";
|
||||||
|
pathError: string;
|
||||||
|
dimensionsError: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type VectorEmbeddingPolicyProperty = "dataType" | "distanceFunction" | "indexType";
|
||||||
|
|
||||||
|
const textFieldStyles: IStyleFunctionOrObject<ITextFieldStyleProps, ITextFieldStyles> = {
|
||||||
|
fieldGroup: {
|
||||||
|
height: 27,
|
||||||
|
},
|
||||||
|
field: {
|
||||||
|
fontSize: 12,
|
||||||
|
padding: "0 8px",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const dropdownStyles = {
|
||||||
|
title: {
|
||||||
|
height: 27,
|
||||||
|
lineHeight: "24px",
|
||||||
|
fontSize: 12,
|
||||||
|
},
|
||||||
|
dropdown: {
|
||||||
|
height: 27,
|
||||||
|
lineHeight: "24px",
|
||||||
|
},
|
||||||
|
dropdownItem: {
|
||||||
|
fontSize: 12,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AddVectorEmbeddingPolicyForm: FunctionComponent<IAddVectorEmbeddingPolicyFormProps> = ({
|
||||||
|
vectorEmbedding,
|
||||||
|
vectorIndex,
|
||||||
|
onVectorEmbeddingChange,
|
||||||
|
}): JSX.Element => {
|
||||||
|
const onVectorEmbeddingPathError = (path: string, index?: number): string => {
|
||||||
|
let error = "";
|
||||||
|
if (!path) {
|
||||||
|
error = "Vector embedding path should not be empty";
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
index >= 0 &&
|
||||||
|
vectorEmbeddingPolicyData?.find(
|
||||||
|
(vectorEmbedding: VectorEmbeddingPolicyData, dataIndex: number) =>
|
||||||
|
dataIndex !== index && vectorEmbedding.path === path,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
error = "Vector embedding path is already defined";
|
||||||
|
}
|
||||||
|
return error;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onVectorEmbeddingDimensionError = (dimension: number, indexType: VectorIndex["type"] | "none"): string => {
|
||||||
|
let error = "";
|
||||||
|
if (dimension <= 0 || dimension > 4096) {
|
||||||
|
error = "Vector embedding dimension must be greater than 0 and less than or equal 4096";
|
||||||
|
}
|
||||||
|
if (indexType === "flat" && dimension > 505) {
|
||||||
|
error = "Maximum allowed dimension for flat index is 505";
|
||||||
|
}
|
||||||
|
return error;
|
||||||
|
};
|
||||||
|
|
||||||
|
const initializeData = (vectorEmbedding: VectorEmbedding[], vectorIndex: VectorIndex[]) => {
|
||||||
|
const mergedData: VectorEmbeddingPolicyData[] = [];
|
||||||
|
vectorEmbedding.forEach((embedding) => {
|
||||||
|
const matchingIndex = vectorIndex.find((index) => index.path === embedding.path);
|
||||||
|
mergedData.push({
|
||||||
|
...embedding,
|
||||||
|
indexType: matchingIndex?.type || "none",
|
||||||
|
pathError: onVectorEmbeddingPathError(embedding.path),
|
||||||
|
dimensionsError: onVectorEmbeddingDimensionError(embedding.dimensions, matchingIndex?.type || "none"),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return mergedData;
|
||||||
|
};
|
||||||
|
|
||||||
|
const [vectorEmbeddingPolicyData, setVectorEmbeddingPolicyData] = useState<VectorEmbeddingPolicyData[]>(
|
||||||
|
initializeData(vectorEmbedding, vectorIndex),
|
||||||
|
);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
propagateData();
|
||||||
|
}, [vectorEmbeddingPolicyData]);
|
||||||
|
|
||||||
|
const propagateData = () => {
|
||||||
|
const vectorEmbeddings: VectorEmbedding[] = vectorEmbeddingPolicyData.map((policy: VectorEmbeddingPolicyData) => ({
|
||||||
|
dataType: policy.dataType,
|
||||||
|
dimensions: policy.dimensions,
|
||||||
|
distanceFunction: policy.distanceFunction,
|
||||||
|
path: policy.path,
|
||||||
|
}));
|
||||||
|
const vectorIndexingPolicies: VectorIndex[] = vectorEmbeddingPolicyData
|
||||||
|
.filter((policy: VectorEmbeddingPolicyData) => policy.indexType !== "none")
|
||||||
|
.map(
|
||||||
|
(policy) =>
|
||||||
|
({
|
||||||
|
path: policy.path,
|
||||||
|
type: policy.indexType,
|
||||||
|
}) as VectorIndex,
|
||||||
|
);
|
||||||
|
const validationPassed = vectorEmbeddingPolicyData.every(
|
||||||
|
(policy: VectorEmbeddingPolicyData) => policy.pathError === "" && policy.dimensionsError === "",
|
||||||
|
);
|
||||||
|
onVectorEmbeddingChange(vectorEmbeddings, vectorIndexingPolicies, validationPassed);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onVectorEmbeddingPathChange = (index: number, event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const value = event.target.value.trim();
|
||||||
|
const vectorEmbeddings = [...vectorEmbeddingPolicyData];
|
||||||
|
if (!vectorEmbeddings[index]?.path && !value.startsWith("/")) {
|
||||||
|
vectorEmbeddings[index].path = "/" + value;
|
||||||
|
} else {
|
||||||
|
vectorEmbeddings[index].path = value;
|
||||||
|
}
|
||||||
|
const error = onVectorEmbeddingPathError(value, index);
|
||||||
|
vectorEmbeddings[index].pathError = error;
|
||||||
|
setVectorEmbeddingPolicyData(vectorEmbeddings);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onVectorEmbeddingDimensionsChange = (index: number, event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const value = parseInt(event.target.value.trim()) || 0;
|
||||||
|
const vectorEmbeddings = [...vectorEmbeddingPolicyData];
|
||||||
|
const vectorEmbedding = vectorEmbeddings[index];
|
||||||
|
vectorEmbeddings[index].dimensions = value;
|
||||||
|
const error = onVectorEmbeddingDimensionError(value, vectorEmbedding.indexType);
|
||||||
|
vectorEmbeddings[index].dimensionsError = error;
|
||||||
|
setVectorEmbeddingPolicyData(vectorEmbeddings);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onVectorEmbeddingIndexTypeChange = (index: number, option: IDropdownOption): void => {
|
||||||
|
const vectorEmbeddings = [...vectorEmbeddingPolicyData];
|
||||||
|
const vectorEmbedding = vectorEmbeddings[index];
|
||||||
|
vectorEmbeddings[index].indexType = option.key as never;
|
||||||
|
const error = onVectorEmbeddingDimensionError(vectorEmbedding.dimensions, vectorEmbedding.indexType);
|
||||||
|
vectorEmbeddings[index].dimensionsError = error;
|
||||||
|
setVectorEmbeddingPolicyData(vectorEmbeddings);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onVectorEmbeddingPolicyChange = (
|
||||||
|
index: number,
|
||||||
|
option: IDropdownOption,
|
||||||
|
property: VectorEmbeddingPolicyProperty,
|
||||||
|
): void => {
|
||||||
|
const vectorEmbeddings = [...vectorEmbeddingPolicyData];
|
||||||
|
vectorEmbeddings[index][property] = option.key as never;
|
||||||
|
setVectorEmbeddingPolicyData(vectorEmbeddings);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onAdd = () => {
|
||||||
|
setVectorEmbeddingPolicyData([
|
||||||
|
...vectorEmbeddingPolicyData,
|
||||||
|
{
|
||||||
|
path: "",
|
||||||
|
dataType: "float32",
|
||||||
|
distanceFunction: "euclidean",
|
||||||
|
dimensions: 0,
|
||||||
|
indexType: "none",
|
||||||
|
pathError: onVectorEmbeddingPathError(""),
|
||||||
|
dimensionsError: onVectorEmbeddingDimensionError(0, "none"),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDelete = (index: number) => {
|
||||||
|
const vectorEmbeddings = vectorEmbeddingPolicyData.filter((_uniqueKey, j) => index !== j);
|
||||||
|
setVectorEmbeddingPolicyData(vectorEmbeddings);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack tokens={{ childrenGap: 4 }}>
|
||||||
|
{vectorEmbeddingPolicyData.length > 0 &&
|
||||||
|
vectorEmbeddingPolicyData.map((vectorEmbeddingPolicy: VectorEmbeddingPolicyData, index: number) => (
|
||||||
|
<CollapsibleSectionComponent key={index} isExpandedByDefault={true} title={`Vector embedding ${index + 1}`}>
|
||||||
|
<Stack horizontal tokens={{ childrenGap: 4 }}>
|
||||||
|
<Stack
|
||||||
|
styles={{
|
||||||
|
root: {
|
||||||
|
margin: "0 0 6px 20px !important",
|
||||||
|
paddingLeft: 20,
|
||||||
|
width: "80%",
|
||||||
|
borderLeft: "1px solid",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack>
|
||||||
|
<Label styles={{ root: { fontSize: 12 } }}>Path</Label>
|
||||||
|
<TextField
|
||||||
|
id={`vector-policy-path-${index + 1}`}
|
||||||
|
required={true}
|
||||||
|
placeholder="/vector1"
|
||||||
|
styles={textFieldStyles}
|
||||||
|
onChange={(event: React.ChangeEvent<HTMLInputElement>) => onVectorEmbeddingPathChange(index, event)}
|
||||||
|
value={vectorEmbeddingPolicy.path || ""}
|
||||||
|
errorMessage={vectorEmbeddingPolicy.pathError}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
<Stack>
|
||||||
|
<Label styles={{ root: { fontSize: 12 } }}>Data type</Label>
|
||||||
|
<Dropdown
|
||||||
|
required={true}
|
||||||
|
styles={dropdownStyles}
|
||||||
|
options={getDataTypeOptions()}
|
||||||
|
selectedKey={vectorEmbeddingPolicy.dataType}
|
||||||
|
onChange={(_event: React.FormEvent<HTMLDivElement>, option: IDropdownOption) =>
|
||||||
|
onVectorEmbeddingPolicyChange(index, option, "dataType")
|
||||||
|
}
|
||||||
|
></Dropdown>
|
||||||
|
</Stack>
|
||||||
|
<Stack>
|
||||||
|
<Label styles={{ root: { fontSize: 12 } }}>Distance function</Label>
|
||||||
|
<Dropdown
|
||||||
|
required={true}
|
||||||
|
styles={dropdownStyles}
|
||||||
|
options={getDistanceFunctionOptions()}
|
||||||
|
selectedKey={vectorEmbeddingPolicy.distanceFunction}
|
||||||
|
onChange={(_event: React.FormEvent<HTMLDivElement>, option: IDropdownOption) =>
|
||||||
|
onVectorEmbeddingPolicyChange(index, option, "distanceFunction")
|
||||||
|
}
|
||||||
|
></Dropdown>
|
||||||
|
</Stack>
|
||||||
|
<Stack>
|
||||||
|
<Label styles={{ root: { fontSize: 12 } }}>Dimensions</Label>
|
||||||
|
<TextField
|
||||||
|
id={`vector-policy-dimension-${index + 1}`}
|
||||||
|
required={true}
|
||||||
|
styles={textFieldStyles}
|
||||||
|
value={String(vectorEmbeddingPolicy.dimensions || 0)}
|
||||||
|
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
onVectorEmbeddingDimensionsChange(index, event)
|
||||||
|
}
|
||||||
|
errorMessage={vectorEmbeddingPolicy.dimensionsError}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
<Stack>
|
||||||
|
<Label styles={{ root: { fontSize: 12 } }}>Index type</Label>
|
||||||
|
<Dropdown
|
||||||
|
required={true}
|
||||||
|
styles={dropdownStyles}
|
||||||
|
options={getIndexTypeOptions()}
|
||||||
|
selectedKey={vectorEmbeddingPolicy.indexType}
|
||||||
|
onChange={(_event: React.FormEvent<HTMLDivElement>, option: IDropdownOption) =>
|
||||||
|
onVectorEmbeddingIndexTypeChange(index, option)
|
||||||
|
}
|
||||||
|
></Dropdown>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
<IconButton
|
||||||
|
id={`delete-vector-policy-${index + 1}`}
|
||||||
|
iconProps={{ iconName: "Delete" }}
|
||||||
|
style={{ height: 27, margin: "auto" }}
|
||||||
|
onClick={() => onDelete(index)}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</CollapsibleSectionComponent>
|
||||||
|
))}
|
||||||
|
<DefaultButton id={`add-vector-policy`} styles={{ root: { maxWidth: 170, fontSize: 12 } }} onClick={onAdd}>
|
||||||
|
Add vector embedding
|
||||||
|
</DefaultButton>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
16
src/Explorer/Panes/VectorSearchPanel/VectorSearchUtils.ts
Normal file
16
src/Explorer/Panes/VectorSearchPanel/VectorSearchUtils.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { IDropdownOption } from "@fluentui/react";
|
||||||
|
|
||||||
|
const dataTypes = ["float32", "uint8", "int8"];
|
||||||
|
const distanceFunctions = ["euclidean", "cosine", "dotproduct"];
|
||||||
|
const indexTypes = ["none", "flat", "diskANN", "quantizedFlat"];
|
||||||
|
|
||||||
|
export const getDataTypeOptions = (): IDropdownOption[] => createDropdownOptionsFromLiterals(dataTypes);
|
||||||
|
export const getDistanceFunctionOptions = (): IDropdownOption[] => createDropdownOptionsFromLiterals(distanceFunctions);
|
||||||
|
export const getIndexTypeOptions = (): IDropdownOption[] => createDropdownOptionsFromLiterals(indexTypes);
|
||||||
|
|
||||||
|
function createDropdownOptionsFromLiterals<T extends string>(literals: T[]): IDropdownOption[] {
|
||||||
|
return literals.map((value) => ({
|
||||||
|
key: value,
|
||||||
|
text: value,
|
||||||
|
}));
|
||||||
|
}
|
||||||
@@ -504,7 +504,12 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
|||||||
{!showFeedbackBar && (
|
{!showFeedbackBar && (
|
||||||
<Text style={{ fontSize: 12 }}>
|
<Text style={{ fontSize: 12 }}>
|
||||||
AI-generated content can have mistakes. Make sure it's accurate and appropriate before using it.{" "}
|
AI-generated content can have mistakes. Make sure it's accurate and appropriate before using it.{" "}
|
||||||
<Link href="https://aka.ms/cdb-copilot-preview-terms" target="_blank" style={{ color: "#0072D4" }}>
|
<Link
|
||||||
|
href="https://aka.ms/cdb-copilot-preview-terms"
|
||||||
|
target="_blank"
|
||||||
|
style={{ color: "#0072D4" }}
|
||||||
|
className="underlinedLink"
|
||||||
|
>
|
||||||
Read preview terms
|
Read preview terms
|
||||||
</Link>
|
</Link>
|
||||||
{showErrorMessageBar && (
|
{showErrorMessageBar && (
|
||||||
|
|||||||
@@ -355,15 +355,15 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||||||
) : (
|
) : (
|
||||||
<div className="moreStuffContainer">
|
<div className="moreStuffContainer">
|
||||||
<div className="moreStuffColumn commonTasks">
|
<div className="moreStuffColumn commonTasks">
|
||||||
<div className="title">Recents</div>
|
<h2 className="title">Recents</h2>
|
||||||
{this.getRecentItems()}
|
{this.getRecentItems()}
|
||||||
</div>
|
</div>
|
||||||
<div className="moreStuffColumn">
|
<div className="moreStuffColumn">
|
||||||
<div className="title">Top 3 things you need to know</div>
|
<h2 className="title">Top 3 things you need to know</h2>
|
||||||
{this.top3Items()}
|
{this.top3Items()}
|
||||||
</div>
|
</div>
|
||||||
<div className="moreStuffColumn tipsContainer">
|
<div className="moreStuffColumn tipsContainer">
|
||||||
<div className="title">Learning Resources</div>
|
<h2 className="title">Learning Resources</h2>
|
||||||
{this.getLearningResourceItems()}
|
{this.getLearningResourceItems()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ const handleMessage = async (event: MessageEvent): Promise<void> => {
|
|||||||
|
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
ARM_ENDPOINT: normalizeArmEndpoint(inputs.csmEndpoint || configContext.ARM_ENDPOINT),
|
ARM_ENDPOINT: normalizeArmEndpoint(inputs.csmEndpoint || configContext.ARM_ENDPOINT),
|
||||||
|
CATALOG_API_KEY: inputs.catalogAPIKey,
|
||||||
});
|
});
|
||||||
|
|
||||||
updateUserContext({
|
updateUserContext({
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
import { configContext } from "../../ConfigContext";
|
import { configContext } from "../../ConfigContext";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
import { armRequestWithoutPolling } from "../../Utils/arm/request";
|
import { get } from "../../Utils/arm/generatedClients/cosmos/locations";
|
||||||
|
import { armRequestWithoutPolling, getOfferingIdsRequest } from "../../Utils/arm/request";
|
||||||
import { selfServeTraceFailure, selfServeTraceStart, selfServeTraceSuccess } from "../SelfServeTelemetryProcessor";
|
import { selfServeTraceFailure, selfServeTraceStart, selfServeTraceSuccess } from "../SelfServeTelemetryProcessor";
|
||||||
import { RefreshResult } from "../SelfServeTypes";
|
import { RefreshResult } from "../SelfServeTypes";
|
||||||
import SqlX from "./SqlX";
|
import SqlX from "./SqlX";
|
||||||
import {
|
import {
|
||||||
FetchPricesResponse,
|
FetchPricesResponse,
|
||||||
|
GetOfferingIdsResponse,
|
||||||
|
OfferingIdMap,
|
||||||
|
OfferingIdRequest,
|
||||||
PriceMapAndCurrencyCode,
|
PriceMapAndCurrencyCode,
|
||||||
RegionItem,
|
RegionItem,
|
||||||
RegionsResponse,
|
RegionsResponse,
|
||||||
@@ -166,11 +170,21 @@ export const getRegions = async (): Promise<Array<RegionItem>> => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getRegionShortName = async (regionDisplayName: string): Promise<string> => {
|
||||||
|
const locationsList = await get(userContext.subscriptionId, regionDisplayName);
|
||||||
|
|
||||||
|
if ("id" in locationsList) {
|
||||||
|
const locationId = locationsList.id;
|
||||||
|
return locationId.substring(locationId.lastIndexOf("/") + 1);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
const getFetchPricesPathForRegion = (subscriptionId: string): string => {
|
const getFetchPricesPathForRegion = (subscriptionId: string): string => {
|
||||||
return `/subscriptions/${subscriptionId}/providers/Microsoft.CostManagement/fetchPrices`;
|
return `/subscriptions/${subscriptionId}/providers/Microsoft.CostManagement/fetchPrices`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getPriceMapAndCurrencyCode = async (regions: Array<RegionItem>): Promise<PriceMapAndCurrencyCode> => {
|
export const getPriceMapAndCurrencyCode = async (map: OfferingIdMap): Promise<PriceMapAndCurrencyCode> => {
|
||||||
const telemetryData = {
|
const telemetryData = {
|
||||||
feature: "Calculate approximate cost",
|
feature: "Calculate approximate cost",
|
||||||
function: "getPriceMapAndCurrencyCode",
|
function: "getPriceMapAndCurrencyCode",
|
||||||
@@ -181,39 +195,94 @@ export const getPriceMapAndCurrencyCode = async (regions: Array<RegionItem>): Pr
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const priceMap = new Map<string, Map<string, number>>();
|
const priceMap = new Map<string, Map<string, number>>();
|
||||||
let currencyCode;
|
let billingCurrency;
|
||||||
for (const regionItem of regions) {
|
for (const region of map.keys()) {
|
||||||
const regionPriceMap = new Map<string, number>();
|
const regionPriceMap = new Map<string, number>();
|
||||||
|
const regionShortName = await getRegionShortName(region);
|
||||||
|
const requestBody: OfferingIdRequest = {
|
||||||
|
location: regionShortName,
|
||||||
|
ids: Array.from(map.get(region).keys()),
|
||||||
|
};
|
||||||
|
|
||||||
const response = await armRequestWithoutPolling<FetchPricesResponse>({
|
const response = await armRequestWithoutPolling<FetchPricesResponse>({
|
||||||
host: configContext.ARM_ENDPOINT,
|
host: configContext.ARM_ENDPOINT,
|
||||||
path: getFetchPricesPathForRegion(userContext.subscriptionId),
|
path: getFetchPricesPathForRegion(userContext.subscriptionId),
|
||||||
method: "POST",
|
method: "POST",
|
||||||
apiVersion: "2020-01-01-preview",
|
apiVersion: "2023-04-01-preview",
|
||||||
queryParams: {
|
body: requestBody,
|
||||||
filter:
|
|
||||||
"armRegionName eq '" +
|
|
||||||
regionItem.locationName.split(" ").join("").toLowerCase() +
|
|
||||||
"' and serviceFamily eq 'Databases' and productName eq 'Azure Cosmos DB Dedicated Gateway - General Purpose'",
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const item of response.result.Items) {
|
for (const item of response.result) {
|
||||||
if (currencyCode === undefined) {
|
if (item.error) {
|
||||||
currencyCode = item.currencyCode;
|
continue;
|
||||||
} else if (item.currencyCode !== currencyCode) {
|
}
|
||||||
|
|
||||||
|
if (billingCurrency === undefined) {
|
||||||
|
billingCurrency = item.billingCurrency;
|
||||||
|
} else if (item.billingCurrency !== billingCurrency) {
|
||||||
throw Error("Currency Code Mismatch: Currency code not same for all regions / skus.");
|
throw Error("Currency Code Mismatch: Currency code not same for all regions / skus.");
|
||||||
}
|
}
|
||||||
regionPriceMap.set(item.skuName, item.retailPrice);
|
|
||||||
|
const offeringId = item.id;
|
||||||
|
const skuName = map.get(region).get(offeringId);
|
||||||
|
const unitPriceinBillingCurrency = item.prices.find((x) => x.type === "Consumption")
|
||||||
|
?.unitPriceinBillingCurrency;
|
||||||
|
regionPriceMap.set(skuName, unitPriceinBillingCurrency);
|
||||||
}
|
}
|
||||||
priceMap.set(regionItem.locationName, regionPriceMap);
|
priceMap.set(region, regionPriceMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
selfServeTraceSuccess(telemetryData, getPriceMapAndCurrencyCodeTimestamp);
|
selfServeTraceSuccess(telemetryData, getPriceMapAndCurrencyCodeTimestamp);
|
||||||
return { priceMap: priceMap, currencyCode: currencyCode };
|
return { priceMap: priceMap, billingCurrency: billingCurrency };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const failureTelemetry = { err, selfServeClassName: SqlX.name };
|
const failureTelemetry = { err, selfServeClassName: SqlX.name };
|
||||||
selfServeTraceFailure(failureTelemetry, getPriceMapAndCurrencyCodeTimestamp);
|
selfServeTraceFailure(failureTelemetry, getPriceMapAndCurrencyCodeTimestamp);
|
||||||
return { priceMap: undefined, currencyCode: undefined };
|
return { priceMap: undefined, billingCurrency: undefined };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getOfferingIdPathForRegion = (): string => {
|
||||||
|
return `/skus?serviceFamily=Databases&service=Azure Cosmos DB`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getOfferingIds = async (regions: Array<RegionItem>): Promise<OfferingIdMap> => {
|
||||||
|
const telemetryData = {
|
||||||
|
feature: "Get Offering Ids to calculate approximate cost",
|
||||||
|
function: "getOfferingIds",
|
||||||
|
description: "fetch offering ids API call",
|
||||||
|
selfServeClassName: SqlX.name,
|
||||||
|
};
|
||||||
|
const getOfferingIdsCodeTimestamp = selfServeTraceStart(telemetryData);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const offeringIdMap = new Map<string, Map<string, string>>();
|
||||||
|
for (const regionItem of regions) {
|
||||||
|
const regionOfferingIdMap = new Map<string, string>();
|
||||||
|
const regionShortName = await getRegionShortName(regionItem.locationName);
|
||||||
|
|
||||||
|
const response = await getOfferingIdsRequest<GetOfferingIdsResponse>({
|
||||||
|
host: configContext.CATALOG_ENDPOINT,
|
||||||
|
path: getOfferingIdPathForRegion(),
|
||||||
|
method: "GET",
|
||||||
|
apiVersion: "2023-05-01-preview",
|
||||||
|
queryParams: {
|
||||||
|
filter: "armRegionName eq '" + regionShortName + "'",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const item of response.result.items) {
|
||||||
|
if (item.offeringProperties?.length > 0) {
|
||||||
|
regionOfferingIdMap.set(item.offeringProperties[0].offeringId, item.skuName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
offeringIdMap.set(regionItem.locationName, regionOfferingIdMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
selfServeTraceSuccess(telemetryData, getOfferingIdsCodeTimestamp);
|
||||||
|
return offeringIdMap;
|
||||||
|
} catch (err) {
|
||||||
|
const failureTelemetry = { err, selfServeClassName: SqlX.name };
|
||||||
|
selfServeTraceFailure(failureTelemetry, getOfferingIdsCodeTimestamp);
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import { BladeType, generateBladeLink } from "../SelfServeUtils";
|
|||||||
import {
|
import {
|
||||||
deleteDedicatedGatewayResource,
|
deleteDedicatedGatewayResource,
|
||||||
getCurrentProvisioningState,
|
getCurrentProvisioningState,
|
||||||
|
getOfferingIds,
|
||||||
getPriceMapAndCurrencyCode,
|
getPriceMapAndCurrencyCode,
|
||||||
getRegions,
|
getRegions,
|
||||||
refreshDedicatedGatewayProvisioning,
|
refreshDedicatedGatewayProvisioning,
|
||||||
@@ -370,9 +371,10 @@ export default class SqlX extends SelfServeBaseClass {
|
|||||||
});
|
});
|
||||||
|
|
||||||
regions = await getRegions();
|
regions = await getRegions();
|
||||||
const priceMapAndCurrencyCode = await getPriceMapAndCurrencyCode(regions);
|
const offeringIdMap = await getOfferingIds(regions);
|
||||||
|
const priceMapAndCurrencyCode = await getPriceMapAndCurrencyCode(offeringIdMap);
|
||||||
priceMap = priceMapAndCurrencyCode.priceMap;
|
priceMap = priceMapAndCurrencyCode.priceMap;
|
||||||
currencyCode = priceMapAndCurrencyCode.currencyCode;
|
currencyCode = priceMapAndCurrencyCode.billingCurrency;
|
||||||
|
|
||||||
const response = await getCurrentProvisioningState();
|
const response = await getCurrentProvisioningState();
|
||||||
if (response.status && response.status !== "Deleting") {
|
if (response.status && response.status !== "Deleting") {
|
||||||
|
|||||||
@@ -30,23 +30,51 @@ export type UpdateDedicatedGatewayRequestProperties = {
|
|||||||
serviceType: string;
|
serviceType: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FetchPricesResponse = {
|
export type FetchPricesResponse = Array<PriceItem>;
|
||||||
Items: Array<PriceItem>;
|
|
||||||
NextPageLink: string | undefined;
|
export type PriceItem = {
|
||||||
Count: number;
|
prices: Array<PriceType>;
|
||||||
|
id: string;
|
||||||
|
billingCurrency: string;
|
||||||
|
error: PriceError;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PriceType = {
|
||||||
|
type: string;
|
||||||
|
unitPriceinBillingCurrency: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PriceError = {
|
||||||
|
type: string;
|
||||||
|
description: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PriceMapAndCurrencyCode = {
|
export type PriceMapAndCurrencyCode = {
|
||||||
priceMap: Map<string, Map<string, number>>;
|
priceMap: Map<string, Map<string, number>>;
|
||||||
currencyCode: string;
|
billingCurrency: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PriceItem = {
|
export type GetOfferingIdsResponse = {
|
||||||
retailPrice: number;
|
items: Array<OfferingIdItem>;
|
||||||
skuName: string;
|
nextPageLink: string | undefined;
|
||||||
currencyCode: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type OfferingIdItem = {
|
||||||
|
skuName: string;
|
||||||
|
offeringProperties: Array<OfferingProperties>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type OfferingProperties = {
|
||||||
|
offeringId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type OfferingIdRequest = {
|
||||||
|
ids: Array<string>;
|
||||||
|
location: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type OfferingIdMap = Map<string, Map<string, string>>;
|
||||||
|
|
||||||
export type RegionsResponse = {
|
export type RegionsResponse = {
|
||||||
properties: RegionsProperties;
|
properties: RegionsProperties;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import * as StringUtility from "./StringUtility";
|
|||||||
export { LocalStorageUtility, SessionStorageUtility };
|
export { LocalStorageUtility, SessionStorageUtility };
|
||||||
export enum StorageKey {
|
export enum StorageKey {
|
||||||
ActualItemPerPage,
|
ActualItemPerPage,
|
||||||
|
DataPlaneRbacEnabled,
|
||||||
RUThresholdEnabled,
|
RUThresholdEnabled,
|
||||||
RUThreshold,
|
RUThreshold,
|
||||||
QueryTimeoutEnabled,
|
QueryTimeoutEnabled,
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ export interface UserContext {
|
|||||||
sampleDataConnectionInfo?: ParsedResourceTokenConnectionString;
|
sampleDataConnectionInfo?: ParsedResourceTokenConnectionString;
|
||||||
readonly vcoreMongoConnectionParams?: VCoreMongoConnectionParams;
|
readonly vcoreMongoConnectionParams?: VCoreMongoConnectionParams;
|
||||||
readonly feedbackPolicies?: AdminFeedbackPolicySettings;
|
readonly feedbackPolicies?: AdminFeedbackPolicySettings;
|
||||||
|
readonly dataPlaneRbacEnabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ApiType = "SQL" | "Mongo" | "Gremlin" | "Tables" | "Cassandra" | "Postgres" | "VCoreMongo";
|
export type ApiType = "SQL" | "Mongo" | "Gremlin" | "Tables" | "Cassandra" | "Postgres" | "VCoreMongo";
|
||||||
|
|||||||
@@ -160,3 +160,52 @@ async function getOperationStatus(operationStatusUrl: string) {
|
|||||||
}
|
}
|
||||||
throw new Error(`Operation Response: ${JSON.stringify(body)}. Retrying.`);
|
throw new Error(`Operation Response: ${JSON.stringify(body)}. Retrying.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getOfferingIdsRequest<T>({
|
||||||
|
host,
|
||||||
|
path,
|
||||||
|
apiVersion,
|
||||||
|
method,
|
||||||
|
body: requestBody,
|
||||||
|
queryParams,
|
||||||
|
}: Options): Promise<{ result: T; operationStatusUrl: string }> {
|
||||||
|
const url = new URL(path, host);
|
||||||
|
url.searchParams.append("api-version", configContext.armAPIVersion || apiVersion);
|
||||||
|
if (queryParams) {
|
||||||
|
queryParams.filter && url.searchParams.append("$filter", queryParams.filter);
|
||||||
|
queryParams.metricNames && url.searchParams.append("metricnames", queryParams.metricNames);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!configContext.CATALOG_API_KEY) {
|
||||||
|
throw new Error("No catalog API key provided");
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await window.fetch(url.href, {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
[HttpHeaders.xAPIKey]: configContext.CATALOG_API_KEY,
|
||||||
|
},
|
||||||
|
body: requestBody ? JSON.stringify(requestBody) : undefined,
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
let error: ARMError;
|
||||||
|
try {
|
||||||
|
const errorResponse = (await response.json()) as ParsedErrorResponse;
|
||||||
|
if ("error" in errorResponse) {
|
||||||
|
error = new ARMError(errorResponse.error.message);
|
||||||
|
error.code = errorResponse.error.code;
|
||||||
|
} else {
|
||||||
|
error = new ARMError(errorResponse.message);
|
||||||
|
error.code = errorResponse.code;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(await response.text());
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const operationStatusUrl = (response.headers && response.headers.get("location")) || "";
|
||||||
|
const responseBody = (await response.json()) as T;
|
||||||
|
return { result: responseBody, operationStatusUrl: operationStatusUrl };
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import * as Constants from "Common/Constants";
|
||||||
import { createUri } from "Common/UrlUtility";
|
import { createUri } from "Common/UrlUtility";
|
||||||
import { DATA_EXPLORER_RPC_VERSION } from "Contracts/DataExplorerMessagesContract";
|
import { DATA_EXPLORER_RPC_VERSION } from "Contracts/DataExplorerMessagesContract";
|
||||||
import { FabricMessageTypes } from "Contracts/FabricMessageTypes";
|
import { FabricMessageTypes } from "Contracts/FabricMessageTypes";
|
||||||
@@ -5,6 +6,7 @@ import { FABRIC_RPC_VERSION, FabricMessageV2 } from "Contracts/FabricMessagesCon
|
|||||||
import Explorer from "Explorer/Explorer";
|
import Explorer from "Explorer/Explorer";
|
||||||
import { useSelectedNode } from "Explorer/useSelectedNode";
|
import { useSelectedNode } from "Explorer/useSelectedNode";
|
||||||
import { scheduleRefreshDatabaseResourceToken } from "Platform/Fabric/FabricUtil";
|
import { scheduleRefreshDatabaseResourceToken } from "Platform/Fabric/FabricUtil";
|
||||||
|
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||||
import { getNetworkSettingsWarningMessage } from "Utils/NetworkUtility";
|
import { getNetworkSettingsWarningMessage } from "Utils/NetworkUtility";
|
||||||
import { logConsoleError } from "Utils/NotificationConsoleUtils";
|
import { logConsoleError } from "Utils/NotificationConsoleUtils";
|
||||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
@@ -41,6 +43,7 @@ import { isInvalidParentFrameOrigin, shouldProcessMessage } from "../Utils/Messa
|
|||||||
import { listKeys } from "../Utils/arm/generatedClients/cosmos/databaseAccounts";
|
import { listKeys } from "../Utils/arm/generatedClients/cosmos/databaseAccounts";
|
||||||
import { DatabaseAccountListKeysResult } from "../Utils/arm/generatedClients/cosmos/types";
|
import { DatabaseAccountListKeysResult } from "../Utils/arm/generatedClients/cosmos/types";
|
||||||
import { applyExplorerBindings } from "../applyExplorerBindings";
|
import { applyExplorerBindings } from "../applyExplorerBindings";
|
||||||
|
import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane";
|
||||||
|
|
||||||
// This hook will create a new instance of Explorer.ts and bind it to the DOM
|
// This hook will create a new instance of Explorer.ts and bind it to the DOM
|
||||||
// This hook has a LOT of magic, but ideally we can delete it once we have removed KO and switched entirely to React
|
// This hook has a LOT of magic, but ideally we can delete it once we have removed KO and switched entirely to React
|
||||||
@@ -253,7 +256,6 @@ async function configureHostedWithAAD(config: AAD): Promise<Explorer> {
|
|||||||
const subscriptionId = accountResourceId && accountResourceId.split("subscriptions/")[1].split("/")[0];
|
const subscriptionId = accountResourceId && accountResourceId.split("subscriptions/")[1].split("/")[0];
|
||||||
const resourceGroup = accountResourceId && accountResourceId.split("resourceGroups/")[1].split("/")[0];
|
const resourceGroup = accountResourceId && accountResourceId.split("resourceGroups/")[1].split("/")[0];
|
||||||
let aadToken;
|
let aadToken;
|
||||||
let keys: DatabaseAccountListKeysResult = {};
|
|
||||||
if (account.properties?.documentEndpoint) {
|
if (account.properties?.documentEndpoint) {
|
||||||
const hrefEndpoint = new URL(account.properties.documentEndpoint).href.replace(/\/$/, "/.default");
|
const hrefEndpoint = new URL(account.properties.documentEndpoint).href.replace(/\/$/, "/.default");
|
||||||
const msalInstance = await getMsalInstance();
|
const msalInstance = await getMsalInstance();
|
||||||
@@ -271,8 +273,30 @@ async function configureHostedWithAAD(config: AAD): Promise<Explorer> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
if (!account.properties.disableLocalAuth) {
|
updateUserContext({
|
||||||
keys = await listKeys(subscriptionId, resourceGroup, account.name);
|
databaseAccount: config.databaseAccount,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!userContext.features.enableAadDataPlane) {
|
||||||
|
if (userContext.apiType === "SQL") {
|
||||||
|
if (LocalStorageUtility.hasItem(StorageKey.DataPlaneRbacEnabled)) {
|
||||||
|
const isDataPlaneRbacSetting = LocalStorageUtility.getEntryString(StorageKey.DataPlaneRbacEnabled);
|
||||||
|
|
||||||
|
let dataPlaneRbacEnabled;
|
||||||
|
if (isDataPlaneRbacSetting === Constants.RBACOptions.setAutomaticRBACOption) {
|
||||||
|
dataPlaneRbacEnabled = account.properties.disableLocalAuth;
|
||||||
|
} else {
|
||||||
|
dataPlaneRbacEnabled = isDataPlaneRbacSetting === Constants.RBACOptions.setTrueRBACOption;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateUserContext({ dataPlaneRbacEnabled });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const keys: DatabaseAccountListKeysResult = await listKeys(subscriptionId, resourceGroup, account.name);
|
||||||
|
updateUserContext({
|
||||||
|
masterKey: keys.primaryMasterKey,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (userContext.features.enableAadDataPlane) {
|
if (userContext.features.enableAadDataPlane) {
|
||||||
@@ -285,8 +309,6 @@ async function configureHostedWithAAD(config: AAD): Promise<Explorer> {
|
|||||||
subscriptionId,
|
subscriptionId,
|
||||||
resourceGroup,
|
resourceGroup,
|
||||||
aadToken,
|
aadToken,
|
||||||
databaseAccount: config.databaseAccount,
|
|
||||||
masterKey: keys.primaryMasterKey,
|
|
||||||
});
|
});
|
||||||
const explorer = new Explorer();
|
const explorer = new Explorer();
|
||||||
return explorer;
|
return explorer;
|
||||||
@@ -420,7 +442,7 @@ async function configurePortal(): Promise<Explorer> {
|
|||||||
// In the Portal, configuration of Explorer happens via iframe message
|
// In the Portal, configuration of Explorer happens via iframe message
|
||||||
window.addEventListener(
|
window.addEventListener(
|
||||||
"message",
|
"message",
|
||||||
(event) => {
|
async (event) => {
|
||||||
if (isInvalidParentFrameOrigin(event)) {
|
if (isInvalidParentFrameOrigin(event)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -450,6 +472,29 @@ async function configurePortal(): Promise<Explorer> {
|
|||||||
setTimeout(() => explorer.openNPSSurveyDialog(), 3000);
|
setTimeout(() => explorer.openNPSSurveyDialog(), 3000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { databaseAccount: account, subscriptionId, resourceGroup } = userContext;
|
||||||
|
|
||||||
|
if (userContext.apiType === "SQL") {
|
||||||
|
if (LocalStorageUtility.hasItem(StorageKey.DataPlaneRbacEnabled)) {
|
||||||
|
const isDataPlaneRbacSetting = LocalStorageUtility.getEntryString(StorageKey.DataPlaneRbacEnabled);
|
||||||
|
|
||||||
|
let dataPlaneRbacEnabled;
|
||||||
|
if (isDataPlaneRbacSetting === Constants.RBACOptions.setAutomaticRBACOption) {
|
||||||
|
dataPlaneRbacEnabled = account.properties.disableLocalAuth;
|
||||||
|
} else {
|
||||||
|
dataPlaneRbacEnabled = isDataPlaneRbacSetting === Constants.RBACOptions.setTrueRBACOption;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateUserContext({ dataPlaneRbacEnabled });
|
||||||
|
useDataPlaneRbac.setState({ dataPlaneRbacEnabled: dataPlaneRbacEnabled });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const keys: DatabaseAccountListKeysResult = await listKeys(subscriptionId, resourceGroup, account.name);
|
||||||
|
updateUserContext({
|
||||||
|
masterKey: keys.primaryMasterKey,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (openAction) {
|
if (openAction) {
|
||||||
handleOpenAction(openAction, useDatabases.getState().databases, explorer);
|
handleOpenAction(openAction, useDatabases.getState().databases, explorer);
|
||||||
}
|
}
|
||||||
@@ -490,7 +535,6 @@ function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const authorizationToken = inputs.authorizationToken || "";
|
const authorizationToken = inputs.authorizationToken || "";
|
||||||
const masterKey = inputs.masterKey || "";
|
|
||||||
const databaseAccount = inputs.databaseAccount;
|
const databaseAccount = inputs.databaseAccount;
|
||||||
|
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
@@ -503,7 +547,6 @@ function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) {
|
|||||||
|
|
||||||
updateUserContext({
|
updateUserContext({
|
||||||
authorizationToken,
|
authorizationToken,
|
||||||
masterKey,
|
|
||||||
databaseAccount,
|
databaseAccount,
|
||||||
resourceGroup: inputs.resourceGroup,
|
resourceGroup: inputs.resourceGroup,
|
||||||
subscriptionId: inputs.subscriptionId,
|
subscriptionId: inputs.subscriptionId,
|
||||||
|
|||||||
@@ -33,9 +33,11 @@ All you need to provide is a resource group to deploy in to.
|
|||||||
|
|
||||||
To use this script, there are a few prerequisites that must be done at least once:
|
To use this script, there are a few prerequisites that must be done at least once:
|
||||||
|
|
||||||
1. [Install Azure PowerShell](https://learn.microsoft.com/en-us/powershell/azure/install-azps-windows?view=azps-12.0.0&tabs=powershell&pivots=windows-psgallery) if you don't already have it.
|
1. This script requires Powershell 7+. Install it [here](https://learn.microsoft.com/en-us/powershell/scripting/install/installing-powershell-on-windows).
|
||||||
2. Connect to your Azure account using `Connect-AzAccount`.
|
2. [Install Azure PowerShell](https://learn.microsoft.com/en-us/powershell/azure/install-azps-windows?view=azps-12.0.0&tabs=powershell&pivots=windows-psgallery) if you don't already have it.
|
||||||
3. Ensure you have a Resource Group _ready_ to deploy into, the deploy script requires an existing resource group. This resource group should be named `[username]-e2e-testing`, where `[username]` is your Windows username, (**Microsoft employees:** This should be your alias). The easiest way to do this is by running the `create-resource-group.ps1` script, specifying the Subscription (Name or ID) and Location in which you want to create the Resource Group. For example:
|
3. [Install Bicep CLI](https://learn.microsoft.com/en-us/azure/azure-resource-manager/bicep/install#install-manually) if it is not already installed.
|
||||||
|
4. Connect to your Azure account using `Connect-AzAccount`.
|
||||||
|
5. Ensure you have a Resource Group _ready_ to deploy into, the deploy script requires an existing resource group. This resource group should be named `[username]-e2e-testing`, where `[username]` is your Windows username, (**Microsoft employees:** This should be your alias). The easiest way to do this is by running the `create-resource-group.ps1` script, specifying the Subscription (Name or ID) and Location in which you want to create the Resource Group. For example:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
.\test\resources\create-resource-group.ps1 -SubscriptionName "My Subscription" -Location "West US 3"
|
.\test\resources\create-resource-group.ps1 -SubscriptionName "My Subscription" -Location "West US 3"
|
||||||
@@ -120,6 +122,14 @@ Configuring for E2E Testing
|
|||||||
|
|
||||||
## Running the tests
|
## Running the tests
|
||||||
|
|
||||||
|
If Azure CLI is not installed, please [install it](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli).
|
||||||
|
|
||||||
|
Log into Az CLI with the following command:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
az login --scope https://management.core.windows.net//.default
|
||||||
|
```
|
||||||
|
|
||||||
To run all tests in a headless browser, run the following command from the root of the repo:
|
To run all tests in a headless browser, run the following command from the root of the repo:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ test("Tables CRUD", async ({ page }) => {
|
|||||||
await okButton.click();
|
await okButton.click();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const databaseNode = explorer.treeNode("DATA/TablesDB");
|
||||||
|
await databaseNode.expand();
|
||||||
|
|
||||||
const tableNode = explorer.treeNode(`DATA/TablesDB/${tableId}`);
|
const tableNode = explorer.treeNode(`DATA/TablesDB/${tableId}`);
|
||||||
await expect(tableNode.element).toBeAttached();
|
await expect(tableNode.element).toBeAttached();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user