mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-24 11:21:23 +00:00
Compare commits
1 Commits
add-dp-rba
...
master_clo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e7680c6c9e |
@@ -2296,17 +2296,6 @@ a:link {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.monaco-editor .quick-input-list-label {
|
||||
/* Restore some of Monaco's default styles that are clobbered by our global styles */
|
||||
padding: 0;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.monaco-editor .quick-input-list .highlight {
|
||||
/* Padding in highlighted text within the quick input list breaks the flow of the text */
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
td a {
|
||||
color: #393939;
|
||||
}
|
||||
|
||||
@@ -124,35 +124,6 @@ export enum MongoBackendEndpointType {
|
||||
remote,
|
||||
}
|
||||
|
||||
export class BackendApi {
|
||||
public static readonly GenerateToken: string = "GenerateToken";
|
||||
public static readonly PortalSettings: string = "PortalSettings";
|
||||
}
|
||||
|
||||
export class PortalBackendEndpoints {
|
||||
public static readonly Development: string = "https://localhost:7235";
|
||||
public static readonly Mpac: string = "https://cdb-ms-mpac-pbe.cosmos.azure.com";
|
||||
public static readonly Prod: string = "https://cdb-ms-prod-pbe.cosmos.azure.com";
|
||||
public static readonly Fairfax: string = "https://cdb-ff-prod-pbe.cosmos.azure.us";
|
||||
public static readonly Mooncake: string = "https://cdb-mc-prod-pbe.cosmos.azure.cn";
|
||||
}
|
||||
|
||||
export class MongoProxyEndpoints {
|
||||
public static readonly Development: string = "https://localhost:7238";
|
||||
public static readonly Mpac: string = "https://cdb-ms-mpac-mp.cosmos.azure.com";
|
||||
public static readonly Prod: string = "https://cdb-ms-prod-mp.cosmos.azure.com";
|
||||
public static readonly Fairfax: string = "https://cdb-ff-prod-mp.cosmos.azure.us";
|
||||
public static readonly Mooncake: string = "https://cdb-mc-prod-mp.cosmos.azure.cn";
|
||||
}
|
||||
|
||||
export class CassandraProxyEndpoints {
|
||||
public static readonly Development: string = "https://localhost:7240";
|
||||
public static readonly Mpac: string = "https://cdb-ms-mpac-cp.cosmos.azure.com";
|
||||
public static readonly Prod: string = "https://cdb-ms-prod-cp.cosmos.azure.com";
|
||||
public static readonly Fairfax: string = "https://cdb-ff-prod-cp.cosmos.azure.us";
|
||||
public static readonly Mooncake: string = "https://cdb-mc-prod-cp.cosmos.azure.cn";
|
||||
}
|
||||
|
||||
//TODO: Remove this when new backend is migrated over
|
||||
export class CassandraBackend {
|
||||
public static readonly createOrDeleteApi: string = "api/cassandra/createordelete";
|
||||
@@ -168,7 +139,7 @@ export class CassandraBackend {
|
||||
export class CassandraProxyAPIs {
|
||||
public static readonly createOrDeleteApi: string = "api/cassandra/createordelete";
|
||||
public static readonly connectionStringCreateOrDeleteApi: string = "api/connectionstring/cassandra/createordelete";
|
||||
public static readonly queryApi: string = "api/cassandra";
|
||||
public static readonly queryApi: string = "api/cassandra/postquery";
|
||||
public static readonly connectionStringQueryApi: string = "api/connectionstring/cassandra";
|
||||
public static readonly keysApi: string = "api/cassandra/keys";
|
||||
public static readonly connectionStringKeysApi: string = "api/connectionstring/cassandra/keys";
|
||||
@@ -179,9 +150,6 @@ export class CassandraProxyAPIs {
|
||||
export class Queries {
|
||||
public static CustomPageOption: string = "custom";
|
||||
public static UnlimitedPageOption: string = "unlimited";
|
||||
public static setAutomaticRBACOption: string = "Automatic";
|
||||
public static setTrueRBACOption: string = "True";
|
||||
public static setFalseRBACOption: string = "False";
|
||||
public static itemsPerPage: number = 100;
|
||||
public static unlimitedItemsPerPage: number = 100; // TODO: Figure out appropriate value so it works for accounts with a large number of partitions
|
||||
public static containersPerPage: number = 50;
|
||||
@@ -478,6 +446,22 @@ export class JunoEndpoints {
|
||||
public static readonly Stage = "https://tools-staging.cosmos.azure.com";
|
||||
}
|
||||
|
||||
export class MongoProxyEndpoints {
|
||||
public static readonly Development: string = "https://localhost:7238";
|
||||
public static readonly Mpac: string = "https://cdb-ms-mpac-mp.cosmos.azure.com";
|
||||
public static readonly Prod: string = "https://cdb-ms-prod-mp.cosmos.azure.com";
|
||||
public static readonly Fairfax: string = "https://cdb-ff-prod-mp.cosmos.azure.us";
|
||||
public static readonly Mooncake: string = "https://cdb-mc-prod-mp.cosmos.azure.cn";
|
||||
}
|
||||
|
||||
export class CassandraProxyEndpoints {
|
||||
public static readonly Development: string = "https://localhost:7240";
|
||||
public static readonly Mpac: string = "https://cdb-ms-mpac-cp.cosmos.azure.com";
|
||||
public static readonly Prod: string = "https://cdb-ms-prod-cp.cosmos.azure.com";
|
||||
public static readonly Fairfax: string = "https://cdb-ff-prod-cp.cosmos.azure.us";
|
||||
public static readonly Mooncake: string = "https://cdb-mc-prod-cp.cosmos.azure.cn";
|
||||
}
|
||||
|
||||
export class PriorityLevel {
|
||||
public static readonly High = "high";
|
||||
public static readonly Low = "low";
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as Cosmos from "@azure/cosmos";
|
||||
import { sendCachedDataMessage } from "Common/MessageHandler";
|
||||
import { getAuthorizationTokenUsingResourceTokens } from "Common/getAuthorizationTokenUsingResourceTokens";
|
||||
import { AuthorizationToken } from "Contracts/MessageTypes";
|
||||
import { AuthorizationToken, MessageTypes } from "Contracts/MessageTypes";
|
||||
import { checkDatabaseResourceTokensValidity } from "Platform/Fabric/FabricUtil";
|
||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||
import { AuthType } from "../AuthType";
|
||||
@@ -50,23 +51,15 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
|
||||
case Cosmos.ResourceType.offer:
|
||||
case Cosmos.ResourceType.user:
|
||||
case Cosmos.ResourceType.permission:
|
||||
// For now, these operations aren't used, so fetching the authorization token is commented out.
|
||||
// This provider must return a real token to pass validation by the client, so we return the cached resource token
|
||||
// (which is a valid token, but won't work for these operations).
|
||||
const resourceTokens2 = userContext.fabricContext.databaseConnectionInfo.resourceTokens;
|
||||
return getAuthorizationTokenUsingResourceTokens(resourceTokens2, requestInfo.path, requestInfo.resourceId);
|
||||
|
||||
/* ************** TODO: Uncomment this code if we need to support these operations **************
|
||||
// User master tokens
|
||||
const authorizationToken = await sendCachedDataMessage<AuthorizationToken>(
|
||||
MessageTypes.GetAuthorizationToken,
|
||||
[requestInfo],
|
||||
userContext.fabricContext.connectionId,
|
||||
);
|
||||
console.log("Response from Fabric: ", authorizationToken);
|
||||
headers[HttpHeaders.msDate] = authorizationToken.XDate;
|
||||
return decodeURIComponent(authorizationToken.PrimaryReadWriteToken);
|
||||
***********************************************************************************************/
|
||||
// User master tokens
|
||||
const authorizationToken = await sendCachedDataMessage<AuthorizationToken>(
|
||||
MessageTypes.GetAuthorizationToken,
|
||||
[requestInfo],
|
||||
userContext.fabricContext.connectionId,
|
||||
);
|
||||
console.log("Response from Fabric: ", authorizationToken);
|
||||
headers[HttpHeaders.msDate] = authorizationToken.XDate;
|
||||
return decodeURIComponent(authorizationToken.PrimaryReadWriteToken);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ export function queryDocuments(
|
||||
query: string,
|
||||
continuationToken?: string,
|
||||
): Promise<QueryResponse> {
|
||||
if (!useMongoProxyEndpoint("resourcelist") || !useMongoProxyEndpoint("queryDocuments")) {
|
||||
if (!useMongoProxyEndpoint("resourcelist")) {
|
||||
return queryDocuments_ToBeDeprecated(databaseId, collection, isResourceList, query, continuationToken);
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ export function queryDocuments(
|
||||
headers[CosmosSDKConstants.HttpHeaders.Continuation] = continuationToken;
|
||||
}
|
||||
|
||||
const path = isResourceList ? "/resourcelist" : "/queryDocuments";
|
||||
const path = isResourceList ? "/resourcelist" : "";
|
||||
|
||||
return window
|
||||
.fetch(`${endpoint}${path}`, {
|
||||
@@ -690,22 +690,14 @@ export function getARMCreateCollectionEndpoint(params: DataModels.MongoParameter
|
||||
}
|
||||
|
||||
function useMongoProxyEndpoint(api: string): boolean {
|
||||
const activeMongoProxyEndpoints: string[] = [
|
||||
MongoProxyEndpoints.Development,
|
||||
MongoProxyEndpoints.Mpac,
|
||||
MongoProxyEndpoints.Prod,
|
||||
];
|
||||
let canAccessMongoProxy: boolean = userContext.databaseAccount.properties.publicNetworkAccess === "Enabled";
|
||||
if (
|
||||
configContext.MONGO_PROXY_ENDPOINT !== MongoProxyEndpoints.Development &&
|
||||
userContext.databaseAccount.properties.ipRules?.length > 0
|
||||
) {
|
||||
if (userContext.databaseAccount.properties.ipRules?.length > 0) {
|
||||
canAccessMongoProxy = canAccessMongoProxy && configContext.MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED;
|
||||
}
|
||||
|
||||
return (
|
||||
canAccessMongoProxy &&
|
||||
configContext.NEW_MONGO_APIS?.includes(api) &&
|
||||
activeMongoProxyEndpoints.includes(configContext.MONGO_PROXY_ENDPOINT)
|
||||
[MongoProxyEndpoints.Development, MongoProxyEndpoints.Mpac].includes(configContext.MONGO_PROXY_ENDPOINT)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -122,21 +122,14 @@ const pollDataTransferJobOperation = async (
|
||||
|
||||
updateDataTransferJob(body);
|
||||
|
||||
if (status === "Cancelled") {
|
||||
removeFromPolling(jobName);
|
||||
clearMessage && clearMessage();
|
||||
const cancelMessage = `Data transfer job ${jobName} cancelled`;
|
||||
NotificationConsoleUtils.logConsoleError(cancelMessage);
|
||||
throw new AbortError(cancelMessage);
|
||||
}
|
||||
if (status === "Failed" || status === "Faulted") {
|
||||
if (status === "Cancelled" || status === "Failed" || status === "Faulted") {
|
||||
removeFromPolling(jobName);
|
||||
const errorMessage = body?.properties?.error
|
||||
? JSON.stringify(body?.properties?.error)
|
||||
: "Operation could not be completed";
|
||||
const error = new Error(errorMessage);
|
||||
clearMessage && clearMessage();
|
||||
NotificationConsoleUtils.logConsoleError(`Data transfer job ${jobName} failed: ${errorMessage}`);
|
||||
NotificationConsoleUtils.logConsoleError(`Data transfer job ${jobName} Failed`);
|
||||
throw new AbortError(error);
|
||||
}
|
||||
if (status === "Completed") {
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
import {
|
||||
BackendApi,
|
||||
CassandraProxyEndpoints,
|
||||
JunoEndpoints,
|
||||
MongoProxyEndpoints,
|
||||
PortalBackendEndpoints,
|
||||
} from "Common/Constants";
|
||||
import { CassandraProxyEndpoints, JunoEndpoints, MongoProxyEndpoints } from "Common/Constants";
|
||||
import {
|
||||
allowedAadEndpoints,
|
||||
allowedArcadiaEndpoints,
|
||||
@@ -45,8 +39,6 @@ export interface ConfigContext {
|
||||
ARCADIA_ENDPOINT: string;
|
||||
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: string;
|
||||
BACKEND_ENDPOINT?: string;
|
||||
PORTAL_BACKEND_ENDPOINT?: string;
|
||||
NEW_BACKEND_APIS?: BackendApi[];
|
||||
MONGO_BACKEND_ENDPOINT?: string;
|
||||
MONGO_PROXY_ENDPOINT?: string;
|
||||
MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED?: boolean;
|
||||
@@ -98,20 +90,23 @@ let configContext: Readonly<ConfigContext> = {
|
||||
GITHUB_TEST_ENV_CLIENT_ID: "b63fc8cbf87fd3c6e2eb", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1777772
|
||||
JUNO_ENDPOINT: JunoEndpoints.Prod,
|
||||
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
||||
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Prod,
|
||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||
NEW_MONGO_APIS: [
|
||||
"resourcelist",
|
||||
"queryDocuments",
|
||||
"createDocument",
|
||||
"readDocument",
|
||||
"updateDocument",
|
||||
"deleteDocument",
|
||||
"createCollectionWithProxy",
|
||||
// "resourcelist",
|
||||
// "createDocument",
|
||||
// "readDocument",
|
||||
// "updateDocument",
|
||||
// "deleteDocument",
|
||||
// "createCollectionWithProxy",
|
||||
],
|
||||
MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED: false,
|
||||
CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Prod,
|
||||
NEW_CASSANDRA_APIS: ["postQuery", "createOrDelete", "getKeys", "getSchema"],
|
||||
NEW_CASSANDRA_APIS: [
|
||||
// "postQuery",
|
||||
// "createOrDelete",
|
||||
// "getKeys",
|
||||
// "getSchema",
|
||||
],
|
||||
CASSANDRA_PROXY_OUTBOUND_IPS_ALLOWLISTED: false,
|
||||
isTerminalEnabled: false,
|
||||
isPhoenixEnabled: false,
|
||||
|
||||
@@ -53,7 +53,6 @@ export type FabricMessageV2 =
|
||||
id: string;
|
||||
message: {
|
||||
connectionId: string;
|
||||
isVisible: boolean;
|
||||
};
|
||||
}
|
||||
| {
|
||||
@@ -73,7 +72,7 @@ export type FabricMessageV2 =
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: "explorerVisible";
|
||||
type: "setToolbarStatus";
|
||||
message: {
|
||||
visible: boolean;
|
||||
};
|
||||
|
||||
@@ -47,7 +47,6 @@ export enum MessageTypes {
|
||||
GetAllResourceTokens, // Data Explorer -> Fabric
|
||||
Ready, // Data Explorer -> Fabric
|
||||
OpenCESCVAFeedbackBlade,
|
||||
ActivateTab,
|
||||
}
|
||||
|
||||
export interface AuthorizationToken {
|
||||
|
||||
@@ -387,7 +387,6 @@ export interface DataExplorerInputsFrame {
|
||||
dnsSuffix?: string;
|
||||
serverId?: string;
|
||||
extensionEndpoint?: string;
|
||||
portalBackendEndpoint?: string;
|
||||
mongoProxyEndpoint?: string;
|
||||
cassandraProxyEndpoint?: string;
|
||||
subscriptionType?: SubscriptionType;
|
||||
|
||||
@@ -46,21 +46,9 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
|
||||
}, 100);
|
||||
}
|
||||
|
||||
public componentDidUpdate() {
|
||||
if (!this.editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
const existingContent = this.editor.getModel().getValue();
|
||||
|
||||
if (this.props.content !== existingContent) {
|
||||
this.editor.pushUndoStop();
|
||||
this.editor.executeEdits("", [
|
||||
{
|
||||
range: this.editor.getModel().getFullModelRange(),
|
||||
text: this.props.content,
|
||||
},
|
||||
]);
|
||||
public componentDidUpdate(previous: EditorReactProps) {
|
||||
if (this.props.content !== previous.content) {
|
||||
this.editor?.setValue(this.props.content);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,14 +71,9 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
|
||||
|
||||
protected configureEditor(editor: monaco.editor.IStandaloneCodeEditor) {
|
||||
this.editor = editor;
|
||||
const queryEditorModel = this.editor.getModel();
|
||||
if (!this.props.isReadOnly && this.props.onContentChanged) {
|
||||
// Hooking the model's onDidChangeContent event because of some event ordering issues.
|
||||
// If a single user input causes BOTH the editor content to change AND the cursor selection to change (which is likely),
|
||||
// then there are some inconsistencies as to which event fires first.
|
||||
// But the editor.onDidChangeModelContent event seems to always fire before the cursor selection event.
|
||||
// (This is NOT true for the model's onDidChangeContent event, which sometimes fires after the cursor selection event.)
|
||||
// If the cursor selection event fires first, then the calling component may re-render the component with old content, so we want to ensure the model content changed event always fires first.
|
||||
this.editor.onDidChangeModelContent(() => {
|
||||
queryEditorModel.onDidChangeContent(() => {
|
||||
const queryEditorModel = this.editor.getModel();
|
||||
this.props.onContentChanged(queryEditorModel.getValue());
|
||||
});
|
||||
|
||||
@@ -136,15 +136,15 @@ export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({ da
|
||||
};
|
||||
|
||||
const getPercentageComplete = () => {
|
||||
const jobStatus = portalDataTransferJob?.properties?.status;
|
||||
const isCompleted = jobStatus === "Completed";
|
||||
if (isCompleted) {
|
||||
return 1;
|
||||
}
|
||||
const processedCount = portalDataTransferJob?.properties?.processedCount;
|
||||
const totalCount = portalDataTransferJob?.properties?.totalCount;
|
||||
const isJobInProgress = isCurrentJobInProgress(portalDataTransferJob);
|
||||
return isJobInProgress ? (totalCount > 0 ? processedCount / totalCount : null) : 0;
|
||||
const jobStatus = portalDataTransferJob?.properties?.status;
|
||||
const isCancelled = jobStatus === "Cancelled";
|
||||
const isCompleted = jobStatus === "Completed";
|
||||
if (totalCount <= 0 && !isCompleted) {
|
||||
return isCancelled ? 0 : null;
|
||||
}
|
||||
return isCompleted ? 1 : processedCount / totalCount;
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -149,7 +149,7 @@ export const getTabTitle = (tab: SettingsV2TabTypes): string => {
|
||||
case SettingsV2TabTypes.IndexingPolicyTab:
|
||||
return "Indexing Policy";
|
||||
case SettingsV2TabTypes.PartitionKeyTab:
|
||||
return "Partition Keys (preview)";
|
||||
return "Partition Keys";
|
||||
case SettingsV2TabTypes.ComputedPropertiesTab:
|
||||
return "Computed Properties (preview)";
|
||||
default:
|
||||
|
||||
@@ -207,7 +207,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
/>
|
||||
</PivotItem>
|
||||
<PivotItem
|
||||
headerText="Partition Keys (preview)"
|
||||
headerText="Partition Keys"
|
||||
itemKey="PartitionKeyTab"
|
||||
key="PartitionKeyTab"
|
||||
style={
|
||||
|
||||
@@ -2,8 +2,10 @@ import * as ko from "knockout";
|
||||
import { AuthType } from "../../../AuthType";
|
||||
import { DatabaseAccount } from "../../../Contracts/DataModels";
|
||||
import { CollectionBase } from "../../../Contracts/ViewModels";
|
||||
import { GitHubOAuthService } from "../../../GitHub/GitHubOAuthService";
|
||||
import { updateUserContext } from "../../../UserContext";
|
||||
import Explorer from "../../Explorer";
|
||||
import NotebookManager from "../../Notebook/NotebookManager";
|
||||
import { useNotebook } from "../../Notebook/useNotebook";
|
||||
import { useDatabases } from "../../useDatabases";
|
||||
import { useSelectedNode } from "../../useSelectedNode";
|
||||
@@ -70,6 +72,181 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Enable notebook button", () => {
|
||||
const enableNotebookBtnLabel = "Enable Notebooks (Preview)";
|
||||
const selectedNodeState = useSelectedNode.getState();
|
||||
|
||||
beforeAll(() => {
|
||||
mockExplorer = {} as Explorer;
|
||||
updateUserContext({
|
||||
portalEnv: "prod",
|
||||
databaseAccount: {
|
||||
properties: {
|
||||
capabilities: [{ name: "EnableTable" }],
|
||||
},
|
||||
} as DatabaseAccount,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
updateUserContext({
|
||||
portalEnv: "prod",
|
||||
});
|
||||
useNotebook.getState().setIsNotebookEnabled(false);
|
||||
useNotebook.getState().setIsNotebooksEnabledForAccount(false);
|
||||
});
|
||||
|
||||
it("Notebooks is already enabled - button should be hidden", () => {
|
||||
useNotebook.getState().setIsNotebookEnabled(true);
|
||||
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel);
|
||||
expect(enableNotebookBtn).toBeUndefined();
|
||||
});
|
||||
|
||||
it("Account is running on one of the national clouds - button should be hidden", () => {
|
||||
updateUserContext({
|
||||
portalEnv: "mooncake",
|
||||
});
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel);
|
||||
expect(enableNotebookBtn).toBeUndefined();
|
||||
});
|
||||
|
||||
it("Notebooks is not enabled but is available - button should be shown and enabled", () => {
|
||||
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel);
|
||||
|
||||
//TODO: modify once notebooks are available
|
||||
expect(enableNotebookBtn).toBeUndefined();
|
||||
//expect(enableNotebookBtn).toBeDefined();
|
||||
//expect(enableNotebookBtn.disabled).toBe(false);
|
||||
//expect(enableNotebookBtn.tooltipText).toBe("");
|
||||
});
|
||||
|
||||
it("Notebooks is not enabled and is unavailable - button should be shown and disabled", () => {
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel);
|
||||
|
||||
//TODO: modify once notebooks are available
|
||||
expect(enableNotebookBtn).toBeUndefined();
|
||||
//expect(enableNotebookBtn).toBeDefined();
|
||||
//expect(enableNotebookBtn.disabled).toBe(true);
|
||||
//expect(enableNotebookBtn.tooltipText).toBe(
|
||||
// "Notebooks are not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks."
|
||||
//);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Open Mongo shell button", () => {
|
||||
const openMongoShellBtnLabel = "Open Mongo shell";
|
||||
const selectedNodeState = useSelectedNode.getState();
|
||||
|
||||
beforeAll(() => {
|
||||
mockExplorer = {} as Explorer;
|
||||
updateUserContext({
|
||||
databaseAccount: {
|
||||
properties: {
|
||||
capabilities: [{ name: "EnableTable" }],
|
||||
},
|
||||
} as DatabaseAccount,
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
updateUserContext({
|
||||
apiType: "SQL",
|
||||
});
|
||||
useNotebook.getState().setIsShellEnabled(false);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
updateUserContext({
|
||||
apiType: "Mongo",
|
||||
});
|
||||
useNotebook.getState().setIsShellEnabled(true);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
useNotebook.getState().setIsNotebookEnabled(false);
|
||||
useNotebook.getState().setIsNotebooksEnabledForAccount(false);
|
||||
});
|
||||
|
||||
it("Mongo Api not available - button should be hidden", () => {
|
||||
updateUserContext({
|
||||
apiType: "SQL",
|
||||
});
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
|
||||
expect(openMongoShellBtn).toBeUndefined();
|
||||
});
|
||||
|
||||
it("Running on a national cloud - button should be hidden", () => {
|
||||
updateUserContext({
|
||||
portalEnv: "mooncake",
|
||||
});
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
|
||||
expect(openMongoShellBtn).toBeUndefined();
|
||||
});
|
||||
|
||||
it("Notebooks is not enabled and is unavailable - button should be hidden", () => {
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
|
||||
expect(openMongoShellBtn).toBeUndefined();
|
||||
});
|
||||
|
||||
it("Notebooks is not enabled and is available - button should be hidden", () => {
|
||||
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
|
||||
expect(openMongoShellBtn).toBeUndefined();
|
||||
});
|
||||
|
||||
it("Notebooks is enabled and is unavailable - button should be shown and enabled", () => {
|
||||
useNotebook.getState().setIsNotebookEnabled(true);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
|
||||
expect(openMongoShellBtn).toBeDefined();
|
||||
|
||||
//TODO: modify once notebooks are available
|
||||
expect(openMongoShellBtn.disabled).toBe(true);
|
||||
//expect(openMongoShellBtn.disabled).toBe(false);
|
||||
//expect(openMongoShellBtn.tooltipText).toBe("");
|
||||
});
|
||||
|
||||
it("Notebooks is enabled and is available - button should be shown and enabled", () => {
|
||||
useNotebook.getState().setIsNotebookEnabled(true);
|
||||
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
|
||||
expect(openMongoShellBtn).toBeDefined();
|
||||
|
||||
//TODO: modify once notebooks are available
|
||||
expect(openMongoShellBtn.disabled).toBe(true);
|
||||
//expect(openMongoShellBtn.disabled).toBe(false);
|
||||
//expect(openMongoShellBtn.tooltipText).toBe("");
|
||||
});
|
||||
|
||||
it("Notebooks is enabled and is available, terminal is unavailable due to ipRules - button should be hidden", () => {
|
||||
useNotebook.getState().setIsNotebookEnabled(true);
|
||||
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
|
||||
useNotebook.getState().setIsShellEnabled(false);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
|
||||
expect(openMongoShellBtn).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Open Cassandra shell button", () => {
|
||||
const openCassandraShellBtnLabel = "Open Cassandra shell";
|
||||
const selectedNodeState = useSelectedNode.getState();
|
||||
@@ -128,6 +305,42 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
|
||||
expect(openCassandraShellBtn).toBeUndefined();
|
||||
});
|
||||
|
||||
it("Notebooks is not enabled and is available - button should be shown and enabled", () => {
|
||||
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
|
||||
expect(openCassandraShellBtn).toBeUndefined();
|
||||
});
|
||||
|
||||
it("Notebooks is enabled and is unavailable - button should be shown and enabled", () => {
|
||||
useNotebook.getState().setIsNotebookEnabled(true);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
|
||||
|
||||
expect(openCassandraShellBtn).toBeDefined();
|
||||
|
||||
//TODO: modify once notebooks are available
|
||||
expect(openCassandraShellBtn.disabled).toBe(true);
|
||||
//expect(openCassandraShellBtn.disabled).toBe(false);
|
||||
//expect(openCassandraShellBtn.tooltipText).toBe("");
|
||||
});
|
||||
|
||||
it("Notebooks is enabled and is available - button should be shown and enabled", () => {
|
||||
useNotebook.getState().setIsNotebookEnabled(true);
|
||||
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
|
||||
expect(openCassandraShellBtn).toBeDefined();
|
||||
|
||||
//TODO: modify once notebooks are available
|
||||
expect(openCassandraShellBtn.disabled).toBe(true);
|
||||
//expect(openCassandraShellBtn.disabled).toBe(false);
|
||||
//expect(openCassandraShellBtn.tooltipText).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Open Postgres and vCore Mongo buttons", () => {
|
||||
@@ -155,6 +368,62 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("GitHub buttons", () => {
|
||||
const connectToGitHubBtnLabel = "Connect to GitHub";
|
||||
const manageGitHubSettingsBtnLabel = "Manage GitHub settings";
|
||||
const selectedNodeState = useSelectedNode.getState();
|
||||
|
||||
beforeAll(() => {
|
||||
mockExplorer = {} as Explorer;
|
||||
updateUserContext({
|
||||
databaseAccount: {
|
||||
properties: {
|
||||
capabilities: [{ name: "EnableTable" }],
|
||||
},
|
||||
} as DatabaseAccount,
|
||||
});
|
||||
|
||||
mockExplorer.notebookManager = new NotebookManager();
|
||||
mockExplorer.notebookManager.gitHubOAuthService = new GitHubOAuthService(undefined);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
useNotebook.getState().setIsNotebookEnabled(false);
|
||||
});
|
||||
|
||||
it("Notebooks is enabled and GitHubOAuthService is not logged in - connect to github button should be visible", () => {
|
||||
useNotebook.getState().setIsNotebookEnabled(true);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const connectToGitHubBtn = buttons.find((button) => button.commandButtonLabel === connectToGitHubBtnLabel);
|
||||
expect(connectToGitHubBtn).toBeDefined();
|
||||
});
|
||||
|
||||
it("Notebooks is enabled and GitHubOAuthService is logged in - manage github settings button should be visible", () => {
|
||||
useNotebook.getState().setIsNotebookEnabled(true);
|
||||
mockExplorer.notebookManager.gitHubOAuthService.isLoggedIn = jest.fn().mockReturnValue(true);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const manageGitHubSettingsBtn = buttons.find(
|
||||
(button) => button.commandButtonLabel === manageGitHubSettingsBtnLabel,
|
||||
);
|
||||
expect(manageGitHubSettingsBtn).toBeDefined();
|
||||
});
|
||||
|
||||
it("Notebooks is not enabled - connect to github and manage github settings buttons should be hidden", () => {
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
|
||||
const connectToGitHubBtn = buttons.find((button) => button.commandButtonLabel === connectToGitHubBtnLabel);
|
||||
expect(connectToGitHubBtn).toBeUndefined();
|
||||
|
||||
const manageGitHubSettingsBtn = buttons.find(
|
||||
(button) => button.commandButtonLabel === manageGitHubSettingsBtnLabel,
|
||||
);
|
||||
expect(manageGitHubSettingsBtn).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Resource token", () => {
|
||||
const mockCollection = { id: ko.observable("test") } as CollectionBase;
|
||||
useSelectedNode.getState().setSelectedNode(mockCollection);
|
||||
|
||||
@@ -7,10 +7,14 @@ import AddStoredProcedureIcon from "../../../../images/AddStoredProcedure.svg";
|
||||
import AddTriggerIcon from "../../../../images/AddTrigger.svg";
|
||||
import AddUdfIcon from "../../../../images/AddUdf.svg";
|
||||
import BrowseQueriesIcon from "../../../../images/BrowseQuery.svg";
|
||||
import CosmosTerminalIcon from "../../../../images/Cosmos-Terminal.svg";
|
||||
import FeedbackIcon from "../../../../images/Feedback-Command.svg";
|
||||
import HomeIcon from "../../../../images/Home_16.svg";
|
||||
import HostedTerminalIcon from "../../../../images/Hosted-Terminal.svg";
|
||||
import OpenQueryFromDiskIcon from "../../../../images/OpenQueryFromDisk.svg";
|
||||
import GitHubIcon from "../../../../images/github.svg";
|
||||
import NewNotebookIcon from "../../../../images/notebook/Notebook-new.svg";
|
||||
import ResetWorkspaceIcon from "../../../../images/notebook/Notebook-reset-workspace.svg";
|
||||
import OpenInTabIcon from "../../../../images/open-in-tab.svg";
|
||||
import SettingsIcon from "../../../../images/settings_15x15.svg";
|
||||
import SynapseIcon from "../../../../images/synapse-link.svg";
|
||||
@@ -18,6 +22,7 @@ import { AuthType } from "../../../AuthType";
|
||||
import * as Constants from "../../../Common/Constants";
|
||||
import { Platform, configContext } from "../../../ConfigContext";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { JunoClient } from "../../../Juno/JunoClient";
|
||||
import { userContext } from "../../../UserContext";
|
||||
import { getCollectionName, getDatabaseName } from "../../../Utils/APITypeUtils";
|
||||
import { isRunningOnNationalCloud } from "../../../Utils/CloudUtils";
|
||||
@@ -28,6 +33,7 @@ import { useNotebook } from "../../Notebook/useNotebook";
|
||||
import { OpenFullScreen } from "../../OpenFullScreen";
|
||||
import { AddDatabasePanel } from "../../Panes/AddDatabasePanel/AddDatabasePanel";
|
||||
import { BrowseQueriesPane } from "../../Panes/BrowseQueriesPane/BrowseQueriesPane";
|
||||
import { GitHubReposPanel } from "../../Panes/GitHubReposPanel/GitHubReposPanel";
|
||||
import { LoadQueryPane } from "../../Panes/LoadQueryPane/LoadQueryPane";
|
||||
import { SettingsPane } from "../../Panes/SettingsPane/SettingsPane";
|
||||
import { useDatabases } from "../../useDatabases";
|
||||
@@ -52,10 +58,10 @@ export function createStaticCommandBarButtons(
|
||||
}
|
||||
};
|
||||
|
||||
if (configContext.platform !== Platform.Fabric) {
|
||||
const homeBtn = createHomeButton();
|
||||
buttons.push(homeBtn);
|
||||
const homeBtn = createHomeButton();
|
||||
buttons.push(homeBtn);
|
||||
|
||||
if (configContext.platform !== Platform.Fabric) {
|
||||
const newCollectionBtn = createNewCollectionGroup(container);
|
||||
buttons.push(newCollectionBtn);
|
||||
if (userContext.apiType !== "Tables" && userContext.apiType !== "Cassandra") {
|
||||
@@ -74,6 +80,57 @@ export function createStaticCommandBarButtons(
|
||||
}
|
||||
}
|
||||
|
||||
if (useNotebook.getState().isNotebookEnabled) {
|
||||
addDivider();
|
||||
const notebookButtons: CommandButtonComponentProps[] = [];
|
||||
|
||||
const newNotebookButton = createNewNotebookButton(container);
|
||||
newNotebookButton.children = [createNewNotebookButton(container), createuploadNotebookButton(container)];
|
||||
notebookButtons.push(newNotebookButton);
|
||||
|
||||
if (container.notebookManager?.gitHubOAuthService) {
|
||||
notebookButtons.push(createManageGitHubAccountButton(container));
|
||||
}
|
||||
if (useNotebook.getState().isPhoenixFeatures && configContext.isTerminalEnabled) {
|
||||
notebookButtons.push(createOpenTerminalButton(container));
|
||||
}
|
||||
if (useNotebook.getState().isPhoenixNotebooks && selectedNodeState.isConnectedToContainer()) {
|
||||
notebookButtons.push(createNotebookWorkspaceResetButton(container));
|
||||
}
|
||||
if (
|
||||
(userContext.apiType === "Mongo" &&
|
||||
useNotebook.getState().isShellEnabled &&
|
||||
selectedNodeState.isDatabaseNodeOrNoneSelected()) ||
|
||||
userContext.apiType === "Cassandra"
|
||||
) {
|
||||
notebookButtons.push(createDivider());
|
||||
if (userContext.apiType === "Cassandra") {
|
||||
notebookButtons.push(createOpenTerminalButtonByKind(container, ViewModels.TerminalKind.Cassandra));
|
||||
} else {
|
||||
notebookButtons.push(createOpenTerminalButtonByKind(container, ViewModels.TerminalKind.Mongo));
|
||||
}
|
||||
}
|
||||
|
||||
notebookButtons.forEach((btn) => {
|
||||
if (btn.commandButtonLabel.indexOf("Cassandra") !== -1) {
|
||||
if (!useNotebook.getState().isPhoenixFeatures) {
|
||||
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.cassandraShellTemporarilyDownMsg);
|
||||
}
|
||||
} else if (btn.commandButtonLabel.indexOf("Mongo") !== -1) {
|
||||
if (!useNotebook.getState().isPhoenixFeatures) {
|
||||
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.mongoShellTemporarilyDownMsg);
|
||||
}
|
||||
} else if (btn.commandButtonLabel.indexOf("Open Terminal") !== -1) {
|
||||
if (!useNotebook.getState().isPhoenixFeatures) {
|
||||
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.temporarilyDownMsg);
|
||||
}
|
||||
} else if (!useNotebook.getState().isPhoenixNotebooks) {
|
||||
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.temporarilyDownMsg);
|
||||
}
|
||||
buttons.push(btn);
|
||||
});
|
||||
}
|
||||
|
||||
if (!selectedNodeState.isDatabaseNodeOrNoneSelected()) {
|
||||
const isQuerySupported = userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
|
||||
|
||||
@@ -183,7 +240,7 @@ export function createControlCommandBarButtons(container: Explorer): CommandButt
|
||||
buttons.push(fullScreenButton);
|
||||
}
|
||||
|
||||
if (configContext.platform === Platform.Portal) {
|
||||
if (configContext.platform !== Platform.Emulator) {
|
||||
const label = "Feedback";
|
||||
const feedbackButtonOptions: CommandButtonComponentProps = {
|
||||
iconSrc: FeedbackIcon,
|
||||
@@ -392,6 +449,40 @@ export function createScriptCommandButtons(selectedNodeState: SelectedNodeState)
|
||||
return buttons;
|
||||
}
|
||||
|
||||
function applyNotebooksTemporarilyDownStyle(buttonProps: CommandButtonComponentProps, tooltip: string): void {
|
||||
if (!buttonProps.isDivider) {
|
||||
buttonProps.disabled = true;
|
||||
buttonProps.tooltipText = tooltip;
|
||||
}
|
||||
}
|
||||
|
||||
function createNewNotebookButton(container: Explorer): CommandButtonComponentProps {
|
||||
const label = "New Notebook";
|
||||
return {
|
||||
id: "newNotebookBtn",
|
||||
iconSrc: NewNotebookIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => container.onNewNotebookClicked(),
|
||||
commandButtonLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(),
|
||||
ariaLabel: label,
|
||||
};
|
||||
}
|
||||
|
||||
function createuploadNotebookButton(container: Explorer): CommandButtonComponentProps {
|
||||
const label = "Upload to Notebook Server";
|
||||
return {
|
||||
iconSrc: NewNotebookIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => container.openUploadFilePanel(),
|
||||
commandButtonLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(),
|
||||
ariaLabel: label,
|
||||
};
|
||||
}
|
||||
|
||||
function createOpenQueryButton(container: Explorer): CommandButtonComponentProps {
|
||||
const label = "Open Query";
|
||||
return {
|
||||
@@ -419,6 +510,19 @@ function createOpenQueryFromDiskButton(): CommandButtonComponentProps {
|
||||
};
|
||||
}
|
||||
|
||||
function createOpenTerminalButton(container: Explorer): CommandButtonComponentProps {
|
||||
const label = "Open Terminal";
|
||||
return {
|
||||
iconSrc: CosmosTerminalIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => container.openNotebookTerminal(ViewModels.TerminalKind.Default),
|
||||
commandButtonLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(),
|
||||
ariaLabel: label,
|
||||
};
|
||||
}
|
||||
|
||||
function createOpenTerminalButtonByKind(
|
||||
container: Explorer,
|
||||
terminalKind: ViewModels.TerminalKind,
|
||||
@@ -458,6 +562,45 @@ function createOpenTerminalButtonByKind(
|
||||
};
|
||||
}
|
||||
|
||||
function createNotebookWorkspaceResetButton(container: Explorer): CommandButtonComponentProps {
|
||||
const label = "Reset Workspace";
|
||||
return {
|
||||
iconSrc: ResetWorkspaceIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => container.resetNotebookWorkspace(),
|
||||
commandButtonLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(),
|
||||
ariaLabel: label,
|
||||
};
|
||||
}
|
||||
|
||||
function createManageGitHubAccountButton(container: Explorer): CommandButtonComponentProps {
|
||||
const connectedToGitHub: boolean = container.notebookManager?.gitHubOAuthService.isLoggedIn();
|
||||
const label = connectedToGitHub ? "Manage GitHub settings" : "Connect to GitHub";
|
||||
const junoClient = new JunoClient();
|
||||
return {
|
||||
iconSrc: GitHubIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => {
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
label,
|
||||
<GitHubReposPanel
|
||||
explorer={container}
|
||||
gitHubClientProp={container.notebookManager.gitHubClient}
|
||||
junoClientProp={junoClient}
|
||||
/>,
|
||||
);
|
||||
},
|
||||
commandButtonLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(),
|
||||
ariaLabel: label,
|
||||
};
|
||||
}
|
||||
|
||||
function createStaticCommandBarButtonsForResourceToken(
|
||||
container: Explorer,
|
||||
selectedNodeState: SelectedNodeState,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// TODO convert this file to an action registry in order to have actions and their handlers be more tightly coupled.
|
||||
import { useDatabases } from "Explorer/useDatabases";
|
||||
import React from "react";
|
||||
import { ActionContracts } from "../../Contracts/ExplorerContracts";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
@@ -41,112 +40,97 @@ function openCollectionTab(
|
||||
databases: ViewModels.Database[],
|
||||
initialDatabaseIndex = 0,
|
||||
) {
|
||||
//if databases are not yet loaded, wait until loaded
|
||||
if (!databases || databases.length === 0) {
|
||||
const databaseActionHandler = (databases: ViewModels.Database[]) => {
|
||||
databasesUnsubscription();
|
||||
openCollectionTab(action, databases, 0);
|
||||
return;
|
||||
};
|
||||
const databasesUnsubscription = useDatabases.subscribe(databaseActionHandler, (state) => state.databases);
|
||||
} else {
|
||||
for (let i = initialDatabaseIndex; i < databases.length; i++) {
|
||||
const database: ViewModels.Database = databases[i];
|
||||
if (!!action.databaseResourceId && database.id() !== action.databaseResourceId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
//expand database first if not expanded to load the collections
|
||||
if (!database.isDatabaseExpanded?.()) {
|
||||
database.expandDatabase?.();
|
||||
}
|
||||
|
||||
const collectionActionHandler = (collections: ViewModels.Collection[]) => {
|
||||
if (!action.collectionResourceId && collections.length === 0) {
|
||||
subscription.dispose();
|
||||
openCollectionTab(action, databases, ++i);
|
||||
return;
|
||||
}
|
||||
|
||||
for (let j = 0; j < collections.length; j++) {
|
||||
const collection: ViewModels.Collection = collections[j];
|
||||
if (!!action.collectionResourceId && collection.id() !== action.collectionResourceId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// select the collection
|
||||
collection.expandCollection();
|
||||
|
||||
if (
|
||||
action.tabKind === ActionContracts.TabKind.SQLDocuments ||
|
||||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SQLDocuments]
|
||||
) {
|
||||
collection.onDocumentDBDocumentsClick();
|
||||
break;
|
||||
}
|
||||
|
||||
if (
|
||||
action.tabKind === ActionContracts.TabKind.MongoDocuments ||
|
||||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.MongoDocuments]
|
||||
) {
|
||||
collection.onMongoDBDocumentsClick();
|
||||
break;
|
||||
}
|
||||
|
||||
if (
|
||||
action.tabKind === ActionContracts.TabKind.SchemaAnalyzer ||
|
||||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SchemaAnalyzer]
|
||||
) {
|
||||
collection.onSchemaAnalyzerClick();
|
||||
break;
|
||||
}
|
||||
|
||||
if (
|
||||
action.tabKind === ActionContracts.TabKind.TableEntities ||
|
||||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.TableEntities]
|
||||
) {
|
||||
collection.onTableEntitiesClick();
|
||||
break;
|
||||
}
|
||||
|
||||
if (
|
||||
action.tabKind === ActionContracts.TabKind.Graph ||
|
||||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.Graph]
|
||||
) {
|
||||
collection.onGraphDocumentsClick();
|
||||
break;
|
||||
}
|
||||
|
||||
if (
|
||||
action.tabKind === ActionContracts.TabKind.SQLQuery ||
|
||||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SQLQuery]
|
||||
) {
|
||||
collection.onNewQueryClick(
|
||||
collection,
|
||||
undefined,
|
||||
generateQueryText(action as ActionContracts.OpenQueryTab, collection.partitionKeyProperties),
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
if (
|
||||
action.tabKind === ActionContracts.TabKind.ScaleSettings ||
|
||||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.ScaleSettings]
|
||||
) {
|
||||
collection.onSettingsClick();
|
||||
break;
|
||||
}
|
||||
}
|
||||
subscription.dispose();
|
||||
};
|
||||
|
||||
const subscription = database.collections.subscribe((collections) => collectionActionHandler(collections));
|
||||
if (database.collections && database.collections() && database.collections().length) {
|
||||
collectionActionHandler(database.collections());
|
||||
}
|
||||
|
||||
break;
|
||||
for (let i = initialDatabaseIndex; i < databases.length; i++) {
|
||||
const database: ViewModels.Database = databases[i];
|
||||
if (!!action.databaseResourceId && database.id() !== action.databaseResourceId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const collectionActionHandler = (collections: ViewModels.Collection[]) => {
|
||||
if (!action.collectionResourceId && collections.length === 0) {
|
||||
subscription.dispose();
|
||||
openCollectionTab(action, databases, ++i);
|
||||
return;
|
||||
}
|
||||
|
||||
for (let j = 0; j < collections.length; j++) {
|
||||
const collection: ViewModels.Collection = collections[j];
|
||||
if (!!action.collectionResourceId && collection.id() !== action.collectionResourceId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// select the collection
|
||||
collection.expandCollection();
|
||||
|
||||
if (
|
||||
action.tabKind === ActionContracts.TabKind.SQLDocuments ||
|
||||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SQLDocuments]
|
||||
) {
|
||||
collection.onDocumentDBDocumentsClick();
|
||||
break;
|
||||
}
|
||||
|
||||
if (
|
||||
action.tabKind === ActionContracts.TabKind.MongoDocuments ||
|
||||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.MongoDocuments]
|
||||
) {
|
||||
collection.onMongoDBDocumentsClick();
|
||||
break;
|
||||
}
|
||||
|
||||
if (
|
||||
action.tabKind === ActionContracts.TabKind.SchemaAnalyzer ||
|
||||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SchemaAnalyzer]
|
||||
) {
|
||||
collection.onSchemaAnalyzerClick();
|
||||
break;
|
||||
}
|
||||
|
||||
if (
|
||||
action.tabKind === ActionContracts.TabKind.TableEntities ||
|
||||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.TableEntities]
|
||||
) {
|
||||
collection.onTableEntitiesClick();
|
||||
break;
|
||||
}
|
||||
|
||||
if (
|
||||
action.tabKind === ActionContracts.TabKind.Graph ||
|
||||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.Graph]
|
||||
) {
|
||||
collection.onGraphDocumentsClick();
|
||||
break;
|
||||
}
|
||||
|
||||
if (
|
||||
action.tabKind === ActionContracts.TabKind.SQLQuery ||
|
||||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SQLQuery]
|
||||
) {
|
||||
collection.onNewQueryClick(
|
||||
collection,
|
||||
undefined,
|
||||
generateQueryText(action as ActionContracts.OpenQueryTab, collection.partitionKeyProperties),
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
if (
|
||||
action.tabKind === ActionContracts.TabKind.ScaleSettings ||
|
||||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.ScaleSettings]
|
||||
) {
|
||||
collection.onSettingsClick();
|
||||
break;
|
||||
}
|
||||
}
|
||||
subscription.dispose();
|
||||
};
|
||||
|
||||
const subscription = database.collections.subscribe((collections) => collectionActionHandler(collections));
|
||||
if (database.collections && database.collections() && database.collections().length) {
|
||||
collectionActionHandler(database.collections());
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
Icon,
|
||||
IconButton,
|
||||
Link,
|
||||
MessageBar,
|
||||
Stack,
|
||||
Text,
|
||||
TooltipHost,
|
||||
@@ -208,7 +207,6 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
|
||||
</Stack>
|
||||
{createNewContainer ? (
|
||||
<Stack>
|
||||
<MessageBar>All configurations except for unique keys will be copied from the source container</MessageBar>
|
||||
<Stack className="panelGroupSpacing">
|
||||
<Stack horizontal>
|
||||
<span className="mandatoryStar">* </span>
|
||||
|
||||
@@ -41,13 +41,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
? Constants.Queries.UnlimitedPageOption
|
||||
: Constants.Queries.CustomPageOption,
|
||||
);
|
||||
const [enableDataPlaneRBACOption, setEnableDataPlaneRBACOption] = useState<string>(
|
||||
LocalStorageUtility.getEntryString(StorageKey.DataPlaneRbacEnabled) === Constants.Queries.setAutomaticRBACOption
|
||||
? Constants.Queries.setAutomaticRBACOption
|
||||
: LocalStorageUtility.getEntryString(StorageKey.DataPlaneRbacEnabled) === Constants.Queries.setTrueRBACOption
|
||||
? Constants.Queries.setTrueRBACOption
|
||||
: Constants.Queries.setFalseRBACOption
|
||||
);
|
||||
const [ruThresholdEnabled, setRUThresholdEnabled] = useState<boolean>(isRUThresholdEnabled());
|
||||
const [ruThreshold, setRUThreshold] = useState<number>(getRUThreshold());
|
||||
const [queryTimeoutEnabled, setQueryTimeoutEnabled] = useState<boolean>(
|
||||
@@ -117,14 +110,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
StorageKey.ActualItemPerPage,
|
||||
isCustomPageOptionSelected() ? customItemPerPage : Constants.Queries.unlimitedItemsPerPage,
|
||||
);
|
||||
|
||||
LocalStorageUtility.setEntryNumber(StorageKey.CustomItemPerPage, customItemPerPage);
|
||||
|
||||
LocalStorageUtility.setEntryString(
|
||||
StorageKey.DataPlaneRbacEnabled,
|
||||
enableDataPlaneRBACOption
|
||||
);
|
||||
|
||||
LocalStorageUtility.setEntryBoolean(StorageKey.RUThresholdEnabled, ruThresholdEnabled);
|
||||
LocalStorageUtility.setEntryBoolean(StorageKey.QueryTimeoutEnabled, queryTimeoutEnabled);
|
||||
LocalStorageUtility.setEntryNumber(StorageKey.RetryAttempts, retryAttempts);
|
||||
@@ -211,12 +197,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
{ key: Constants.PriorityLevel.High, text: "High" },
|
||||
];
|
||||
|
||||
const dataPlaneRBACOptionsList: IChoiceGroupOption[] = [
|
||||
{ key: Constants.Queries.setAutomaticRBACOption, text: "Automatic" },
|
||||
{ key: Constants.Queries.setTrueRBACOption, text: "True" },
|
||||
{ key: Constants.Queries.setFalseRBACOption, text: "False"}
|
||||
];
|
||||
|
||||
const handleOnPriorityLevelOptionChange = (
|
||||
ev: React.FormEvent<HTMLInputElement>,
|
||||
option: IChoiceGroupOption,
|
||||
@@ -228,10 +208,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
setPageOption(option.key);
|
||||
};
|
||||
|
||||
const handleOnDataPlaneRBACOptionChange = (ev: React.FormEvent<HTMLInputElement>, option: IChoiceGroupOption): void => {
|
||||
setEnableDataPlaneRBACOption(option.key);
|
||||
};
|
||||
|
||||
const handleOnRUThresholdToggleChange = (ev: React.MouseEvent<HTMLElement>, checked?: boolean): void => {
|
||||
setRUThresholdEnabled(checked);
|
||||
};
|
||||
@@ -385,27 +361,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{(
|
||||
<div className="settingsSection">
|
||||
<div className="settingsSectionPart">
|
||||
<fieldset>
|
||||
<legend id="enableDataPlaneRBACOptions" className="settingsSectionLabel legendLabel">
|
||||
Enable DataPlane RBAC
|
||||
</legend>
|
||||
<InfoTooltip>
|
||||
Choose Automatic to enable DataPlane RBAC automatically. True/False to voluntarily enable/disable DataPlane RBAC
|
||||
</InfoTooltip>
|
||||
<ChoiceGroup
|
||||
ariaLabelledBy="enableDataPlaneRBACOptions"
|
||||
selectedKey={enableDataPlaneRBACOption}
|
||||
options={dataPlaneRBACOptionsList}
|
||||
styles={choiceButtonStyles}
|
||||
onChange={handleOnDataPlaneRBACOptionChange}
|
||||
/>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{userContext.apiType === "SQL" && (
|
||||
<>
|
||||
<div className="settingsSection">
|
||||
|
||||
@@ -49,7 +49,7 @@ export const QueryCopilotFeedbackModal = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal isOpen={showFeedbackModal} styles={{ main: { borderRadius: 8, maxWidth: 600 } }}>
|
||||
<Modal isOpen={showFeedbackModal}>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Stack style={{ padding: 24 }}>
|
||||
<Stack horizontal horizontalAlign="space-between">
|
||||
@@ -68,14 +68,9 @@ export const QueryCopilotFeedbackModal = ({
|
||||
rows={3}
|
||||
/>
|
||||
<TextField
|
||||
styles={{
|
||||
root: { marginBottom: 14 },
|
||||
fieldGroup: { backgroundColor: "#F3F2F1", borderRadius: 4, borderColor: "#D1D1D1" },
|
||||
}}
|
||||
styles={{ root: { marginBottom: 14 } }}
|
||||
label="Query generated"
|
||||
defaultValue={generatedQuery}
|
||||
multiline
|
||||
rows={3}
|
||||
readOnly
|
||||
/>
|
||||
<Text style={{ fontSize: 12, marginBottom: 14 }}>
|
||||
|
||||
@@ -3,14 +3,6 @@
|
||||
exports[`Query Copilot Feedback Modal snapshot test shoud render and match snapshot 1`] = `
|
||||
<Modal
|
||||
isOpen={true}
|
||||
styles={
|
||||
Object {
|
||||
"main": Object {
|
||||
"borderRadius": 8,
|
||||
"maxWidth": 600,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<form
|
||||
onSubmit={[Function]}
|
||||
@@ -75,16 +67,9 @@ exports[`Query Copilot Feedback Modal snapshot test shoud render and match snaps
|
||||
<StyledTextFieldBase
|
||||
defaultValue="test query"
|
||||
label="Query generated"
|
||||
multiline={true}
|
||||
readOnly={true}
|
||||
rows={3}
|
||||
styles={
|
||||
Object {
|
||||
"fieldGroup": Object {
|
||||
"backgroundColor": "#F3F2F1",
|
||||
"borderColor": "#D1D1D1",
|
||||
"borderRadius": 4,
|
||||
},
|
||||
"root": Object {
|
||||
"marginBottom": 14,
|
||||
},
|
||||
@@ -140,14 +125,6 @@ exports[`Query Copilot Feedback Modal snapshot test shoud render and match snaps
|
||||
exports[`Query Copilot Feedback Modal snapshot test should cancel submission 1`] = `
|
||||
<Modal
|
||||
isOpen={false}
|
||||
styles={
|
||||
Object {
|
||||
"main": Object {
|
||||
"borderRadius": 8,
|
||||
"maxWidth": 600,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<form
|
||||
onSubmit={[Function]}
|
||||
@@ -212,16 +189,9 @@ exports[`Query Copilot Feedback Modal snapshot test should cancel submission 1`]
|
||||
<StyledTextFieldBase
|
||||
defaultValue="test query"
|
||||
label="Query generated"
|
||||
multiline={true}
|
||||
readOnly={true}
|
||||
rows={3}
|
||||
styles={
|
||||
Object {
|
||||
"fieldGroup": Object {
|
||||
"backgroundColor": "#F3F2F1",
|
||||
"borderColor": "#D1D1D1",
|
||||
"borderRadius": 4,
|
||||
},
|
||||
"root": Object {
|
||||
"marginBottom": 14,
|
||||
},
|
||||
@@ -277,14 +247,6 @@ exports[`Query Copilot Feedback Modal snapshot test should cancel submission 1`]
|
||||
exports[`Query Copilot Feedback Modal snapshot test should close on cancel click 1`] = `
|
||||
<Modal
|
||||
isOpen={false}
|
||||
styles={
|
||||
Object {
|
||||
"main": Object {
|
||||
"borderRadius": 8,
|
||||
"maxWidth": 600,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<form
|
||||
onSubmit={[Function]}
|
||||
@@ -349,16 +311,9 @@ exports[`Query Copilot Feedback Modal snapshot test should close on cancel click
|
||||
<StyledTextFieldBase
|
||||
defaultValue="test query"
|
||||
label="Query generated"
|
||||
multiline={true}
|
||||
readOnly={true}
|
||||
rows={3}
|
||||
styles={
|
||||
Object {
|
||||
"fieldGroup": Object {
|
||||
"backgroundColor": "#F3F2F1",
|
||||
"borderColor": "#D1D1D1",
|
||||
"borderRadius": 4,
|
||||
},
|
||||
"root": Object {
|
||||
"marginBottom": 14,
|
||||
},
|
||||
@@ -414,14 +369,6 @@ exports[`Query Copilot Feedback Modal snapshot test should close on cancel click
|
||||
exports[`Query Copilot Feedback Modal snapshot test should get user unput 1`] = `
|
||||
<Modal
|
||||
isOpen={false}
|
||||
styles={
|
||||
Object {
|
||||
"main": Object {
|
||||
"borderRadius": 8,
|
||||
"maxWidth": 600,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<form
|
||||
onSubmit={[Function]}
|
||||
@@ -486,16 +433,9 @@ exports[`Query Copilot Feedback Modal snapshot test should get user unput 1`] =
|
||||
<StyledTextFieldBase
|
||||
defaultValue="test query"
|
||||
label="Query generated"
|
||||
multiline={true}
|
||||
readOnly={true}
|
||||
rows={3}
|
||||
styles={
|
||||
Object {
|
||||
"fieldGroup": Object {
|
||||
"backgroundColor": "#F3F2F1",
|
||||
"borderColor": "#D1D1D1",
|
||||
"borderRadius": 4,
|
||||
},
|
||||
"root": Object {
|
||||
"marginBottom": 14,
|
||||
},
|
||||
@@ -551,14 +491,6 @@ exports[`Query Copilot Feedback Modal snapshot test should get user unput 1`] =
|
||||
exports[`Query Copilot Feedback Modal snapshot test should not render dont show again button 1`] = `
|
||||
<Modal
|
||||
isOpen={false}
|
||||
styles={
|
||||
Object {
|
||||
"main": Object {
|
||||
"borderRadius": 8,
|
||||
"maxWidth": 600,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<form
|
||||
onSubmit={[Function]}
|
||||
@@ -623,16 +555,9 @@ exports[`Query Copilot Feedback Modal snapshot test should not render dont show
|
||||
<StyledTextFieldBase
|
||||
defaultValue="test query"
|
||||
label="Query generated"
|
||||
multiline={true}
|
||||
readOnly={true}
|
||||
rows={3}
|
||||
styles={
|
||||
Object {
|
||||
"fieldGroup": Object {
|
||||
"backgroundColor": "#F3F2F1",
|
||||
"borderColor": "#D1D1D1",
|
||||
"borderRadius": 4,
|
||||
},
|
||||
"root": Object {
|
||||
"marginBottom": 14,
|
||||
},
|
||||
@@ -688,14 +613,6 @@ exports[`Query Copilot Feedback Modal snapshot test should not render dont show
|
||||
exports[`Query Copilot Feedback Modal snapshot test should render dont show again button and check it 1`] = `
|
||||
<Modal
|
||||
isOpen={true}
|
||||
styles={
|
||||
Object {
|
||||
"main": Object {
|
||||
"borderRadius": 8,
|
||||
"maxWidth": 600,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<form
|
||||
onSubmit={[Function]}
|
||||
@@ -760,16 +677,9 @@ exports[`Query Copilot Feedback Modal snapshot test should render dont show agai
|
||||
<StyledTextFieldBase
|
||||
defaultValue="test query"
|
||||
label="Query generated"
|
||||
multiline={true}
|
||||
readOnly={true}
|
||||
rows={3}
|
||||
styles={
|
||||
Object {
|
||||
"fieldGroup": Object {
|
||||
"backgroundColor": "#F3F2F1",
|
||||
"borderColor": "#D1D1D1",
|
||||
"borderRadius": 4,
|
||||
},
|
||||
"root": Object {
|
||||
"marginBottom": 14,
|
||||
},
|
||||
@@ -840,14 +750,6 @@ exports[`Query Copilot Feedback Modal snapshot test should render dont show agai
|
||||
exports[`Query Copilot Feedback Modal snapshot test should submit submission 1`] = `
|
||||
<Modal
|
||||
isOpen={false}
|
||||
styles={
|
||||
Object {
|
||||
"main": Object {
|
||||
"borderRadius": 8,
|
||||
"maxWidth": 600,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<form
|
||||
onSubmit={[Function]}
|
||||
@@ -912,16 +814,9 @@ exports[`Query Copilot Feedback Modal snapshot test should submit submission 1`]
|
||||
<StyledTextFieldBase
|
||||
defaultValue="test query"
|
||||
label="Query generated"
|
||||
multiline={true}
|
||||
readOnly={true}
|
||||
rows={3}
|
||||
styles={
|
||||
Object {
|
||||
"fieldGroup": Object {
|
||||
"backgroundColor": "#F3F2F1",
|
||||
"borderColor": "#D1D1D1",
|
||||
"borderRadius": 4,
|
||||
},
|
||||
"root": Object {
|
||||
"marginBottom": 14,
|
||||
},
|
||||
|
||||
@@ -11,8 +11,8 @@ import {
|
||||
Link,
|
||||
MessageBar,
|
||||
MessageBarType,
|
||||
ProgressIndicator,
|
||||
Separator,
|
||||
Spinner,
|
||||
Stack,
|
||||
TeachingBubble,
|
||||
Text,
|
||||
@@ -36,6 +36,7 @@ import { userContext } from "UserContext";
|
||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||
import React, { useRef, useState } from "react";
|
||||
import HintIcon from "../../../images/Hint.svg";
|
||||
import CopilotIcon from "../../../images/QueryCopilotNewLogo.svg";
|
||||
import RecentIcon from "../../../images/Recent.svg";
|
||||
import errorIcon from "../../../images/close-black.svg";
|
||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
@@ -214,12 +215,12 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
||||
const generateSQLQueryResponse: GenerateSQLQueryResponse = await response?.json();
|
||||
if (response.ok) {
|
||||
if (generateSQLQueryResponse?.sql !== "N/A") {
|
||||
const queryExplanation = `-- **Explanation of query:** ${
|
||||
generateSQLQueryResponse.explanation ? generateSQLQueryResponse.explanation : "N/A"
|
||||
}\r\n`;
|
||||
const currentGeneratedQuery = queryExplanation + generateSQLQueryResponse.sql;
|
||||
const lastQuery = generatedQuery && query ? `${query}\r\n` : "";
|
||||
setQuery(`${lastQuery}${currentGeneratedQuery}`);
|
||||
let query = `-- **Prompt:** ${userPrompt}\r\n`;
|
||||
if (generateSQLQueryResponse.explanation) {
|
||||
query += `-- **Explanation of query:** ${generateSQLQueryResponse.explanation}\r\n`;
|
||||
}
|
||||
query += generateSQLQueryResponse.sql;
|
||||
setQuery(query);
|
||||
setGeneratedQuery(generateSQLQueryResponse.sql);
|
||||
setGeneratedQueryComments(generateSQLQueryResponse.explanation);
|
||||
setShowFeedbackBar(true);
|
||||
@@ -296,9 +297,9 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
||||
if (isGeneratingQuery === null) {
|
||||
return " ";
|
||||
} else if (isGeneratingQuery) {
|
||||
return "Content is loading";
|
||||
return "Content is loading!";
|
||||
} else {
|
||||
return "Content is updated";
|
||||
return "Content is updated!";
|
||||
}
|
||||
};
|
||||
|
||||
@@ -309,388 +310,12 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
||||
return (
|
||||
<Stack
|
||||
className="copilot-prompt-pane"
|
||||
styles={{ root: { backgroundColor: "#FAFAFA", padding: "8px" } }}
|
||||
styles={{ root: { backgroundColor: "#FAFAFA", padding: "16px 24px 0px" } }}
|
||||
id="copilot-textfield-label"
|
||||
>
|
||||
<Stack
|
||||
horizontal
|
||||
styles={{
|
||||
root: {
|
||||
width: "100%",
|
||||
borderWidth: 1,
|
||||
borderStyle: "solid",
|
||||
borderColor: "#D1D1D1",
|
||||
borderRadius: 8,
|
||||
boxShadow: "0px 4px 8px 0px #00000024",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Stack style={{ width: "100%" }}>
|
||||
<Stack horizontal verticalAlign="center" style={{ padding: "8px 8px 0px 8px" }}>
|
||||
<TextField
|
||||
id="naturalLanguageInput"
|
||||
value={userPrompt}
|
||||
onChange={handleUserPromptChange}
|
||||
onClick={() => {
|
||||
inputEdited.current = true;
|
||||
setShowSamplePrompts(true);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && userPrompt) {
|
||||
inputEdited.current = true;
|
||||
startGenerateQueryProcess();
|
||||
}
|
||||
}}
|
||||
style={{ lineHeight: 30 }}
|
||||
styles={{
|
||||
root: { width: "100%" },
|
||||
suffix: {
|
||||
background: "none",
|
||||
padding: 0,
|
||||
},
|
||||
fieldGroup: {
|
||||
borderRadius: 4,
|
||||
borderColor: "#D1D1D1",
|
||||
"::after": {
|
||||
border: "inherit",
|
||||
borderWidth: 2,
|
||||
borderBottomColor: "#464FEB",
|
||||
borderRadius: 4,
|
||||
},
|
||||
},
|
||||
}}
|
||||
disabled={isGeneratingQuery}
|
||||
autoComplete="off"
|
||||
placeholder="Ask a question in natural language and we’ll generate the query for you."
|
||||
aria-labelledby="copilot-textfield-label"
|
||||
onRenderSuffix={() => {
|
||||
return (
|
||||
<IconButton
|
||||
iconProps={{ iconName: "Send" }}
|
||||
disabled={isGeneratingQuery || !userPrompt.trim()}
|
||||
style={{ background: "none" }}
|
||||
onClick={() => startGenerateQueryProcess()}
|
||||
aria-label="Send"
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{showPromptTeachingBubble && copilotTeachingBubbleVisible && (
|
||||
<TeachingBubble
|
||||
calloutProps={{ directionalHint: DirectionalHint.bottomCenter }}
|
||||
target="#naturalLanguageInput"
|
||||
hasCloseButton={true}
|
||||
closeButtonAriaLabel="Close"
|
||||
onDismiss={() => toggleCopilotTeachingBubbleVisible(false)}
|
||||
hasSmallHeadline={true}
|
||||
headline="Write a prompt"
|
||||
>
|
||||
Write a prompt here and Copilot will generate the query for you. You can also choose from our{" "}
|
||||
<Link
|
||||
onClick={() => {
|
||||
setShowSamplePrompts(true);
|
||||
toggleCopilotTeachingBubbleVisible(false);
|
||||
}}
|
||||
style={{ color: "white", fontWeight: 600 }}
|
||||
>
|
||||
sample prompts
|
||||
</Link>{" "}
|
||||
or write your own query
|
||||
</TeachingBubble>
|
||||
)}
|
||||
{showSamplePrompts && (
|
||||
<Callout
|
||||
styles={{ root: { minWidth: 400, maxWidth: "70vw" } }}
|
||||
target="#naturalLanguageInput"
|
||||
isBeakVisible={false}
|
||||
onDismiss={() => setShowSamplePrompts(false)}
|
||||
directionalHintFixed={true}
|
||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||
alignTargetEdge={true}
|
||||
gapSpace={4}
|
||||
>
|
||||
<Stack>
|
||||
{filteredHistories?.length > 0 && (
|
||||
<Stack>
|
||||
<Text
|
||||
style={{
|
||||
width: "100%",
|
||||
fontSize: 14,
|
||||
fontWeight: 600,
|
||||
color: "#0078D4",
|
||||
marginLeft: 16,
|
||||
padding: "4px 0",
|
||||
}}
|
||||
>
|
||||
Recent
|
||||
</Text>
|
||||
{filteredHistories.map((history, i) => (
|
||||
<DefaultButton
|
||||
key={i}
|
||||
onClick={() => {
|
||||
setUserPrompt(history);
|
||||
setShowSamplePrompts(false);
|
||||
inputEdited.current = true;
|
||||
}}
|
||||
onRenderIcon={() => <Image src={RecentIcon} styles={{ root: { overflow: "unset" } }} />}
|
||||
styles={promptStyles}
|
||||
>
|
||||
{history}
|
||||
</DefaultButton>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
{filteredSuggestedPrompts?.length > 0 && (
|
||||
<Stack>
|
||||
<Text
|
||||
style={{
|
||||
width: "100%",
|
||||
fontSize: 14,
|
||||
fontWeight: 600,
|
||||
color: "#0078D4",
|
||||
marginLeft: 16,
|
||||
padding: "4px 0",
|
||||
}}
|
||||
>
|
||||
Suggested Prompts
|
||||
</Text>
|
||||
{filteredSuggestedPrompts.map((prompt) => (
|
||||
<DefaultButton
|
||||
key={prompt.id}
|
||||
onClick={() => {
|
||||
setUserPrompt(prompt.text);
|
||||
setShowSamplePrompts(false);
|
||||
inputEdited.current = true;
|
||||
}}
|
||||
onRenderIcon={() => <Image src={HintIcon} />}
|
||||
styles={promptStyles}
|
||||
>
|
||||
{prompt.text}
|
||||
</DefaultButton>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
{(filteredHistories?.length > 0 || filteredSuggestedPrompts?.length > 0) && (
|
||||
<Stack>
|
||||
<Separator
|
||||
styles={{
|
||||
root: {
|
||||
selectors: { "::before": { background: "#E1DFDD" } },
|
||||
padding: 0,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
width: "100%",
|
||||
fontSize: 14,
|
||||
marginLeft: 16,
|
||||
padding: "4px 0",
|
||||
}}
|
||||
>
|
||||
Learn about{" "}
|
||||
<Link target="_blank" href="https://aka.ms/cdb-copilot-writing">
|
||||
writing effective prompts
|
||||
</Link>
|
||||
</Text>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
</Callout>
|
||||
)}
|
||||
</Stack>
|
||||
{!isGeneratingQuery && (
|
||||
<Stack style={{ padding: 8 }}>
|
||||
{!showFeedbackBar && (
|
||||
<Text style={{ fontSize: 12 }}>
|
||||
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" }}>
|
||||
Read preview terms
|
||||
</Link>
|
||||
{showErrorMessageBar && (
|
||||
<MessageBar messageBarType={MessageBarType.error}>
|
||||
{errorMessage ? errorMessage : "We ran into an error and were not able to execute query."}
|
||||
</MessageBar>
|
||||
)}
|
||||
{showInvalidQueryMessageBar && (
|
||||
<MessageBar
|
||||
messageBarType={MessageBarType.info}
|
||||
styles={{ root: { backgroundColor: "#F0F6FF" }, icon: { color: "#015CDA" } }}
|
||||
>
|
||||
We were unable to generate a query based upon the prompt provided. Please modify the prompt and
|
||||
try again. For examples of how to write a good prompt, please read
|
||||
<Link href="https://aka.ms/cdb-copilot-writing" target="_blank">
|
||||
this article.
|
||||
</Link>{" "}
|
||||
Our content guidelines can be found
|
||||
<Link href="https://aka.ms/cdb-query-copilot" target="_blank">
|
||||
here.
|
||||
</Link>
|
||||
</MessageBar>
|
||||
)}
|
||||
</Text>
|
||||
)}
|
||||
{showFeedbackBar && (
|
||||
<Stack horizontal verticalAlign="center" style={{ maxHeight: 20 }}>
|
||||
{userContext.feedbackPolicies?.policyAllowFeedback && (
|
||||
<Stack horizontal verticalAlign="center">
|
||||
<Text style={{ fontSize: 12 }}>Provide feedback</Text>
|
||||
{showCallout && !hideFeedbackModalForLikedQueries && (
|
||||
<Callout
|
||||
role="status"
|
||||
style={{ padding: "6px 12px" }}
|
||||
styles={{
|
||||
root: {
|
||||
borderRadius: 8,
|
||||
},
|
||||
beakCurtain: {
|
||||
borderRadius: 8,
|
||||
},
|
||||
calloutMain: {
|
||||
borderRadius: 8,
|
||||
},
|
||||
}}
|
||||
target="#likeBtn"
|
||||
onDismiss={() => {
|
||||
setShowCallout(false);
|
||||
SubmitFeedback({
|
||||
params: {
|
||||
generatedQuery: generatedQuery,
|
||||
likeQuery: likeQuery,
|
||||
description: "",
|
||||
userPrompt: userPrompt,
|
||||
},
|
||||
explorer,
|
||||
databaseId,
|
||||
containerId,
|
||||
mode: isSampleCopilotActive ? "Sample" : "User",
|
||||
});
|
||||
}}
|
||||
directionalHint={DirectionalHint.topCenter}
|
||||
>
|
||||
<Text>
|
||||
Thank you. Need to give{" "}
|
||||
<Link
|
||||
onClick={() => {
|
||||
setShowCallout(false);
|
||||
openFeedbackModal(generatedQuery, true, userPrompt);
|
||||
}}
|
||||
>
|
||||
more feedback?
|
||||
</Link>
|
||||
</Text>
|
||||
</Callout>
|
||||
)}
|
||||
<IconButton
|
||||
id="likeBtn"
|
||||
style={{ marginLeft: 10 }}
|
||||
aria-label="Like"
|
||||
role="toggle"
|
||||
iconProps={{ iconName: likeQuery === true ? "LikeSolid" : "Like" }}
|
||||
onClick={() => {
|
||||
setShowCallout(!likeQuery);
|
||||
setLikeQuery(!likeQuery);
|
||||
if (likeQuery === true) {
|
||||
document.getElementById("likeStatus").innerHTML = "Unpressed";
|
||||
}
|
||||
if (likeQuery === false) {
|
||||
document.getElementById("likeStatus").innerHTML = "Liked";
|
||||
}
|
||||
if (dislikeQuery) {
|
||||
setDislikeQuery(!dislikeQuery);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
style={{ margin: "0 4px" }}
|
||||
role="toggle"
|
||||
aria-label="Dislike"
|
||||
iconProps={{ iconName: dislikeQuery === true ? "DislikeSolid" : "Dislike" }}
|
||||
onClick={() => {
|
||||
let toggleStatusValue = "Unpressed";
|
||||
if (!dislikeQuery) {
|
||||
openFeedbackModal(generatedQuery, false, userPrompt);
|
||||
setLikeQuery(false);
|
||||
toggleStatusValue = "Disliked";
|
||||
}
|
||||
setDislikeQuery(!dislikeQuery);
|
||||
setShowCallout(false);
|
||||
document.getElementById("likeStatus").innerHTML = toggleStatusValue;
|
||||
}}
|
||||
/>
|
||||
<span role="status" style={{ position: "absolute", left: "-9999px" }} id="likeStatus"></span>
|
||||
<Separator
|
||||
vertical
|
||||
styles={{
|
||||
root: {
|
||||
"::after": {
|
||||
backgroundColor: "#767676",
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
<CommandBarButton
|
||||
className="copyQuery"
|
||||
onClick={copyGeneratedCode}
|
||||
iconProps={{ iconName: "Copy" }}
|
||||
style={{ fontSize: 12, transition: "background-color 0.3s ease", height: "100%" }}
|
||||
styles={{
|
||||
root: {
|
||||
backgroundColor: "inherit",
|
||||
},
|
||||
}}
|
||||
>
|
||||
Copy code
|
||||
</CommandBarButton>
|
||||
<CommandBarButton
|
||||
className="deleteQuery"
|
||||
onClick={() => {
|
||||
setShowDeletePopup(true);
|
||||
}}
|
||||
iconProps={{ iconName: "Delete" }}
|
||||
style={{ fontSize: 12, transition: "background-color 0.3s ease", height: "100%" }}
|
||||
styles={{
|
||||
root: {
|
||||
backgroundColor: "inherit",
|
||||
},
|
||||
}}
|
||||
>
|
||||
Clear editor
|
||||
</CommandBarButton>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
{isGeneratingQuery && (
|
||||
<ProgressIndicator
|
||||
label="Thinking..."
|
||||
ariaLabel={getAriaLabel()}
|
||||
barHeight={4}
|
||||
styles={{
|
||||
root: {
|
||||
fontSize: 12,
|
||||
width: "100%",
|
||||
bottom: 0,
|
||||
},
|
||||
itemName: {
|
||||
padding: "0px 8px",
|
||||
},
|
||||
itemProgress: {
|
||||
borderBottomLeftRadius: 8,
|
||||
borderBottomRightRadius: 8,
|
||||
padding: 0,
|
||||
},
|
||||
progressBar: {
|
||||
backgroundImage:
|
||||
"linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, rgb(24, 90, 189) 35%, rgb(71, 207, 250) 70%, rgb(180, 124, 248) 92%, rgba(0, 0, 0, 0))",
|
||||
animationDuration: "5s",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
<Stack horizontal>
|
||||
<Image src={CopilotIcon} style={{ width: 24, height: 24 }} alt="Copilot" role="none" />
|
||||
<Text style={{ marginLeft: 8, fontWeight: 600, fontSize: 16 }}>Copilot</Text>
|
||||
<IconButton
|
||||
iconProps={{ imageProps: { src: errorIcon } }}
|
||||
onClick={() => {
|
||||
@@ -698,10 +323,307 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
||||
clearFeedback();
|
||||
resetMessageStates();
|
||||
}}
|
||||
styles={{
|
||||
root: {
|
||||
marginLeft: "auto !important",
|
||||
},
|
||||
}}
|
||||
ariaLabel="Close"
|
||||
title="Close copilot"
|
||||
/>
|
||||
</Stack>
|
||||
<Stack horizontal verticalAlign="center">
|
||||
<TextField
|
||||
id="naturalLanguageInput"
|
||||
value={userPrompt}
|
||||
onChange={handleUserPromptChange}
|
||||
onClick={() => {
|
||||
inputEdited.current = true;
|
||||
setShowSamplePrompts(true);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && userPrompt) {
|
||||
inputEdited.current = true;
|
||||
startGenerateQueryProcess();
|
||||
}
|
||||
}}
|
||||
style={{ lineHeight: 30 }}
|
||||
styles={{ root: { width: "95%" }, fieldGroup: { borderRadius: 6 } }}
|
||||
disabled={isGeneratingQuery}
|
||||
autoComplete="off"
|
||||
placeholder="Ask a question in natural language and we’ll generate the query for you."
|
||||
aria-labelledby="copilot-textfield-label"
|
||||
/>
|
||||
{showPromptTeachingBubble && copilotTeachingBubbleVisible && (
|
||||
<TeachingBubble
|
||||
calloutProps={{ directionalHint: DirectionalHint.bottomCenter }}
|
||||
target="#naturalLanguageInput"
|
||||
hasCloseButton={true}
|
||||
closeButtonAriaLabel="Close"
|
||||
onDismiss={() => toggleCopilotTeachingBubbleVisible(false)}
|
||||
hasSmallHeadline={true}
|
||||
headline="Write a prompt"
|
||||
>
|
||||
Write a prompt here and Copilot will generate the query for you. You can also choose from our{" "}
|
||||
<Link
|
||||
onClick={() => {
|
||||
setShowSamplePrompts(true);
|
||||
toggleCopilotTeachingBubbleVisible(false);
|
||||
}}
|
||||
style={{ color: "white", fontWeight: 600 }}
|
||||
>
|
||||
sample prompts
|
||||
</Link>{" "}
|
||||
or write your own query
|
||||
</TeachingBubble>
|
||||
)}
|
||||
<IconButton
|
||||
iconProps={{ iconName: "Send" }}
|
||||
disabled={isGeneratingQuery || !userPrompt.trim()}
|
||||
style={{ marginLeft: 8 }}
|
||||
onClick={() => startGenerateQueryProcess()}
|
||||
aria-label="Send"
|
||||
/>
|
||||
<div role="alert" aria-label={getAriaLabel()}>
|
||||
{isGeneratingQuery && <Spinner style={{ marginLeft: 8 }} />}
|
||||
</div>
|
||||
{showSamplePrompts && (
|
||||
<Callout
|
||||
styles={{ root: { minWidth: 400, maxWidth: "70vw" } }}
|
||||
target="#naturalLanguageInput"
|
||||
isBeakVisible={false}
|
||||
onDismiss={() => setShowSamplePrompts(false)}
|
||||
directionalHintFixed={true}
|
||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||
alignTargetEdge={true}
|
||||
gapSpace={4}
|
||||
>
|
||||
<Stack>
|
||||
{filteredHistories?.length > 0 && (
|
||||
<Stack>
|
||||
<Text
|
||||
style={{
|
||||
width: "100%",
|
||||
fontSize: 14,
|
||||
fontWeight: 600,
|
||||
color: "#0078D4",
|
||||
marginLeft: 16,
|
||||
padding: "4px 0",
|
||||
}}
|
||||
>
|
||||
Recent
|
||||
</Text>
|
||||
{filteredHistories.map((history, i) => (
|
||||
<DefaultButton
|
||||
key={i}
|
||||
onClick={() => {
|
||||
setUserPrompt(history);
|
||||
setShowSamplePrompts(false);
|
||||
inputEdited.current = true;
|
||||
}}
|
||||
onRenderIcon={() => <Image src={RecentIcon} styles={{ root: { overflow: "unset" } }} />}
|
||||
styles={promptStyles}
|
||||
>
|
||||
{history}
|
||||
</DefaultButton>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
{filteredSuggestedPrompts?.length > 0 && (
|
||||
<Stack>
|
||||
<Text
|
||||
style={{
|
||||
width: "100%",
|
||||
fontSize: 14,
|
||||
fontWeight: 600,
|
||||
color: "#0078D4",
|
||||
marginLeft: 16,
|
||||
padding: "4px 0",
|
||||
}}
|
||||
>
|
||||
Suggested Prompts
|
||||
</Text>
|
||||
{filteredSuggestedPrompts.map((prompt) => (
|
||||
<DefaultButton
|
||||
key={prompt.id}
|
||||
onClick={() => {
|
||||
setUserPrompt(prompt.text);
|
||||
setShowSamplePrompts(false);
|
||||
inputEdited.current = true;
|
||||
}}
|
||||
onRenderIcon={() => <Image src={HintIcon} />}
|
||||
styles={promptStyles}
|
||||
>
|
||||
{prompt.text}
|
||||
</DefaultButton>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
{(filteredHistories?.length > 0 || filteredSuggestedPrompts?.length > 0) && (
|
||||
<Stack>
|
||||
<Separator
|
||||
styles={{
|
||||
root: {
|
||||
selectors: { "::before": { background: "#E1DFDD" } },
|
||||
padding: 0,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
width: "100%",
|
||||
fontSize: 14,
|
||||
marginLeft: 16,
|
||||
padding: "4px 0",
|
||||
}}
|
||||
>
|
||||
Learn about{" "}
|
||||
<Link target="_blank" href="https://aka.ms/cdb-copilot-writing">
|
||||
writing effective prompts
|
||||
</Link>
|
||||
</Text>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
</Callout>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
<Stack style={{ margin: "8px 0" }}>
|
||||
<Text style={{ fontSize: 12 }}>
|
||||
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" }}>
|
||||
Read preview terms
|
||||
</Link>
|
||||
{showErrorMessageBar && (
|
||||
<MessageBar messageBarType={MessageBarType.error}>
|
||||
{errorMessage ? errorMessage : "We ran into an error and were not able to execute query."}
|
||||
</MessageBar>
|
||||
)}
|
||||
{showInvalidQueryMessageBar && (
|
||||
<MessageBar
|
||||
messageBarType={MessageBarType.info}
|
||||
styles={{ root: { backgroundColor: "#F0F6FF" }, icon: { color: "#015CDA" } }}
|
||||
>
|
||||
We were unable to generate a query based upon the prompt provided. Please modify the prompt and try again.
|
||||
For examples of how to write a good prompt, please read
|
||||
<Link href="https://aka.ms/cdb-copilot-writing" target="_blank">
|
||||
this article.
|
||||
</Link>{" "}
|
||||
Our content guidelines can be found
|
||||
<Link href="https://aka.ms/cdb-query-copilot" target="_blank">
|
||||
here.
|
||||
</Link>
|
||||
</MessageBar>
|
||||
)}
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
{showFeedbackBar && (
|
||||
<Stack
|
||||
style={{ backgroundColor: "#FFF8F0", padding: "2px 8px", minHeight: 32 }}
|
||||
horizontal
|
||||
verticalAlign="center"
|
||||
>
|
||||
{userContext.feedbackPolicies?.policyAllowFeedback && (
|
||||
<Stack horizontal verticalAlign="center">
|
||||
<Text style={{ fontWeight: 600, fontSize: 12 }}>Provide feedback on the query generated</Text>
|
||||
{showCallout && !hideFeedbackModalForLikedQueries && (
|
||||
<Callout
|
||||
role="status"
|
||||
style={{ padding: 8 }}
|
||||
target="#likeBtn"
|
||||
onDismiss={() => {
|
||||
setShowCallout(false);
|
||||
SubmitFeedback({
|
||||
params: {
|
||||
generatedQuery: generatedQuery,
|
||||
likeQuery: likeQuery,
|
||||
description: "",
|
||||
userPrompt: userPrompt,
|
||||
},
|
||||
explorer,
|
||||
databaseId,
|
||||
containerId,
|
||||
mode: isSampleCopilotActive ? "Sample" : "User",
|
||||
});
|
||||
}}
|
||||
directionalHint={DirectionalHint.topCenter}
|
||||
>
|
||||
<Text>
|
||||
Thank you. Need to give{" "}
|
||||
<Link
|
||||
onClick={() => {
|
||||
setShowCallout(false);
|
||||
openFeedbackModal(generatedQuery, true, userPrompt);
|
||||
}}
|
||||
>
|
||||
more feedback?
|
||||
</Link>
|
||||
</Text>
|
||||
</Callout>
|
||||
)}
|
||||
<IconButton
|
||||
id="likeBtn"
|
||||
style={{ marginLeft: 20 }}
|
||||
aria-label="Like"
|
||||
role="toggle"
|
||||
iconProps={{ iconName: likeQuery === true ? "LikeSolid" : "Like" }}
|
||||
onClick={() => {
|
||||
setShowCallout(!likeQuery);
|
||||
setLikeQuery(!likeQuery);
|
||||
if (likeQuery === true) {
|
||||
document.getElementById("likeStatus").innerHTML = "Unpressed";
|
||||
}
|
||||
if (likeQuery === false) {
|
||||
document.getElementById("likeStatus").innerHTML = "Liked";
|
||||
}
|
||||
if (dislikeQuery) {
|
||||
setDislikeQuery(!dislikeQuery);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
style={{ margin: "0 10px" }}
|
||||
role="toggle"
|
||||
aria-label="Dislike"
|
||||
iconProps={{ iconName: dislikeQuery === true ? "DislikeSolid" : "Dislike" }}
|
||||
onClick={() => {
|
||||
let toggleStatusValue = "Unpressed";
|
||||
if (!dislikeQuery) {
|
||||
openFeedbackModal(generatedQuery, false, userPrompt);
|
||||
setLikeQuery(false);
|
||||
toggleStatusValue = "Disliked";
|
||||
}
|
||||
setDislikeQuery(!dislikeQuery);
|
||||
setShowCallout(false);
|
||||
document.getElementById("likeStatus").innerHTML = toggleStatusValue;
|
||||
}}
|
||||
/>
|
||||
<span role="status" style={{ position: "absolute", left: "-9999px" }} id="likeStatus"></span>
|
||||
<Separator vertical style={{ color: "#EDEBE9" }} />
|
||||
</Stack>
|
||||
)}
|
||||
<CommandBarButton
|
||||
className="copyQuery"
|
||||
onClick={copyGeneratedCode}
|
||||
iconProps={{ iconName: "Copy" }}
|
||||
style={{ margin: "0 10px", backgroundColor: "#FFF8F0", transition: "background-color 0.3s ease" }}
|
||||
>
|
||||
Copy query
|
||||
</CommandBarButton>
|
||||
<CommandBarButton
|
||||
className="deleteQuery"
|
||||
onClick={() => {
|
||||
setShowDeletePopup(true);
|
||||
}}
|
||||
iconProps={{ iconName: "Delete" }}
|
||||
style={{ margin: "0 10px", backgroundColor: "#FFF8F0", transition: "background-color 0.3s ease" }}
|
||||
>
|
||||
Delete query
|
||||
</CommandBarButton>
|
||||
</Stack>
|
||||
)}
|
||||
{isSamplePromptsOpen && <SamplePrompts sampleProps={sampleProps} />}
|
||||
{query !== "" && query.trim().length !== 0 && (
|
||||
<DeletePopup
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { FeedOptions } from "@azure/cosmos";
|
||||
import {
|
||||
Areas,
|
||||
BackendApi,
|
||||
ConnectionStatusType,
|
||||
ContainerStatusType,
|
||||
HttpStatusCodes,
|
||||
@@ -31,7 +30,6 @@ import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||
import { traceFailure, traceStart, traceSuccess } from "Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "UserContext";
|
||||
import { getAuthorizationHeader } from "Utils/AuthorizationUtils";
|
||||
import { useNewPortalBackendEndpoint } from "Utils/EndpointUtils";
|
||||
import { queryPagesUntilContentPresent } from "Utils/QueryUtils";
|
||||
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
|
||||
import { useTabs } from "hooks/useTabs";
|
||||
@@ -82,11 +80,7 @@ export const isCopilotFeatureRegistered = async (subscriptionId: string): Promis
|
||||
};
|
||||
|
||||
export const getCopilotEnabled = async (): Promise<boolean> => {
|
||||
const backendEndpoint: string = useNewPortalBackendEndpoint(BackendApi.PortalSettings)
|
||||
? configContext.PORTAL_BACKEND_ENDPOINT
|
||||
: configContext.BACKEND_ENDPOINT;
|
||||
|
||||
const url = `${backendEndpoint}/api/portalsettings/querycopilot`;
|
||||
const url = `${configContext.BACKEND_ENDPOINT}/api/portalsettings/querycopilot`;
|
||||
const authorizationHeader: AuthorizationTokenHeaderMetadata = getAuthorizationHeader();
|
||||
const headers = { [authorizationHeader.header]: authorizationHeader.token };
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import * as ko from "knockout";
|
||||
import Q from "q";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import * as Constants from "../../Common/Constants";
|
||||
import { CassandraProxyAPIs, CassandraProxyEndpoints } from "../../Common/Constants";
|
||||
import { handleError } from "../../Common/ErrorHandlingUtils";
|
||||
import * as HeadersUtility from "../../Common/HeadersUtility";
|
||||
import { createDocument } from "../../Common/dataAccess/createDocument";
|
||||
@@ -20,6 +19,7 @@ import Explorer from "../Explorer";
|
||||
import * as TableConstants from "./Constants";
|
||||
import * as Entities from "./Entities";
|
||||
import * as TableEntityProcessor from "./TableEntityProcessor";
|
||||
import { CassandraProxyAPIs } from "../../Common/Constants";
|
||||
|
||||
export interface CassandraTableKeys {
|
||||
partitionKeys: CassandraTableKey[];
|
||||
@@ -458,7 +458,7 @@ export class CassandraAPIDataClient extends TableDataClient {
|
||||
}
|
||||
|
||||
public getTableKeys(collection: ViewModels.Collection): Q.Promise<CassandraTableKeys> {
|
||||
if (!this.useCassandraProxyEndpoint("getKeys")) {
|
||||
if (!this.useCassandraProxyEndpoint("getTableKeys")) {
|
||||
return this.getTableKeys_ToBeDeprecated(collection);
|
||||
}
|
||||
|
||||
@@ -732,22 +732,17 @@ export class CassandraAPIDataClient extends TableDataClient {
|
||||
}
|
||||
|
||||
private useCassandraProxyEndpoint(api: string): boolean {
|
||||
const activeCassandraProxyEndpoints: string[] = [
|
||||
CassandraProxyEndpoints.Mpac,
|
||||
CassandraProxyEndpoints.Prod,
|
||||
];
|
||||
let canAccessCassandraProxy: boolean = userContext.databaseAccount.properties.publicNetworkAccess === "Enabled";
|
||||
if (
|
||||
configContext.CASSANDRA_PROXY_ENDPOINT !== CassandraProxyEndpoints.Development &&
|
||||
userContext.databaseAccount.properties.ipRules?.length > 0
|
||||
) {
|
||||
if (userContext.databaseAccount.properties.ipRules?.length > 0) {
|
||||
canAccessCassandraProxy = canAccessCassandraProxy && configContext.CASSANDRA_PROXY_OUTBOUND_IPS_ALLOWLISTED;
|
||||
}
|
||||
|
||||
return (
|
||||
canAccessCassandraProxy &&
|
||||
configContext.NEW_CASSANDRA_APIS?.includes(api) &&
|
||||
activeCassandraProxyEndpoints.includes(configContext.CASSANDRA_PROXY_ENDPOINT)
|
||||
[Constants.CassandraProxyEndpoints.Development, Constants.CassandraProxyEndpoints.Mpac].includes(
|
||||
configContext.CASSANDRA_PROXY_ENDPOINT,
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { sendMessage } from "Common/MessageHandler";
|
||||
import { MessageTypes } from "Contracts/MessageTypes";
|
||||
import { CopilotProvider } from "Explorer/QueryCopilot/QueryCopilotContext";
|
||||
import { userContext } from "UserContext";
|
||||
import React from "react";
|
||||
@@ -56,11 +54,6 @@ export class NewQueryTab extends TabsBase {
|
||||
);
|
||||
}
|
||||
|
||||
public onActivate(): void {
|
||||
this.propagateTabInformation(MessageTypes.ActivateTab);
|
||||
super.onActivate();
|
||||
}
|
||||
|
||||
public onTabClick(): void {
|
||||
useTabs.getState().activateTab(this);
|
||||
this.iTabAccessor.onTabClickEvent();
|
||||
@@ -68,7 +61,6 @@ export class NewQueryTab extends TabsBase {
|
||||
|
||||
public onCloseTabButtonClick(): void {
|
||||
useTabs.getState().closeTab(this);
|
||||
this.propagateTabInformation(MessageTypes.CloseTab);
|
||||
if (this.iTabAccessor) {
|
||||
this.iTabAccessor.onCloseClickEvent(true);
|
||||
}
|
||||
@@ -77,15 +69,4 @@ export class NewQueryTab extends TabsBase {
|
||||
public getContainer(): Explorer {
|
||||
return this.props.container;
|
||||
}
|
||||
|
||||
private propagateTabInformation(type: MessageTypes): void {
|
||||
sendMessage({
|
||||
type,
|
||||
data: {
|
||||
kind: this.tabKind,
|
||||
databaseId: this.collection?.databaseId,
|
||||
collectionId: this.collection?.id?.(),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||
|
||||
this.state = {
|
||||
toggleState: ToggleState.Result,
|
||||
sqlQueryEditorContent: props.isPreferredApiMongoDB ? "{}" : props.queryText || "SELECT * FROM c",
|
||||
sqlQueryEditorContent: props.queryText || "SELECT * FROM c",
|
||||
selectedContent: "",
|
||||
queryResults: undefined,
|
||||
error: "",
|
||||
@@ -496,16 +496,13 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||
};
|
||||
|
||||
public onChangeContent(newContent: string): void {
|
||||
// The copilot store's active query takes precedence over the local state,
|
||||
// and we can't update both states in a single operation.
|
||||
// So, we update the copilot store's state first, then update the local state.
|
||||
if (this.state.copilotActive) {
|
||||
this.props.copilotStore?.setQuery(newContent);
|
||||
}
|
||||
this.setState({
|
||||
sqlQueryEditorContent: newContent,
|
||||
queryCopilotGeneratedQuery: "",
|
||||
});
|
||||
if (this.state.copilotActive) {
|
||||
this.props.copilotStore?.setQuery(newContent);
|
||||
}
|
||||
if (this.isPreferredApiMongoDB) {
|
||||
if (newContent.length > 0) {
|
||||
this.executeQueryButton = {
|
||||
@@ -547,7 +544,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
||||
}
|
||||
|
||||
public getEditorContent(): string {
|
||||
public setEditorContent(): string {
|
||||
if (this.isCopilotTabActive && this.state.queryCopilotGeneratedQuery) {
|
||||
return this.state.queryCopilotGeneratedQuery;
|
||||
}
|
||||
@@ -604,7 +601,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||
<div className="queryEditor" style={{ height: "100%" }}>
|
||||
<EditorReact
|
||||
language={"sql"}
|
||||
content={this.getEditorContent()}
|
||||
content={this.setEditorContent()}
|
||||
isReadOnly={false}
|
||||
wordWrap={"on"}
|
||||
ariaLabel={"Editing Query"}
|
||||
|
||||
@@ -324,17 +324,13 @@ const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): J
|
||||
|
||||
const showMongoAndCassandraProxiesNetworkSettingsWarning = (): boolean => {
|
||||
const ipRules: IpRule[] = userContext.databaseAccount?.properties?.ipRules;
|
||||
if (
|
||||
((userContext.apiType === "Mongo" && configContext.MONGO_PROXY_ENDPOINT !== MongoProxyEndpoints.Development) ||
|
||||
(userContext.apiType === "Cassandra" &&
|
||||
configContext.CASSANDRA_PROXY_ENDPOINT !== CassandraProxyEndpoints.Development)) &&
|
||||
ipRules?.length
|
||||
) {
|
||||
if ((userContext.apiType === "Mongo" || userContext.apiType === "Cassandra") && ipRules?.length) {
|
||||
const legacyPortalBackendIPs: string[] = PortalBackendIPs[configContext.BACKEND_ENDPOINT];
|
||||
const ipAddressesFromIPRules: string[] = ipRules.map((ipRule) => ipRule.ipAddressOrRange);
|
||||
const ipRulesIncludeLegacyPortalBackend: boolean = legacyPortalBackendIPs.every((legacyPortalBackendIP: string) =>
|
||||
ipAddressesFromIPRules.includes(legacyPortalBackendIP),
|
||||
);
|
||||
const ipRulesIncludeLegacyPortalBackend: boolean =
|
||||
ipAddressesFromIPRules.filter((ipAddressFromIPRule) => legacyPortalBackendIPs.includes(ipAddressFromIPRule))
|
||||
?.length === legacyPortalBackendIPs.length;
|
||||
|
||||
if (!ipRulesIncludeLegacyPortalBackend) {
|
||||
return false;
|
||||
}
|
||||
@@ -348,9 +344,9 @@ const showMongoAndCassandraProxiesNetworkSettingsWarning = (): boolean => {
|
||||
? [...MongoProxyOutboundIPs[MongoProxyEndpoints.Mpac], ...MongoProxyOutboundIPs[MongoProxyEndpoints.Prod]]
|
||||
: MongoProxyOutboundIPs[configContext.MONGO_PROXY_ENDPOINT];
|
||||
|
||||
const ipRulesIncludeMongoProxy: boolean = mongoProxyOutboundIPs.every((mongoProxyOutboundIP: string) =>
|
||||
ipAddressesFromIPRules.includes(mongoProxyOutboundIP),
|
||||
);
|
||||
const ipRulesIncludeMongoProxy: boolean =
|
||||
ipAddressesFromIPRules.filter((ipAddressFromIPRule) => mongoProxyOutboundIPs.includes(ipAddressFromIPRule))
|
||||
?.length === mongoProxyOutboundIPs.length;
|
||||
|
||||
if (ipRulesIncludeMongoProxy) {
|
||||
updateConfigContext({
|
||||
@@ -372,15 +368,9 @@ const showMongoAndCassandraProxiesNetworkSettingsWarning = (): boolean => {
|
||||
]
|
||||
: CassandraProxyOutboundIPs[configContext.CASSANDRA_PROXY_ENDPOINT];
|
||||
|
||||
const ipRulesIncludeCassandraProxy: boolean = cassandraProxyOutboundIPs.every(
|
||||
(cassandraProxyOutboundIP: string) => ipAddressesFromIPRules.includes(cassandraProxyOutboundIP),
|
||||
);
|
||||
|
||||
if (ipRulesIncludeCassandraProxy) {
|
||||
updateConfigContext({
|
||||
CASSANDRA_PROXY_OUTBOUND_IPS_ALLOWLISTED: true,
|
||||
});
|
||||
}
|
||||
const ipRulesIncludeCassandraProxy: boolean =
|
||||
ipAddressesFromIPRules.filter((ipAddressFromIPRule) => cassandraProxyOutboundIPs.includes(ipAddressFromIPRule))
|
||||
?.length === cassandraProxyOutboundIPs.length;
|
||||
|
||||
return !ipRulesIncludeCassandraProxy;
|
||||
}
|
||||
|
||||
@@ -40,10 +40,11 @@ export default class TabsBase extends WaitsForTemplateViewModel {
|
||||
this.database = options.database;
|
||||
this.rid = options.rid || (this.collection && this.collection.rid) || "";
|
||||
this.tabKind = options.tabKind;
|
||||
this.tabTitle = ko.observable<string>(this.getTitle(options));
|
||||
this.tabTitle = ko.observable<string>(options.title);
|
||||
this.tabPath =
|
||||
this.collection &&
|
||||
ko.observable<string>(`${this.collection.databaseId}>${this.collection.id()}>${options.title}`);
|
||||
ko.observable(options.tabPath ?? "") ||
|
||||
(this.collection &&
|
||||
ko.observable<string>(`${this.collection.databaseId}>${this.collection.id()}>${this.tabTitle()}`));
|
||||
this.pendingNotification = ko.observable<DataModels.Notification>(undefined);
|
||||
this.onLoadStartKey = options.onLoadStartKey;
|
||||
this.closeTabButton = {
|
||||
@@ -142,26 +143,6 @@ export default class TabsBase extends WaitsForTemplateViewModel {
|
||||
return (this.collection && this.collection.container) || (this.database && this.database.container);
|
||||
}
|
||||
|
||||
public getTitle(options: ViewModels.TabOptions): string {
|
||||
const coll = this.collection?.id();
|
||||
const db = this.database?.id();
|
||||
if (coll) {
|
||||
if (coll.length > 8) {
|
||||
return coll.slice(0, 5) + "…" + options.title;
|
||||
} else {
|
||||
return coll + "." + options.title;
|
||||
}
|
||||
} else if (db) {
|
||||
if (db.length > 8) {
|
||||
return db.slice(0, 5) + "…" + options.title;
|
||||
} else {
|
||||
return db + "." + options.title;
|
||||
}
|
||||
} else {
|
||||
return options.title;
|
||||
}
|
||||
}
|
||||
|
||||
/** Renders a Javascript object to be displayed inside Monaco Editor */
|
||||
public renderObjectForEditor(value: any, replacer: any, space: string | number): string {
|
||||
return JSON.stringify(value, replacer, space);
|
||||
|
||||
@@ -308,7 +308,7 @@ export default class Collection implements ViewModels.Collection {
|
||||
collectionName: this.id(),
|
||||
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
tabTitle: "Items",
|
||||
tabTitle: this.rawDataModel.id + " - Items",
|
||||
});
|
||||
this.documentIds([]);
|
||||
|
||||
@@ -316,7 +316,7 @@ export default class Collection implements ViewModels.Collection {
|
||||
partitionKey: this.partitionKey,
|
||||
documentIds: ko.observableArray<DocumentId>([]),
|
||||
tabKind: ViewModels.CollectionTabKind.Documents,
|
||||
title: "Items",
|
||||
title: this.rawDataModel.id + " - Items",
|
||||
collection: this,
|
||||
node: this,
|
||||
tabPath: `${this.databaseId}>${this.id()}>Documents`,
|
||||
|
||||
@@ -373,6 +373,11 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
|
||||
iconSrc: NewNotebookIcon,
|
||||
onClick: () => container.onCreateDirectory(item, isGithubTree),
|
||||
},
|
||||
{
|
||||
label: "New Notebook",
|
||||
iconSrc: NewNotebookIcon,
|
||||
onClick: () => container.onNewNotebookClicked(item, isGithubTree),
|
||||
},
|
||||
{
|
||||
label: "Upload File",
|
||||
iconSrc: NewNotebookIcon,
|
||||
@@ -781,6 +786,9 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
|
||||
<AccordionItemComponent title={"DATA"} isExpanded={!gitHubNotebooksContentRoot}>
|
||||
<TreeComponent className="dataResourceTree" rootNode={dataRootNode} />
|
||||
</AccordionItemComponent>
|
||||
<AccordionItemComponent title={"NOTEBOOKS"}>
|
||||
<TreeComponent className="notebookResourceTree" rootNode={buildNotebooksTree()} />
|
||||
</AccordionItemComponent>
|
||||
</AccordionComponent>
|
||||
|
||||
{/* {buildGalleryCallout()} */}
|
||||
|
||||
@@ -800,6 +800,11 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
||||
iconSrc: NewNotebookIcon,
|
||||
onClick: () => this.container.onCreateDirectory(item),
|
||||
},
|
||||
{
|
||||
label: "New Notebook",
|
||||
iconSrc: NewNotebookIcon,
|
||||
onClick: () => this.container.onNewNotebookClicked(item),
|
||||
},
|
||||
{
|
||||
label: "Upload File",
|
||||
iconSrc: NewNotebookIcon,
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
// Import this first, to ensure that the dev tools hook is copied before React is loaded.
|
||||
import "./ReactDevTools";
|
||||
|
||||
// CSS Dependencies
|
||||
import { initializeIcons, loadTheme } from "@fluentui/react";
|
||||
import { QuickstartCarousel } from "Explorer/Quickstart/QuickstartCarousel";
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { useBoolean } from "@fluentui/react-hooks";
|
||||
import { userContext } from "UserContext";
|
||||
import { useNewPortalBackendEndpoint } from "Utils/EndpointUtils";
|
||||
import * as React from "react";
|
||||
import ConnectImage from "../../../../images/HdeConnectCosmosDB.svg";
|
||||
import ErrorImage from "../../../../images/error.svg";
|
||||
import { AuthType } from "../../../AuthType";
|
||||
import { BackendApi, HttpHeaders } from "../../../Common/Constants";
|
||||
import { HttpHeaders } from "../../../Common/Constants";
|
||||
import { configContext } from "../../../ConfigContext";
|
||||
import { GenerateTokenResponse } from "../../../Contracts/DataModels";
|
||||
import { isResourceTokenConnectionString } from "../Helpers/ResourceTokenUtils";
|
||||
@@ -19,23 +18,6 @@ interface Props {
|
||||
}
|
||||
|
||||
export const fetchEncryptedToken = async (connectionString: string): Promise<string> => {
|
||||
if (!useNewPortalBackendEndpoint(BackendApi.GenerateToken)) {
|
||||
return await fetchEncryptedToken_ToBeDeprecated(connectionString);
|
||||
}
|
||||
|
||||
const headers = new Headers();
|
||||
headers.append(HttpHeaders.connectionString, connectionString);
|
||||
const url = configContext.PORTAL_BACKEND_ENDPOINT + "/api/connectionstring/token/generatetoken";
|
||||
const response = await fetch(url, { headers, method: "POST" });
|
||||
if (!response.ok) {
|
||||
throw response;
|
||||
}
|
||||
|
||||
const encryptedTokenResponse: string = await response.json();
|
||||
return decodeURIComponent(encryptedTokenResponse);
|
||||
};
|
||||
|
||||
export const fetchEncryptedToken_ToBeDeprecated = async (connectionString: string): Promise<string> => {
|
||||
const headers = new Headers();
|
||||
headers.append(HttpHeaders.connectionString, connectionString);
|
||||
const url = configContext.BACKEND_ENDPOINT + "/api/guest/tokens/generateToken";
|
||||
|
||||
@@ -14,7 +14,6 @@ export type Features = {
|
||||
readonly enableTtl: boolean;
|
||||
readonly executeSproc: boolean;
|
||||
readonly enableAadDataPlane: boolean;
|
||||
readonly enableDataPlaneRbac: boolean;
|
||||
readonly enableResourceGraph: boolean;
|
||||
readonly enableKoResourceTree: boolean;
|
||||
readonly hostedDataExplorer: boolean;
|
||||
@@ -75,7 +74,6 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
|
||||
canExceedMaximumValue: "true" === get("canexceedmaximumvalue"),
|
||||
cosmosdb: "true" === get("cosmosdb"),
|
||||
enableAadDataPlane: "true" === get("enableaaddataplane"),
|
||||
enableDataPlaneRbac: "true" === get("enabledataplanerbac"),
|
||||
enableResourceGraph: "true" === get("enableresourcegraph"),
|
||||
enableChangeFeedPolicy: "true" === get("enablechangefeedpolicy"),
|
||||
enableFixedCollectionWithSharedThroughput: "true" === get("enablefixedcollectionwithsharedthroughput"),
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
if (window.parent !== window) {
|
||||
try {
|
||||
(window as any).__REACT_DEVTOOLS_GLOBAL_HOOK__ = (window.parent as any).__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
||||
} catch {
|
||||
// No-op. We can throw here if the parent is not the same origin (such as in the Azure portal).
|
||||
}
|
||||
(window as any).__REACT_DEVTOOLS_GLOBAL_HOOK__ = (window.parent as any).__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
||||
}
|
||||
|
||||
@@ -5,9 +5,6 @@ import * as StringUtility from "./StringUtility";
|
||||
export { LocalStorageUtility, SessionStorageUtility };
|
||||
export enum StorageKey {
|
||||
ActualItemPerPage,
|
||||
DataPlaneRbacEnabled,
|
||||
DataPlaneRbacDisabled,
|
||||
isDataPlaneRbacAutomatic,
|
||||
RUThresholdEnabled,
|
||||
RUThreshold,
|
||||
QueryTimeoutEnabled,
|
||||
|
||||
@@ -51,7 +51,6 @@ interface FabricContext {
|
||||
connectionId: string;
|
||||
databaseConnectionInfo: FabricDatabaseConnectionInfo | undefined;
|
||||
isReadOnly: boolean;
|
||||
isVisible: boolean;
|
||||
}
|
||||
|
||||
export type AdminFeedbackControlPolicy =
|
||||
@@ -101,7 +100,6 @@ interface UserContext {
|
||||
sampleDataConnectionInfo?: ParsedResourceTokenConnectionString;
|
||||
readonly vcoreMongoConnectionParams?: VCoreMongoConnectionParams;
|
||||
readonly feedbackPolicies?: AdminFeedbackPolicySettings;
|
||||
readonly dataPlaneRbacEnabled?: boolean;
|
||||
}
|
||||
|
||||
export type ApiType = "SQL" | "Mongo" | "Gremlin" | "Tables" | "Cassandra" | "Postgres" | "VCoreMongo";
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
import {
|
||||
BackendApi,
|
||||
CassandraProxyEndpoints,
|
||||
JunoEndpoints,
|
||||
MongoProxyEndpoints,
|
||||
PortalBackendEndpoints,
|
||||
} from "Common/Constants";
|
||||
import { configContext } from "ConfigContext";
|
||||
import { CassandraProxyEndpoints, JunoEndpoints, MongoProxyEndpoints } from "Common/Constants";
|
||||
import * as Logger from "../Common/Logger";
|
||||
|
||||
export function validateEndpoint(
|
||||
@@ -144,23 +137,3 @@ export const allowedJunoOrigins: ReadonlyArray<string> = [
|
||||
];
|
||||
|
||||
export const allowedNotebookServerUrls: ReadonlyArray<string> = [];
|
||||
|
||||
//
|
||||
// Temporary function to determine if a portal backend API is supported by the
|
||||
// new backend in this environment.
|
||||
//
|
||||
// TODO: Remove this function once new backend migration is completed for all environments.
|
||||
//
|
||||
export function useNewPortalBackendEndpoint(backendApi: string): boolean {
|
||||
// This maps backend APIs to the environments supported by the new backend.
|
||||
const newBackendApiEnvironmentMap: { [key: string]: string[] } = {
|
||||
[BackendApi.GenerateToken]: [PortalBackendEndpoints.Development],
|
||||
[BackendApi.PortalSettings]: [PortalBackendEndpoints.Development, PortalBackendEndpoints.Mpac],
|
||||
};
|
||||
|
||||
if (!newBackendApiEnvironmentMap[backendApi] || !configContext.PORTAL_BACKEND_ENDPOINT) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return newBackendApiEnvironmentMap[backendApi].includes(configContext.PORTAL_BACKEND_ENDPOINT);
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@ import { createUri } from "Common/UrlUtility";
|
||||
import { DATA_EXPLORER_RPC_VERSION } from "Contracts/DataExplorerMessagesContract";
|
||||
import { FABRIC_RPC_VERSION, FabricMessageV2 } from "Contracts/FabricMessagesContract";
|
||||
import Explorer from "Explorer/Explorer";
|
||||
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
||||
import { useSelectedNode } from "Explorer/useSelectedNode";
|
||||
import { scheduleRefreshDatabaseResourceToken } from "Platform/Fabric/FabricUtil";
|
||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||
import { getNetworkSettingsWarningMessage } from "Utils/NetworkUtility";
|
||||
import { logConsoleError } from "Utils/NotificationConsoleUtils";
|
||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||
@@ -90,7 +90,6 @@ async function configureFabric(): Promise<Explorer> {
|
||||
// These are the versions of Fabric that Data Explorer supports.
|
||||
const SUPPORTED_FABRIC_VERSIONS = [FABRIC_RPC_VERSION];
|
||||
|
||||
let firstContainerOpened = false;
|
||||
let explorer: Explorer;
|
||||
return new Promise<Explorer>((resolve) => {
|
||||
window.addEventListener(
|
||||
@@ -122,10 +121,7 @@ async function configureFabric(): Promise<Explorer> {
|
||||
await scheduleRefreshDatabaseResourceToken(true);
|
||||
resolve(explorer);
|
||||
await explorer.refreshAllDatabases();
|
||||
if (userContext.fabricContext.isVisible && !firstContainerOpened) {
|
||||
firstContainerOpened = true;
|
||||
openFirstContainer(explorer, userContext.fabricContext.databaseConnectionInfo.databaseId);
|
||||
}
|
||||
openFirstContainer(explorer, userContext.fabricContext.databaseConnectionInfo.databaseId);
|
||||
break;
|
||||
}
|
||||
case "newContainer":
|
||||
@@ -136,16 +132,8 @@ async function configureFabric(): Promise<Explorer> {
|
||||
handleCachedDataMessage(data);
|
||||
break;
|
||||
}
|
||||
case "explorerVisible": {
|
||||
userContext.fabricContext.isVisible = data.message.visible;
|
||||
if (
|
||||
userContext.fabricContext.isVisible &&
|
||||
!firstContainerOpened &&
|
||||
userContext?.fabricContext?.databaseConnectionInfo?.databaseId !== undefined
|
||||
) {
|
||||
firstContainerOpened = true;
|
||||
openFirstContainer(explorer, userContext.fabricContext.databaseConnectionInfo.databaseId);
|
||||
}
|
||||
case "setToolbarStatus": {
|
||||
useCommandBar.getState().setIsHidden(data.message.visible === false);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
@@ -271,30 +259,8 @@ async function configureHostedWithAAD(config: AAD): Promise<Explorer> {
|
||||
}
|
||||
}
|
||||
try {
|
||||
if(LocalStorageUtility.hasItem(StorageKey.DataPlaneRbacEnabled)) {
|
||||
var isDataPlaneRbacSetting = LocalStorageUtility.getEntryString(StorageKey.DataPlaneRbacEnabled);
|
||||
if (isDataPlaneRbacSetting == "Automatic")
|
||||
{
|
||||
if (!account.properties.disableLocalAuth) {
|
||||
keys = await listKeys(subscriptionId, resourceGroup, account.name);
|
||||
}
|
||||
else {
|
||||
updateUserContext({
|
||||
dataPlaneRbacEnabled: true
|
||||
});
|
||||
}
|
||||
}
|
||||
else if(isDataPlaneRbacSetting == "True") {
|
||||
updateUserContext({
|
||||
dataPlaneRbacEnabled: true
|
||||
});
|
||||
}
|
||||
else {
|
||||
keys = await listKeys(subscriptionId, resourceGroup, account.name);
|
||||
updateUserContext({
|
||||
dataPlaneRbacEnabled: false
|
||||
});
|
||||
}
|
||||
if (!account.properties.disableLocalAuth) {
|
||||
keys = await listKeys(subscriptionId, resourceGroup, account.name);
|
||||
}
|
||||
} catch (e) {
|
||||
if (userContext.features.enableAadDataPlane) {
|
||||
@@ -361,13 +327,12 @@ function configureHostedWithResourceToken(config: ResourceToken): Explorer {
|
||||
return explorer;
|
||||
}
|
||||
|
||||
function createExplorerFabric(params: { connectionId: string; isVisible: boolean }): Explorer {
|
||||
function createExplorerFabric(params: { connectionId: string }): Explorer {
|
||||
updateUserContext({
|
||||
fabricContext: {
|
||||
connectionId: params.connectionId,
|
||||
databaseConnectionInfo: undefined,
|
||||
isReadOnly: true,
|
||||
isVisible: params.isVisible ?? true,
|
||||
},
|
||||
authType: AuthType.ConnectionString,
|
||||
databaseAccount: {
|
||||
@@ -416,9 +381,8 @@ async function configurePortal(): Promise<Explorer> {
|
||||
updateUserContext({
|
||||
authType: AuthType.AAD,
|
||||
});
|
||||
|
||||
let explorer: Explorer;
|
||||
return new Promise(async (resolve) => {
|
||||
return new Promise((resolve) => {
|
||||
// In development mode, try to load the iframe message from session storage.
|
||||
// This allows webpack hot reload to function properly in the portal
|
||||
if (process.env.NODE_ENV === "development" && !window.location.search.includes("disablePortalInitCache")) {
|
||||
@@ -431,7 +395,6 @@ async function configurePortal(): Promise<Explorer> {
|
||||
console.dir(message);
|
||||
updateContextsFromPortalMessage(message);
|
||||
explorer = new Explorer();
|
||||
|
||||
// In development mode, save the iframe message from the portal in session storage.
|
||||
// This allows webpack hot reload to funciton properly
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
@@ -440,11 +403,11 @@ async function configurePortal(): Promise<Explorer> {
|
||||
resolve(explorer);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// In the Portal, configuration of Explorer happens via iframe message
|
||||
window.addEventListener(
|
||||
"message",
|
||||
async (event) => {
|
||||
(event) => {
|
||||
if (isInvalidParentFrameOrigin(event)) {
|
||||
return;
|
||||
}
|
||||
@@ -474,37 +437,6 @@ async function configurePortal(): Promise<Explorer> {
|
||||
setTimeout(() => explorer.openNPSSurveyDialog(), 3000);
|
||||
}
|
||||
|
||||
let dbAccount = userContext.databaseAccount;
|
||||
let keys: DatabaseAccountListKeysResult = {};
|
||||
const account = userContext.databaseAccount;
|
||||
const subscriptionId = userContext.subscriptionId;
|
||||
const resourceGroup = userContext.resourceGroup;
|
||||
if(LocalStorageUtility.hasItem(StorageKey.DataPlaneRbacEnabled)) {
|
||||
var isDataPlaneRbacSetting = LocalStorageUtility.getEntryString(StorageKey.DataPlaneRbacEnabled);
|
||||
if (isDataPlaneRbacSetting == "Automatic")
|
||||
{
|
||||
if (!account.properties.disableLocalAuth) {
|
||||
keys = await listKeys(subscriptionId, resourceGroup, account.name);
|
||||
}
|
||||
else {
|
||||
updateUserContext({
|
||||
dataPlaneRbacEnabled: true
|
||||
});
|
||||
}
|
||||
}
|
||||
else if(isDataPlaneRbacSetting == "True") {
|
||||
updateUserContext({
|
||||
dataPlaneRbacEnabled: true
|
||||
});
|
||||
}
|
||||
else {
|
||||
keys = await listKeys(subscriptionId, resourceGroup, account.name);
|
||||
updateUserContext({
|
||||
dataPlaneRbacEnabled: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (openAction) {
|
||||
handleOpenAction(openAction, useDatabases.getState().databases, explorer);
|
||||
}
|
||||
@@ -525,11 +457,9 @@ async function configurePortal(): Promise<Explorer> {
|
||||
},
|
||||
false,
|
||||
);
|
||||
|
||||
|
||||
sendReadyMessage();
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function shouldForwardMessage(message: PortalMessage, messageOrigin: string) {
|
||||
@@ -555,7 +485,6 @@ function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) {
|
||||
ARM_ENDPOINT: normalizeArmEndpoint(inputs.csmEndpoint || configContext.ARM_ENDPOINT),
|
||||
MONGO_PROXY_ENDPOINT: inputs.mongoProxyEndpoint,
|
||||
CASSANDRA_PROXY_ENDPOINT: inputs.cassandraProxyEndpoint,
|
||||
PORTAL_BACKEND_ENDPOINT: inputs.portalBackendEndpoint,
|
||||
});
|
||||
|
||||
updateUserContext({
|
||||
|
||||
Reference in New Issue
Block a user