mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-01-25 12:44:06 +00:00
Compare commits
6 Commits
pg_fix
...
users/sind
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad165ae069 | ||
|
|
913a96afec | ||
|
|
e26207e949 | ||
|
|
5d6273889d | ||
|
|
889cf77801 | ||
|
|
0975591945 |
@@ -147,6 +147,7 @@
|
|||||||
|
|
||||||
// CommandBar
|
// CommandBar
|
||||||
@CommandBarButtonHeight: 40px;
|
@CommandBarButtonHeight: 40px;
|
||||||
|
@FabricCommandBarButtonHeight: 34px;
|
||||||
|
|
||||||
/**********************************************************************************
|
/**********************************************************************************
|
||||||
Portal Consts
|
Portal Consts
|
||||||
@@ -164,7 +165,7 @@
|
|||||||
@FabricFont: "Segoe UI", "Segoe UI Web (West European)", "Segoe UI", -apple-system, BlinkMacSystemFont, Roboto, "Helvetica Neue", sans-serif;
|
@FabricFont: "Segoe UI", "Segoe UI Web (West European)", "Segoe UI", -apple-system, BlinkMacSystemFont, Roboto, "Helvetica Neue", sans-serif;
|
||||||
|
|
||||||
@FabricBoxBorderRadius: 8px;
|
@FabricBoxBorderRadius: 8px;
|
||||||
@FabricBoxBorderShadow: 0 0 2px rgba(0, 0, 0, 0.12), 0 2px 4px rgba(0, 0, 0, 0.14);
|
@FabricBoxBorderShadow: rgba(0, 0, 0, 0.133) 0px 1.6px 3.6px 0px, rgba(0, 0, 0, 0.11) 0px 0.3px 0.9px 0px;
|
||||||
@FabricBoxMargin: 4px 3px 4px 3px;
|
@FabricBoxMargin: 4px 3px 4px 3px;
|
||||||
|
|
||||||
@FabricAccentMediumHigh: #0c695a;
|
@FabricAccentMediumHigh: #0c695a;
|
||||||
|
|||||||
@@ -47,11 +47,15 @@ a:focus {
|
|||||||
border-radius: @FabricBoxBorderRadius;
|
border-radius: @FabricBoxBorderRadius;
|
||||||
box-shadow: @FabricBoxBorderShadow;
|
box-shadow: @FabricBoxBorderShadow;
|
||||||
margin: @FabricBoxMargin;
|
margin: @FabricBoxMargin;
|
||||||
|
margin-top: 0px;
|
||||||
padding-top: 2px;
|
padding-top: 2px;
|
||||||
|
padding: 0px;
|
||||||
|
height: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dividerContainer {
|
.dividerContainer {
|
||||||
padding: @SmallSpace 0px @SmallSpace 0px;
|
padding: @SmallSpace 0px @SmallSpace 0px;
|
||||||
|
height: @FabricCommandBarButtonHeight;
|
||||||
.flex-display();
|
.flex-display();
|
||||||
|
|
||||||
span {
|
span {
|
||||||
|
|||||||
@@ -40,9 +40,10 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
|
|||||||
case Cosmos.ResourceType.item:
|
case Cosmos.ResourceType.item:
|
||||||
case Cosmos.ResourceType.pkranges:
|
case Cosmos.ResourceType.pkranges:
|
||||||
// User resource tokens
|
// User resource tokens
|
||||||
|
// TODO userContext.fabricContext.databaseConnectionInfo can be undefined
|
||||||
headers[HttpHeaders.msDate] = new Date().toUTCString();
|
headers[HttpHeaders.msDate] = new Date().toUTCString();
|
||||||
const resourceTokens = userContext.fabricDatabaseConnectionInfo.resourceTokens;
|
const resourceTokens = userContext.fabricContext.databaseConnectionInfo.resourceTokens;
|
||||||
checkDatabaseResourceTokensValidity(userContext.fabricDatabaseConnectionInfo.resourceTokensTimestamp);
|
checkDatabaseResourceTokensValidity(userContext.fabricContext.databaseConnectionInfo.resourceTokensTimestamp);
|
||||||
return getAuthorizationTokenUsingResourceTokens(resourceTokens, requestInfo.path, requestInfo.resourceId);
|
return getAuthorizationTokenUsingResourceTokens(resourceTokens, requestInfo.path, requestInfo.resourceId);
|
||||||
|
|
||||||
case Cosmos.ResourceType.none:
|
case Cosmos.ResourceType.none:
|
||||||
@@ -51,9 +52,11 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
|
|||||||
case Cosmos.ResourceType.user:
|
case Cosmos.ResourceType.user:
|
||||||
case Cosmos.ResourceType.permission:
|
case Cosmos.ResourceType.permission:
|
||||||
// User master tokens
|
// User master tokens
|
||||||
const authorizationToken = await sendCachedDataMessage<AuthorizationToken>(MessageTypes.GetAuthorizationToken, [
|
const authorizationToken = await sendCachedDataMessage<AuthorizationToken>(
|
||||||
requestInfo,
|
MessageTypes.GetAuthorizationToken,
|
||||||
]);
|
[requestInfo],
|
||||||
|
userContext.fabricContext.connectionId,
|
||||||
|
);
|
||||||
console.log("Response from Fabric: ", authorizationToken);
|
console.log("Response from Fabric: ", authorizationToken);
|
||||||
headers[HttpHeaders.msDate] = authorizationToken.XDate;
|
headers[HttpHeaders.msDate] = authorizationToken.XDate;
|
||||||
return decodeURIComponent(authorizationToken.PrimaryReadWriteToken);
|
return decodeURIComponent(authorizationToken.PrimaryReadWriteToken);
|
||||||
|
|||||||
@@ -27,15 +27,24 @@ export function handleCachedDataMessage(message: any): void {
|
|||||||
runGarbageCollector();
|
runGarbageCollector();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param messageType
|
||||||
|
* @param params
|
||||||
|
* @param scope Use this string to identify request Useful to distinguish response from different senders
|
||||||
|
* @param timeoutInMs
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
export function sendCachedDataMessage<TResponseDataModel>(
|
export function sendCachedDataMessage<TResponseDataModel>(
|
||||||
messageType: MessageTypes,
|
messageType: MessageTypes,
|
||||||
params: Object[],
|
params: Object[],
|
||||||
|
scope?: string,
|
||||||
timeoutInMs?: number,
|
timeoutInMs?: number,
|
||||||
): Q.Promise<TResponseDataModel> {
|
): Q.Promise<TResponseDataModel> {
|
||||||
let cachedDataPromise: CachedDataPromise<TResponseDataModel> = {
|
let cachedDataPromise: CachedDataPromise<TResponseDataModel> = {
|
||||||
deferred: Q.defer<TResponseDataModel>(),
|
deferred: Q.defer<TResponseDataModel>(),
|
||||||
startTime: new Date(),
|
startTime: new Date(),
|
||||||
id: _.uniqueId(),
|
id: _.uniqueId(scope),
|
||||||
};
|
};
|
||||||
RequestMap[cachedDataPromise.id] = cachedDataPromise;
|
RequestMap[cachedDataPromise.id] = cachedDataPromise;
|
||||||
sendMessage({ type: messageType, params: params, id: cachedDataPromise.id });
|
sendMessage({ type: messageType, params: params, id: cachedDataPromise.id });
|
||||||
@@ -47,6 +56,10 @@ export function sendCachedDataMessage<TResponseDataModel>(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param data Overwrite the data property of the message
|
||||||
|
*/
|
||||||
export function sendMessage(data: any): void {
|
export function sendMessage(data: any): void {
|
||||||
_sendMessage({
|
_sendMessage({
|
||||||
signature: "pcIframe",
|
signature: "pcIframe",
|
||||||
|
|||||||
@@ -18,13 +18,13 @@ export async function readCollections(databaseId: string): Promise<DataModels.Co
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
configContext.platform === Platform.Fabric &&
|
configContext.platform === Platform.Fabric &&
|
||||||
userContext.fabricDatabaseConnectionInfo &&
|
userContext.fabricContext &&
|
||||||
userContext.fabricDatabaseConnectionInfo.databaseId === databaseId
|
userContext.fabricContext.databaseConnectionInfo.databaseId === databaseId
|
||||||
) {
|
) {
|
||||||
const collections: DataModels.Collection[] = [];
|
const collections: DataModels.Collection[] = [];
|
||||||
const promises: Promise<ContainerResponse>[] = [];
|
const promises: Promise<ContainerResponse>[] = [];
|
||||||
|
|
||||||
for (const collectionResourceId in userContext.fabricDatabaseConnectionInfo.resourceTokens) {
|
for (const collectionResourceId in userContext.fabricContext.databaseConnectionInfo.resourceTokens) {
|
||||||
// Dictionary key looks like this: dbs/SampleDB/colls/Container
|
// Dictionary key looks like this: dbs/SampleDB/colls/Container
|
||||||
const resourceIdObj = collectionResourceId.split("/");
|
const resourceIdObj = collectionResourceId.split("/");
|
||||||
const tokenDatabaseId = resourceIdObj[1];
|
const tokenDatabaseId = resourceIdObj[1];
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ export async function readDatabases(): Promise<DataModels.Database[]> {
|
|||||||
let databases: DataModels.Database[];
|
let databases: DataModels.Database[];
|
||||||
const clearMessage = logConsoleProgress(`Querying databases`);
|
const clearMessage = logConsoleProgress(`Querying databases`);
|
||||||
|
|
||||||
if (configContext.platform === Platform.Fabric && userContext.fabricDatabaseConnectionInfo?.resourceTokens) {
|
if (configContext.platform === Platform.Fabric && userContext.fabricContext?.databaseConnectionInfo.resourceTokens) {
|
||||||
const tokensData = userContext.fabricDatabaseConnectionInfo;
|
const tokensData = userContext.fabricContext.databaseConnectionInfo;
|
||||||
|
|
||||||
const databaseIdsSet = new Set<string>(); // databaseId
|
const databaseIdsSet = new Set<string>(); // databaseId
|
||||||
|
|
||||||
|
|||||||
42
src/Contracts/DataExplorerMessagesContract.ts
Normal file
42
src/Contracts/DataExplorerMessagesContract.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { MessageTypes } from "./MessageTypes";
|
||||||
|
|
||||||
|
// This is the current version of these messages
|
||||||
|
export const DATA_EXPLORER_RPC_VERSION = "2";
|
||||||
|
|
||||||
|
// Data Explorer to Fabric
|
||||||
|
|
||||||
|
// TODO Remove when upgrading to Fabric v2
|
||||||
|
export type DataExploreMessageV1 =
|
||||||
|
| "ready"
|
||||||
|
| {
|
||||||
|
type: MessageTypes.GetAuthorizationToken;
|
||||||
|
id: string;
|
||||||
|
params: GetCosmosTokenMessageOptions[];
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: MessageTypes.GetAllResourceTokens;
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
// -----------------------------
|
||||||
|
|
||||||
|
export type DataExploreMessageV2 =
|
||||||
|
| {
|
||||||
|
type: MessageTypes.Ready;
|
||||||
|
id: string;
|
||||||
|
params: [string]; // version
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: MessageTypes.GetAuthorizationToken;
|
||||||
|
id: string;
|
||||||
|
params: GetCosmosTokenMessageOptions[];
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: MessageTypes.GetAllResourceTokens;
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GetCosmosTokenMessageOptions = {
|
||||||
|
verb: "connect" | "delete" | "get" | "head" | "options" | "patch" | "post" | "put" | "trace";
|
||||||
|
resourceType: "" | "dbs" | "colls" | "docs" | "sprocs" | "pkranges";
|
||||||
|
resourceId: string;
|
||||||
|
};
|
||||||
@@ -1,6 +1,12 @@
|
|||||||
import { AuthorizationToken, MessageTypes } from "./MessageTypes";
|
import { AuthorizationToken } from "./MessageTypes";
|
||||||
|
|
||||||
export type FabricMessage =
|
// This is the version of these messages
|
||||||
|
export const FABRIC_RPC_VERSION = "2";
|
||||||
|
|
||||||
|
// Fabric to Data Explorer
|
||||||
|
|
||||||
|
// TODO Deprecated. Remove this section once DE is updated
|
||||||
|
export type FabricMessageV1 =
|
||||||
| {
|
| {
|
||||||
type: "newContainer";
|
type: "newContainer";
|
||||||
databaseName: string;
|
databaseName: string;
|
||||||
@@ -26,38 +32,52 @@ export type FabricMessage =
|
|||||||
| {
|
| {
|
||||||
type: "allResourceTokens";
|
type: "allResourceTokens";
|
||||||
message: {
|
message: {
|
||||||
|
id: string;
|
||||||
|
error: string | undefined;
|
||||||
endpoint: string | undefined;
|
endpoint: string | undefined;
|
||||||
databaseId: string | undefined;
|
databaseId: string | undefined;
|
||||||
resourceTokens: unknown | undefined;
|
resourceTokens: unknown | undefined;
|
||||||
resourceTokensTimestamp: number | undefined;
|
resourceTokensTimestamp: number | undefined;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
// -----------------------------
|
||||||
|
|
||||||
export type DataExploreMessage =
|
export type FabricMessageV2 =
|
||||||
| "ready"
|
|
||||||
| {
|
| {
|
||||||
type: MessageTypes.TelemetryInfo;
|
type: "newContainer";
|
||||||
data: {
|
databaseName: string;
|
||||||
action: "LoadDatabases";
|
}
|
||||||
actionModifier: "success" | "start";
|
| {
|
||||||
defaultExperience: "SQL";
|
type: "initialize";
|
||||||
|
version: string;
|
||||||
|
id: string;
|
||||||
|
message: {
|
||||||
|
connectionId: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: MessageTypes.GetAuthorizationToken;
|
type: "authorizationToken";
|
||||||
id: string;
|
message: {
|
||||||
params: GetCosmosTokenMessageOptions[];
|
id: string;
|
||||||
|
error: string | undefined;
|
||||||
|
data: AuthorizationToken | undefined;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: MessageTypes.GetAllResourceTokens;
|
type: "allResourceTokens_v2";
|
||||||
|
message: {
|
||||||
|
id: string;
|
||||||
|
error: string | undefined;
|
||||||
|
data: FabricDatabaseConnectionInfo | undefined;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "setToolbarStatus";
|
||||||
|
message: {
|
||||||
|
visible: boolean;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GetCosmosTokenMessageOptions = {
|
|
||||||
verb: "connect" | "delete" | "get" | "head" | "options" | "patch" | "post" | "put" | "trace";
|
|
||||||
resourceType: "" | "dbs" | "colls" | "docs" | "sprocs" | "pkranges";
|
|
||||||
resourceId: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type CosmosDBTokenResponse = {
|
export type CosmosDBTokenResponse = {
|
||||||
token: string;
|
token: string;
|
||||||
date: string;
|
date: string;
|
||||||
@@ -66,12 +86,9 @@ export type CosmosDBTokenResponse = {
|
|||||||
export type CosmosDBConnectionInfoResponse = {
|
export type CosmosDBConnectionInfoResponse = {
|
||||||
endpoint: string;
|
endpoint: string;
|
||||||
databaseId: string;
|
databaseId: string;
|
||||||
resourceTokens: unknown;
|
resourceTokens: { [resourceId: string]: string };
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface FabricDatabaseConnectionInfo {
|
export interface FabricDatabaseConnectionInfo extends CosmosDBConnectionInfoResponse {
|
||||||
endpoint: string;
|
|
||||||
databaseId: string;
|
|
||||||
resourceTokens: { [resourceId: string]: string };
|
|
||||||
resourceTokensTimestamp: number;
|
resourceTokensTimestamp: number;
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,12 @@
|
|||||||
/**
|
/**
|
||||||
* Messaging types used with Data Explorer <-> Portal communication,
|
* Messaging types used with Data Explorer <-> Portal communication,
|
||||||
* Hosted <-> Explorer communication and Data Explorer -> Fabric communication.
|
* Hosted <-> Explorer communication and Data Explorer -> Fabric communication.
|
||||||
|
*
|
||||||
|
* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||||
|
* WARNING: !!!!!!! YOU CAN ONLY ADD NEW TYPES TO THE END OF THIS ENUM !!!!!!!
|
||||||
|
* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||||
|
*
|
||||||
|
* Enum are integers, so inserting or deleting a type will break the communication.
|
||||||
*/
|
*/
|
||||||
export enum MessageTypes {
|
export enum MessageTypes {
|
||||||
TelemetryInfo,
|
TelemetryInfo,
|
||||||
@@ -37,10 +43,9 @@ export enum MessageTypes {
|
|||||||
DisplayNPSSurvey,
|
DisplayNPSSurvey,
|
||||||
OpenVCoreMongoNetworkingBlade,
|
OpenVCoreMongoNetworkingBlade,
|
||||||
OpenVCoreMongoConnectionStringsBlade,
|
OpenVCoreMongoConnectionStringsBlade,
|
||||||
|
GetAuthorizationToken, // Data Explorer -> Fabric
|
||||||
// Data Explorer -> Fabric communication
|
GetAllResourceTokens, // Data Explorer -> Fabric
|
||||||
GetAuthorizationToken,
|
Ready, // Data Explorer -> Fabric
|
||||||
GetAllResourceTokens,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AuthorizationToken {
|
export interface AuthorizationToken {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Platform, configContext } from "ConfigContext";
|
|||||||
import { MessageTypes } from "Contracts/ExplorerContracts";
|
import { MessageTypes } from "Contracts/ExplorerContracts";
|
||||||
import { getCopilotEnabled, isCopilotFeatureRegistered } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
import { getCopilotEnabled, isCopilotFeatureRegistered } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
||||||
import { IGalleryItem } from "Juno/JunoClient";
|
import { IGalleryItem } from "Juno/JunoClient";
|
||||||
import { requestDatabaseResourceTokens } from "Platform/Fabric/FabricUtil";
|
import { scheduleRefreshDatabaseResourceToken } from "Platform/Fabric/FabricUtil";
|
||||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||||
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointValidation";
|
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointValidation";
|
||||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
@@ -277,21 +277,32 @@ export default class Explorer {
|
|||||||
const NINETY_DAYS_IN_MS = 7776000000;
|
const NINETY_DAYS_IN_MS = 7776000000;
|
||||||
const ONE_DAY_IN_MS = 86400000;
|
const ONE_DAY_IN_MS = 86400000;
|
||||||
const THREE_DAYS_IN_MS = 259200000;
|
const THREE_DAYS_IN_MS = 259200000;
|
||||||
const isAccountNewerThanNinetyDays = isAccountNewerThanThresholdInMs(
|
|
||||||
userContext.databaseAccount?.systemData?.createdAt || "",
|
|
||||||
NINETY_DAYS_IN_MS,
|
|
||||||
);
|
|
||||||
const lastSubmitted: string = localStorage.getItem("lastSubmitted");
|
const lastSubmitted: string = localStorage.getItem("lastSubmitted");
|
||||||
|
Logger.logInfo(`NPS Survey last shown date: ${lastSubmitted}`, "Explorer/openNPSSurveyDialog");
|
||||||
|
|
||||||
if (lastSubmitted !== null) {
|
if (lastSubmitted !== null) {
|
||||||
|
Logger.logInfo(`NPS Survey last shown is not empty ${lastSubmitted}`, "Explorer/openNPSSurveyDialog");
|
||||||
|
|
||||||
let lastSubmittedDate: number = parseInt(lastSubmitted);
|
let lastSubmittedDate: number = parseInt(lastSubmitted);
|
||||||
|
Logger.logInfo(`NPS Survey last shown is parsed ${lastSubmittedDate.toString()}`, "Explorer/openNPSSurveyDialog");
|
||||||
|
|
||||||
if (isNaN(lastSubmittedDate)) {
|
if (isNaN(lastSubmittedDate)) {
|
||||||
|
Logger.logInfo(
|
||||||
|
`NPS Survey last shown is not a number ${lastSubmittedDate.toString()}`,
|
||||||
|
"Explorer/openNPSSurveyDialog",
|
||||||
|
);
|
||||||
lastSubmittedDate = 0;
|
lastSubmittedDate = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const nowMs: number = Date.now();
|
const nowMs: number = Date.now();
|
||||||
|
Logger.logInfo(`NPS Survey current date ${nowMs.toString()}`, "Explorer/openNPSSurveyDialog");
|
||||||
|
|
||||||
const millisecsSinceLastSubmitted = nowMs - lastSubmittedDate;
|
const millisecsSinceLastSubmitted = nowMs - lastSubmittedDate;
|
||||||
if (millisecsSinceLastSubmitted < NINETY_DAYS_IN_MS) {
|
if (millisecsSinceLastSubmitted < NINETY_DAYS_IN_MS) {
|
||||||
|
Logger.logInfo(
|
||||||
|
`NPS Survey last shown is less than ninety days ${millisecsSinceLastSubmitted.toString()}`,
|
||||||
|
"Explorer/openNPSSurveyDialog",
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -299,26 +310,32 @@ export default class Explorer {
|
|||||||
// Try Cosmos DB subscription - survey shown to 100% of users at day 1 in Data Explorer.
|
// Try Cosmos DB subscription - survey shown to 100% of users at day 1 in Data Explorer.
|
||||||
if (userContext.isTryCosmosDBSubscription) {
|
if (userContext.isTryCosmosDBSubscription) {
|
||||||
if (isAccountNewerThanThresholdInMs(userContext.databaseAccount?.systemData?.createdAt || "", ONE_DAY_IN_MS)) {
|
if (isAccountNewerThanThresholdInMs(userContext.databaseAccount?.systemData?.createdAt || "", ONE_DAY_IN_MS)) {
|
||||||
|
Logger.logInfo(
|
||||||
|
`Displaying NPS Survey for Try Cosmos DB ${userContext.apiType}`,
|
||||||
|
"Explorer/openNPSSurveyDialog",
|
||||||
|
);
|
||||||
this.sendNPSMessage();
|
this.sendNPSMessage();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// An existing account is older than 3 days but less than 90 days old. For existing account show to 100% of users in Data Explorer.
|
// Show survey when an existing account is older than 3 days
|
||||||
if (
|
if (
|
||||||
!isAccountNewerThanThresholdInMs(userContext.databaseAccount?.systemData?.createdAt || "", THREE_DAYS_IN_MS) &&
|
!isAccountNewerThanThresholdInMs(userContext.databaseAccount?.systemData?.createdAt || "", THREE_DAYS_IN_MS)
|
||||||
isAccountNewerThanNinetyDays
|
|
||||||
) {
|
) {
|
||||||
|
Logger.logInfo(
|
||||||
|
`Displaying NPS Survey for users with existing ${userContext.apiType} account older than 3 days`,
|
||||||
|
"Explorer/openNPSSurveyDialog",
|
||||||
|
);
|
||||||
this.sendNPSMessage();
|
this.sendNPSMessage();
|
||||||
} else {
|
|
||||||
// An existing account is greater than 90 days. For existing account show to random 33% of users in Data Explorer.
|
|
||||||
if (this.getRandomInt(100) < 33) {
|
|
||||||
this.sendNPSMessage();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private sendNPSMessage() {
|
private sendNPSMessage() {
|
||||||
sendMessage({ type: MessageTypes.DisplayNPSSurvey });
|
sendMessage({ type: MessageTypes.DisplayNPSSurvey });
|
||||||
|
Logger.logInfo(
|
||||||
|
`NPS Survey logging current date when survey is shown ${Date.now().toString()}`,
|
||||||
|
"Explorer/openNPSSurveyDialog",
|
||||||
|
);
|
||||||
localStorage.setItem("lastSubmitted", Date.now().toString());
|
localStorage.setItem("lastSubmitted", Date.now().toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -384,9 +401,7 @@ export default class Explorer {
|
|||||||
|
|
||||||
public onRefreshResourcesClick = (): void => {
|
public onRefreshResourcesClick = (): void => {
|
||||||
if (configContext.platform === Platform.Fabric) {
|
if (configContext.platform === Platform.Fabric) {
|
||||||
// Requesting the tokens will trigger a refresh of the databases
|
scheduleRefreshDatabaseResourceToken(true).then(() => this.refreshAllDatabases());
|
||||||
// TODO: Once the id is returned from Fabric, we can await this call and then refresh the databases here
|
|
||||||
requestDatabaseResourceTokens();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,16 +24,21 @@ interface Props {
|
|||||||
export interface CommandBarStore {
|
export interface CommandBarStore {
|
||||||
contextButtons: CommandButtonComponentProps[];
|
contextButtons: CommandButtonComponentProps[];
|
||||||
setContextButtons: (contextButtons: CommandButtonComponentProps[]) => void;
|
setContextButtons: (contextButtons: CommandButtonComponentProps[]) => void;
|
||||||
|
isHidden: boolean;
|
||||||
|
setIsHidden: (isHidden: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useCommandBar: UseStore<CommandBarStore> = create((set) => ({
|
export const useCommandBar: UseStore<CommandBarStore> = create((set) => ({
|
||||||
contextButtons: [],
|
contextButtons: [],
|
||||||
setContextButtons: (contextButtons: CommandButtonComponentProps[]) => set((state) => ({ ...state, contextButtons })),
|
setContextButtons: (contextButtons: CommandButtonComponentProps[]) => set((state) => ({ ...state, contextButtons })),
|
||||||
|
isHidden: false,
|
||||||
|
setIsHidden: (isHidden: boolean) => set((state) => ({ ...state, isHidden })),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
||||||
const selectedNodeState = useSelectedNode();
|
const selectedNodeState = useSelectedNode();
|
||||||
const buttons = useCommandBar((state) => state.contextButtons);
|
const buttons = useCommandBar((state) => state.contextButtons);
|
||||||
|
const isHidden = useCommandBar((state) => state.isHidden);
|
||||||
const backgroundColor = StyleConstants.BaseLight;
|
const backgroundColor = StyleConstants.BaseLight;
|
||||||
|
|
||||||
if (userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo") {
|
if (userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo") {
|
||||||
@@ -42,7 +47,7 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
|||||||
? CommandBarComponentButtonFactory.createPostgreButtons(container)
|
? CommandBarComponentButtonFactory.createPostgreButtons(container)
|
||||||
: CommandBarComponentButtonFactory.createVCoreMongoButtons(container);
|
: CommandBarComponentButtonFactory.createVCoreMongoButtons(container);
|
||||||
return (
|
return (
|
||||||
<div className="commandBarContainer">
|
<div className="commandBarContainer" style={{ display: isHidden ? "none" : "initial" }}>
|
||||||
<FluentCommandBar
|
<FluentCommandBar
|
||||||
ariaLabel="Use left and right arrow keys to navigate between commands"
|
ariaLabel="Use left and right arrow keys to navigate between commands"
|
||||||
items={CommandBarUtil.convertButton(buttons, backgroundColor)}
|
items={CommandBarUtil.convertButton(buttons, backgroundColor)}
|
||||||
@@ -91,7 +96,7 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
|||||||
? {
|
? {
|
||||||
root: {
|
root: {
|
||||||
backgroundColor: "transparent",
|
backgroundColor: "transparent",
|
||||||
padding: "0px 14px 0px 14px",
|
padding: "2px 8px 0px 8px",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
@@ -101,7 +106,7 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="commandBarContainer">
|
<div className="commandBarContainer" style={{ display: isHidden ? "none" : "initial" }}>
|
||||||
<FluentCommandBar
|
<FluentCommandBar
|
||||||
ariaLabel="Use left and right arrow keys to navigate between commands"
|
ariaLabel="Use left and right arrow keys to navigate between commands"
|
||||||
items={uiFabricStaticButtons.concat(uiFabricTabsButtons)}
|
items={uiFabricStaticButtons.concat(uiFabricTabsButtons)}
|
||||||
|
|||||||
@@ -25,7 +25,10 @@ import { MemoryTracker } from "./MemoryTrackerComponent";
|
|||||||
* @param btns
|
* @param btns
|
||||||
*/
|
*/
|
||||||
export const convertButton = (btns: CommandButtonComponentProps[], backgroundColor: string): ICommandBarItemProps[] => {
|
export const convertButton = (btns: CommandButtonComponentProps[], backgroundColor: string): ICommandBarItemProps[] => {
|
||||||
const buttonHeightPx = StyleConstants.CommandBarButtonHeight;
|
const buttonHeightPx =
|
||||||
|
configContext.platform == Platform.Fabric
|
||||||
|
? StyleConstants.FabricCommandBarButtonHeight
|
||||||
|
: StyleConstants.CommandBarButtonHeight;
|
||||||
|
|
||||||
const hoverColor =
|
const hoverColor =
|
||||||
configContext.platform == Platform.Fabric ? StyleConstants.FabricAccentLight : StyleConstants.AccentLight;
|
configContext.platform == Platform.Fabric ? StyleConstants.FabricAccentLight : StyleConstants.AccentLight;
|
||||||
@@ -112,6 +115,7 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol
|
|||||||
splitButtonContainer: {
|
splitButtonContainer: {
|
||||||
marginLeft: 5,
|
marginLeft: 5,
|
||||||
marginRight: 5,
|
marginRight: 5,
|
||||||
|
height: buttonHeightPx,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
className: btn.className,
|
className: btn.className,
|
||||||
|
|||||||
@@ -234,7 +234,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
const handleSampleDatabaseChange = async (ev: React.MouseEvent<HTMLElement>, checked?: boolean): Promise<void> => {
|
const handleSampleDatabaseChange = async (ev: React.MouseEvent<HTMLElement>, checked?: boolean): Promise<void> => {
|
||||||
setCopilotSampleDBEnabled(checked);
|
setCopilotSampleDBEnabled(checked);
|
||||||
useQueryCopilot.getState().setCopilotSampleDBEnabled(checked);
|
useQueryCopilot.getState().setCopilotSampleDBEnabled(checked);
|
||||||
setRefreshExplorer(false);
|
setRefreshExplorer(!refreshExplorer);
|
||||||
};
|
};
|
||||||
|
|
||||||
const choiceButtonStyles = {
|
const choiceButtonStyles = {
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ const CopilotProvider = ({ children }: { children: React.ReactNode }): JSX.Eleme
|
|||||||
queryResults: undefined,
|
queryResults: undefined,
|
||||||
errorMessage: "",
|
errorMessage: "",
|
||||||
isSamplePromptsOpen: false,
|
isSamplePromptsOpen: false,
|
||||||
showPromptTeachingBubble: true,
|
|
||||||
showDeletePopup: false,
|
showDeletePopup: false,
|
||||||
showFeedbackBar: false,
|
showFeedbackBar: false,
|
||||||
showCopyPopup: false,
|
showCopyPopup: false,
|
||||||
@@ -66,7 +65,6 @@ const CopilotProvider = ({ children }: { children: React.ReactNode }): JSX.Eleme
|
|||||||
setQueryResults: (queryResults: QueryResults | undefined) => set({ queryResults }),
|
setQueryResults: (queryResults: QueryResults | undefined) => set({ queryResults }),
|
||||||
setErrorMessage: (errorMessage: string) => set({ errorMessage }),
|
setErrorMessage: (errorMessage: string) => set({ errorMessage }),
|
||||||
setIsSamplePromptsOpen: (isSamplePromptsOpen: boolean) => set({ isSamplePromptsOpen }),
|
setIsSamplePromptsOpen: (isSamplePromptsOpen: boolean) => set({ isSamplePromptsOpen }),
|
||||||
setShowPromptTeachingBubble: (showPromptTeachingBubble: boolean) => set({ showPromptTeachingBubble }),
|
|
||||||
setShowDeletePopup: (showDeletePopup: boolean) => set({ showDeletePopup }),
|
setShowDeletePopup: (showDeletePopup: boolean) => set({ showDeletePopup }),
|
||||||
setShowFeedbackBar: (showFeedbackBar: boolean) => set({ showFeedbackBar }),
|
setShowFeedbackBar: (showFeedbackBar: boolean) => set({ showFeedbackBar }),
|
||||||
setshowCopyPopup: (showCopyPopup: boolean) => set({ showCopyPopup }),
|
setshowCopyPopup: (showCopyPopup: boolean) => set({ showCopyPopup }),
|
||||||
@@ -105,7 +103,6 @@ const CopilotProvider = ({ children }: { children: React.ReactNode }): JSX.Eleme
|
|||||||
queryResults: undefined,
|
queryResults: undefined,
|
||||||
errorMessage: "",
|
errorMessage: "",
|
||||||
isSamplePromptsOpen: false,
|
isSamplePromptsOpen: false,
|
||||||
showPromptTeachingBubble: true,
|
|
||||||
showDeletePopup: false,
|
showDeletePopup: false,
|
||||||
showFeedbackBar: false,
|
showFeedbackBar: false,
|
||||||
showCopyPopup: false,
|
showCopyPopup: false,
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
TextField,
|
TextField,
|
||||||
} from "@fluentui/react";
|
} from "@fluentui/react";
|
||||||
|
import { useBoolean } from "@fluentui/react-hooks";
|
||||||
import { HttpStatusCodes } from "Common/Constants";
|
import { HttpStatusCodes } from "Common/Constants";
|
||||||
import { handleError } from "Common/ErrorHandlingUtils";
|
import { handleError } from "Common/ErrorHandlingUtils";
|
||||||
import { createUri } from "Common/UrlUtility";
|
import { createUri } from "Common/UrlUtility";
|
||||||
@@ -70,7 +71,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
|||||||
databaseId,
|
databaseId,
|
||||||
containerId,
|
containerId,
|
||||||
}: QueryCopilotPromptProps): JSX.Element => {
|
}: QueryCopilotPromptProps): JSX.Element => {
|
||||||
const [copilotTeachingBubbleVisible, setCopilotTeachingBubbleVisible] = useState<boolean>(false);
|
const [copilotTeachingBubbleVisible, { toggle: toggleCopilotTeachingBubbleVisible }] = useBoolean(false);
|
||||||
const inputEdited = useRef(false);
|
const inputEdited = useRef(false);
|
||||||
const {
|
const {
|
||||||
openFeedbackModal,
|
openFeedbackModal,
|
||||||
@@ -93,8 +94,6 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
|||||||
setIsSamplePromptsOpen,
|
setIsSamplePromptsOpen,
|
||||||
showSamplePrompts,
|
showSamplePrompts,
|
||||||
setShowSamplePrompts,
|
setShowSamplePrompts,
|
||||||
showPromptTeachingBubble,
|
|
||||||
setShowPromptTeachingBubble,
|
|
||||||
showDeletePopup,
|
showDeletePopup,
|
||||||
setShowDeletePopup,
|
setShowDeletePopup,
|
||||||
showFeedbackBar,
|
showFeedbackBar,
|
||||||
@@ -273,23 +272,16 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const showTeachingBubble = (): void => {
|
const showTeachingBubble = (): void => {
|
||||||
if (showPromptTeachingBubble && !inputEdited.current) {
|
if (!inputEdited.current) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!inputEdited.current && !isWelcomModalVisible()) {
|
if (!inputEdited.current && !isWelcomModalVisible()) {
|
||||||
setCopilotTeachingBubbleVisible(true);
|
toggleCopilotTeachingBubbleVisible();
|
||||||
inputEdited.current = true;
|
inputEdited.current = true;
|
||||||
}
|
}
|
||||||
}, 30000);
|
}, 30000);
|
||||||
} else {
|
|
||||||
toggleCopilotTeachingBubbleVisible(false);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleCopilotTeachingBubbleVisible = (visible: boolean): void => {
|
|
||||||
setCopilotTeachingBubbleVisible(visible);
|
|
||||||
setShowPromptTeachingBubble(visible);
|
|
||||||
};
|
|
||||||
|
|
||||||
const isWelcomModalVisible = (): boolean => {
|
const isWelcomModalVisible = (): boolean => {
|
||||||
return localStorage.getItem("hideWelcomeModal") !== "true";
|
return localStorage.getItem("hideWelcomeModal") !== "true";
|
||||||
};
|
};
|
||||||
@@ -372,13 +364,13 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
|||||||
placeholder="Ask a question in natural language and we’ll generate the query for you."
|
placeholder="Ask a question in natural language and we’ll generate the query for you."
|
||||||
aria-labelledby="copilot-textfield-label"
|
aria-labelledby="copilot-textfield-label"
|
||||||
/>
|
/>
|
||||||
{showPromptTeachingBubble && copilotTeachingBubbleVisible && (
|
{copilotTeachingBubbleVisible && (
|
||||||
<TeachingBubble
|
<TeachingBubble
|
||||||
calloutProps={{ directionalHint: DirectionalHint.bottomCenter }}
|
calloutProps={{ directionalHint: DirectionalHint.bottomCenter }}
|
||||||
target="#naturalLanguageInput"
|
target="#naturalLanguageInput"
|
||||||
hasCloseButton={true}
|
hasCloseButton={true}
|
||||||
closeButtonAriaLabel="Close"
|
closeButtonAriaLabel="Close"
|
||||||
onDismiss={() => toggleCopilotTeachingBubbleVisible(false)}
|
onDismiss={toggleCopilotTeachingBubbleVisible}
|
||||||
hasSmallHeadline={true}
|
hasSmallHeadline={true}
|
||||||
headline="Write a prompt"
|
headline="Write a prompt"
|
||||||
>
|
>
|
||||||
@@ -386,7 +378,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
|||||||
<Link
|
<Link
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowSamplePrompts(true);
|
setShowSamplePrompts(true);
|
||||||
toggleCopilotTeachingBubbleVisible(false);
|
toggleCopilotTeachingBubbleVisible();
|
||||||
}}
|
}}
|
||||||
style={{ color: "white", fontWeight: 600 }}
|
style={{ color: "white", fontWeight: 600 }}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack horizontal tokens={{ childrenGap: 16 }}>
|
<Stack horizontal tokens={{ childrenGap: 16 }}>
|
||||||
{useQueryCopilot.getState().copilotEnabled && (
|
{useQueryCopilot.getState().copilotEnabled && useQueryCopilot.getState().copilotSampleDBEnabled && (
|
||||||
<SplashScreenButton
|
<SplashScreenButton
|
||||||
imgSrc={CopilotIcon}
|
imgSrc={CopilotIcon}
|
||||||
title={"Query faster with Copilot"}
|
title={"Query faster with Copilot"}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import React from "react";
|
|||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
|
|
||||||
export const PostgresConnectTab: React.FC = (): JSX.Element => {
|
export const PostgresConnectTab: React.FC = (): JSX.Element => {
|
||||||
const { adminLogin, databaseName, nodes, enablePublicIpAccess } = userContext.postgresConnectionStrParams;
|
const { adminLogin, nodes, enablePublicIpAccess } = userContext.postgresConnectionStrParams;
|
||||||
const [usePgBouncerPort, setUsePgBouncerPort] = React.useState<boolean>(false);
|
const [usePgBouncerPort, setUsePgBouncerPort] = React.useState<boolean>(false);
|
||||||
const [selectedNode, setSelectedNode] = React.useState<string>(nodes?.[0]?.value);
|
const [selectedNode, setSelectedNode] = React.useState<string>(nodes?.[0]?.value);
|
||||||
const portNumber = usePgBouncerPort ? "6432" : "5432";
|
const portNumber = usePgBouncerPort ? "6432" : "5432";
|
||||||
@@ -40,11 +40,11 @@ export const PostgresConnectTab: React.FC = (): JSX.Element => {
|
|||||||
text: node.text,
|
text: node.text,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const postgresSQLConnectionURL = `postgres://${adminLogin}:{your_password}@${selectedNode}:${portNumber}/${databaseName}?sslmode=require`;
|
const postgresSQLConnectionURL = `postgres://${adminLogin}:{your_password}@${selectedNode}:${portNumber}/citus?sslmode=require`;
|
||||||
const psql = `psql "host=${selectedNode} port=${portNumber} dbname=${databaseName} user=${adminLogin} password={your_password} sslmode=require"`;
|
const psql = `psql "host=${selectedNode} port=${portNumber} dbname=citus user=${adminLogin} password={your_password} sslmode=require"`;
|
||||||
const jdbc = `jdbc:postgresql://${selectedNode}:${portNumber}/${databaseName}?user=${adminLogin}&password={your_password}&sslmode=require`;
|
const jdbc = `jdbc:postgresql://${selectedNode}:${portNumber}/citus?user=${adminLogin}&password={your_password}&sslmode=require`;
|
||||||
const libpq = `host=${selectedNode} port=${portNumber} dbname=${databaseName} user=${adminLogin} password={your_password} sslmode=require`;
|
const libpq = `host=${selectedNode} port=${portNumber} dbname=citus user=${adminLogin} password={your_password} sslmode=require`;
|
||||||
const adoDotNet = `Server=${selectedNode};Database=${databaseName};Port=${portNumber};User Id=${adminLogin};Password={your_password};Ssl Mode=Require;`;
|
const adoDotNet = `Server=${selectedNode};Database=citus;Port=${portNumber};User Id=${adminLogin};Password={your_password};Ssl Mode=Require;`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ width: "100%", padding: 16 }}>
|
<div style={{ width: "100%", padding: 16 }}>
|
||||||
|
|||||||
@@ -1,33 +1,44 @@
|
|||||||
import { sendCachedDataMessage } from "Common/MessageHandler";
|
import { sendCachedDataMessage } from "Common/MessageHandler";
|
||||||
import { FabricDatabaseConnectionInfo } from "Contracts/FabricContract";
|
import { FabricDatabaseConnectionInfo } from "Contracts/FabricMessagesContract";
|
||||||
import { MessageTypes } from "Contracts/MessageTypes";
|
import { MessageTypes } from "Contracts/MessageTypes";
|
||||||
import Explorer from "Explorer/Explorer";
|
import { updateUserContext, userContext } from "UserContext";
|
||||||
import { updateUserContext } from "UserContext";
|
import { logConsoleError } from "Utils/NotificationConsoleUtils";
|
||||||
|
|
||||||
const TOKEN_VALIDITY_MS = (3600 - 600) * 1000; // 1 hour minus 10 minutes to be safe
|
const TOKEN_VALIDITY_MS = (3600 - 600) * 1000; // 1 hour minus 10 minutes to be safe
|
||||||
|
const DEBOUNCE_DELAY_MS = 1000 * 20; // 20 second
|
||||||
let timeoutId: NodeJS.Timeout;
|
let timeoutId: NodeJS.Timeout;
|
||||||
|
|
||||||
// Prevents multiple parallel requests
|
// Prevents multiple parallel requests during DEBOUNCE_DELAY_MS
|
||||||
let isRequestPending = false;
|
let lastRequestTimestamp: number = undefined;
|
||||||
|
|
||||||
export const requestDatabaseResourceTokens = (): void => {
|
const requestDatabaseResourceTokens = async (): Promise<void> => {
|
||||||
if (isRequestPending) {
|
if (lastRequestTimestamp !== undefined && lastRequestTimestamp + DEBOUNCE_DELAY_MS > Date.now()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO Make Fabric return the message id so we can handle this promise
|
lastRequestTimestamp = Date.now();
|
||||||
isRequestPending = true;
|
try {
|
||||||
sendCachedDataMessage<FabricDatabaseConnectionInfo>(MessageTypes.GetAllResourceTokens, []);
|
const fabricDatabaseConnectionInfo = await sendCachedDataMessage<FabricDatabaseConnectionInfo>(
|
||||||
};
|
MessageTypes.GetAllResourceTokens,
|
||||||
|
[],
|
||||||
|
userContext.fabricContext.connectionId,
|
||||||
|
);
|
||||||
|
|
||||||
export const handleRequestDatabaseResourceTokensResponse = (
|
if (!userContext.databaseAccount.properties.documentEndpoint) {
|
||||||
explorer: Explorer,
|
userContext.databaseAccount.properties.documentEndpoint = fabricDatabaseConnectionInfo.endpoint;
|
||||||
fabricDatabaseConnectionInfo: FabricDatabaseConnectionInfo,
|
}
|
||||||
): void => {
|
|
||||||
isRequestPending = false;
|
updateUserContext({
|
||||||
updateUserContext({ fabricDatabaseConnectionInfo });
|
fabricContext: { ...userContext.fabricContext, databaseConnectionInfo: fabricDatabaseConnectionInfo },
|
||||||
scheduleRefreshDatabaseResourceToken();
|
databaseAccount: { ...userContext.databaseAccount },
|
||||||
explorer.refreshAllDatabases();
|
});
|
||||||
|
scheduleRefreshDatabaseResourceToken();
|
||||||
|
} catch (error) {
|
||||||
|
logConsoleError(error);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
lastRequestTimestamp = undefined;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -35,19 +46,24 @@ export const handleRequestDatabaseResourceTokensResponse = (
|
|||||||
* @param tokenTimestamp
|
* @param tokenTimestamp
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const scheduleRefreshDatabaseResourceToken = (): void => {
|
export const scheduleRefreshDatabaseResourceToken = (refreshNow?: boolean): Promise<void> => {
|
||||||
if (timeoutId !== undefined) {
|
return new Promise((resolve) => {
|
||||||
clearTimeout(timeoutId);
|
if (timeoutId !== undefined) {
|
||||||
timeoutId = undefined;
|
clearTimeout(timeoutId);
|
||||||
}
|
timeoutId = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
timeoutId = setTimeout(() => {
|
timeoutId = setTimeout(
|
||||||
requestDatabaseResourceTokens();
|
() => {
|
||||||
}, TOKEN_VALIDITY_MS);
|
requestDatabaseResourceTokens().then(resolve);
|
||||||
|
},
|
||||||
|
refreshNow ? 0 : TOKEN_VALIDITY_MS,
|
||||||
|
);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const checkDatabaseResourceTokensValidity = (tokenTimestamp: number): void => {
|
export const checkDatabaseResourceTokensValidity = (tokenTimestamp: number): void => {
|
||||||
if (tokenTimestamp + TOKEN_VALIDITY_MS < Date.now()) {
|
if (tokenTimestamp + TOKEN_VALIDITY_MS < Date.now()) {
|
||||||
requestDatabaseResourceTokens();
|
scheduleRefreshDatabaseResourceToken(true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { FabricDatabaseConnectionInfo } from "Contracts/FabricContract";
|
import { FabricDatabaseConnectionInfo } from "Contracts/FabricMessagesContract";
|
||||||
import { ParsedResourceTokenConnectionString } from "Platform/Hosted/Helpers/ResourceTokenUtils";
|
import { ParsedResourceTokenConnectionString } from "Platform/Hosted/Helpers/ResourceTokenUtils";
|
||||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||||
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
|
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
|
||||||
@@ -36,7 +36,6 @@ export interface Node {
|
|||||||
|
|
||||||
export interface PostgresConnectionStrParams {
|
export interface PostgresConnectionStrParams {
|
||||||
adminLogin: string;
|
adminLogin: string;
|
||||||
databaseName: string;
|
|
||||||
enablePublicIpAccess: boolean;
|
enablePublicIpAccess: boolean;
|
||||||
nodes: Node[];
|
nodes: Node[];
|
||||||
isMarlinServerGroup: boolean;
|
isMarlinServerGroup: boolean;
|
||||||
@@ -48,8 +47,13 @@ export interface VCoreMongoConnectionParams {
|
|||||||
connectionString: string;
|
connectionString: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface FabricContext {
|
||||||
|
connectionId: string;
|
||||||
|
databaseConnectionInfo: FabricDatabaseConnectionInfo | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
interface UserContext {
|
interface UserContext {
|
||||||
readonly fabricDatabaseConnectionInfo?: FabricDatabaseConnectionInfo;
|
readonly fabricContext?: FabricContext;
|
||||||
readonly authType?: AuthType;
|
readonly authType?: AuthType;
|
||||||
readonly masterKey?: string;
|
readonly masterKey?: string;
|
||||||
readonly subscriptionId?: string;
|
readonly subscriptionId?: string;
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { createUri } from "Common/UrlUtility";
|
import { createUri } from "Common/UrlUtility";
|
||||||
import { FabricDatabaseConnectionInfo, FabricMessage } from "Contracts/FabricContract";
|
import { DATA_EXPLORER_RPC_VERSION } from "Contracts/DataExplorerMessagesContract";
|
||||||
|
import { FABRIC_RPC_VERSION, FabricMessageV2 } from "Contracts/FabricMessagesContract";
|
||||||
import Explorer from "Explorer/Explorer";
|
import Explorer from "Explorer/Explorer";
|
||||||
|
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
import { useSelectedNode } from "Explorer/useSelectedNode";
|
import { useSelectedNode } from "Explorer/useSelectedNode";
|
||||||
import {
|
import { scheduleRefreshDatabaseResourceToken } from "Platform/Fabric/FabricUtil";
|
||||||
handleRequestDatabaseResourceTokensResponse,
|
|
||||||
scheduleRefreshDatabaseResourceToken,
|
|
||||||
} from "Platform/Fabric/FabricUtil";
|
|
||||||
import { getNetworkSettingsWarningMessage } from "Utils/NetworkUtility";
|
import { getNetworkSettingsWarningMessage } from "Utils/NetworkUtility";
|
||||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
||||||
@@ -88,6 +87,9 @@ export function useKnockoutExplorer(platform: Platform): Explorer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function configureFabric(): Promise<Explorer> {
|
async function configureFabric(): Promise<Explorer> {
|
||||||
|
// These are the versions of Fabric that Data Explorer supports.
|
||||||
|
const SUPPORTED_FABRIC_VERSIONS = [FABRIC_RPC_VERSION];
|
||||||
|
|
||||||
let explorer: Explorer;
|
let explorer: Explorer;
|
||||||
return new Promise<Explorer>((resolve) => {
|
return new Promise<Explorer>((resolve) => {
|
||||||
window.addEventListener(
|
window.addEventListener(
|
||||||
@@ -101,38 +103,37 @@ async function configureFabric(): Promise<Explorer> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data: FabricMessage = event.data?.data;
|
const data: FabricMessageV2 = event.data?.data;
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (data.type) {
|
switch (data.type) {
|
||||||
case "initialize": {
|
case "initialize": {
|
||||||
const fabricDatabaseConnectionInfo: FabricDatabaseConnectionInfo = {
|
const fabricVersion = data.version;
|
||||||
endpoint: data.message.endpoint,
|
if (!SUPPORTED_FABRIC_VERSIONS.includes(fabricVersion)) {
|
||||||
databaseId: data.message.databaseId,
|
// TODO Surface error to user
|
||||||
resourceTokens: data.message.resourceTokens as { [resourceId: string]: string },
|
console.error(`Unsupported Fabric version: ${fabricVersion}`);
|
||||||
resourceTokensTimestamp: data.message.resourceTokensTimestamp,
|
return;
|
||||||
};
|
}
|
||||||
explorer = await createExplorerFabric(fabricDatabaseConnectionInfo);
|
|
||||||
resolve(explorer);
|
|
||||||
|
|
||||||
explorer.refreshAllDatabases().then(() => {
|
explorer = createExplorerFabric(data.message);
|
||||||
openFirstContainer(explorer, fabricDatabaseConnectionInfo.databaseId);
|
await scheduleRefreshDatabaseResourceToken(true);
|
||||||
});
|
resolve(explorer);
|
||||||
scheduleRefreshDatabaseResourceToken();
|
await explorer.refreshAllDatabases();
|
||||||
|
openFirstContainer(explorer, userContext.fabricContext.databaseConnectionInfo.databaseId);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "newContainer":
|
case "newContainer":
|
||||||
explorer.onNewCollectionClicked();
|
explorer.onNewCollectionClicked();
|
||||||
break;
|
break;
|
||||||
case "authorizationToken": {
|
case "authorizationToken":
|
||||||
|
case "allResourceTokens_v2": {
|
||||||
handleCachedDataMessage(data);
|
handleCachedDataMessage(data);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "allResourceTokens": {
|
case "setToolbarStatus": {
|
||||||
// TODO call handleCachedDataMessage when Fabric echoes message id back
|
useCommandBar.getState().setIsHidden(data.message.visible === false);
|
||||||
handleRequestDatabaseResourceTokensResponse(explorer, data.message as FabricDatabaseConnectionInfo);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
@@ -143,7 +144,11 @@ async function configureFabric(): Promise<Explorer> {
|
|||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
sendReadyMessage();
|
sendMessage({
|
||||||
|
type: MessageTypes.Ready,
|
||||||
|
id: "ready",
|
||||||
|
params: [DATA_EXPLORER_RPC_VERSION],
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -319,9 +324,12 @@ function configureHostedWithResourceToken(config: ResourceToken): Explorer {
|
|||||||
return explorer;
|
return explorer;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createExplorerFabric(fabricDatabaseConnectionInfo: FabricDatabaseConnectionInfo): Explorer {
|
function createExplorerFabric(params: { connectionId: string }): Explorer {
|
||||||
updateUserContext({
|
updateUserContext({
|
||||||
fabricDatabaseConnectionInfo,
|
fabricContext: {
|
||||||
|
connectionId: params.connectionId,
|
||||||
|
databaseConnectionInfo: undefined,
|
||||||
|
},
|
||||||
authType: AuthType.ConnectionString,
|
authType: AuthType.ConnectionString,
|
||||||
databaseAccount: {
|
databaseAccount: {
|
||||||
id: "",
|
id: "",
|
||||||
@@ -330,7 +338,7 @@ function createExplorerFabric(fabricDatabaseConnectionInfo: FabricDatabaseConnec
|
|||||||
name: "Mounted",
|
name: "Mounted",
|
||||||
kind: AccountKind.Default,
|
kind: AccountKind.Default,
|
||||||
properties: {
|
properties: {
|
||||||
documentEndpoint: fabricDatabaseConnectionInfo.endpoint,
|
documentEndpoint: undefined,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ export interface QueryCopilotState {
|
|||||||
queryResults: QueryResults | undefined;
|
queryResults: QueryResults | undefined;
|
||||||
errorMessage: string;
|
errorMessage: string;
|
||||||
isSamplePromptsOpen: boolean;
|
isSamplePromptsOpen: boolean;
|
||||||
showPromptTeachingBubble: boolean;
|
|
||||||
showDeletePopup: boolean;
|
showDeletePopup: boolean;
|
||||||
showFeedbackBar: boolean;
|
showFeedbackBar: boolean;
|
||||||
showCopyPopup: boolean;
|
showCopyPopup: boolean;
|
||||||
@@ -72,7 +71,6 @@ export interface QueryCopilotState {
|
|||||||
setQueryResults: (queryResults: QueryResults | undefined) => void;
|
setQueryResults: (queryResults: QueryResults | undefined) => void;
|
||||||
setErrorMessage: (errorMessage: string) => void;
|
setErrorMessage: (errorMessage: string) => void;
|
||||||
setIsSamplePromptsOpen: (isSamplePromptsOpen: boolean) => void;
|
setIsSamplePromptsOpen: (isSamplePromptsOpen: boolean) => void;
|
||||||
setShowPromptTeachingBubble: (showPromptTeachingBubble: boolean) => void;
|
|
||||||
setShowDeletePopup: (showDeletePopup: boolean) => void;
|
setShowDeletePopup: (showDeletePopup: boolean) => void;
|
||||||
setShowFeedbackBar: (showFeedbackBar: boolean) => void;
|
setShowFeedbackBar: (showFeedbackBar: boolean) => void;
|
||||||
setshowCopyPopup: (showCopyPopup: boolean) => void;
|
setshowCopyPopup: (showCopyPopup: boolean) => void;
|
||||||
@@ -95,7 +93,7 @@ export interface QueryCopilotState {
|
|||||||
resetQueryCopilotStates: () => void;
|
resetQueryCopilotStates: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
type QueryCopilotStore = UseStore<Partial<QueryCopilotState>>;
|
type QueryCopilotStore = UseStore<QueryCopilotState>;
|
||||||
|
|
||||||
export const useQueryCopilot: QueryCopilotStore = create((set) => ({
|
export const useQueryCopilot: QueryCopilotStore = create((set) => ({
|
||||||
copilotEnabled: false,
|
copilotEnabled: false,
|
||||||
|
|||||||
Reference in New Issue
Block a user