Fabric: clean up RPC and support show/hide toolbar message (#1680)

* Use Promise for allResourceToken fabric message. Cleanup token message handling and add debounce.

* Improve rpc and update initalization flow

* Fix format

* Rev up message names for new version

* Refactor RPC with Fabric

* Build fix

* Fix build

* Fix format

* Update Message types

* Fix format

* Fix comments

* Fabric toolbar style and support to show/hide it (#1720)

* Add Fabric specific Toolbar design

* Add Fabric message to show/hide the Toolbar

* Fix CommandBarUtil formatting

* Update zustand state on setToolbarStatus to trigger a redraw of the command bar with updated visibility

---------

Co-authored-by: Laurent Nguyen <laurent.nguyen@microsoft.com>

* Fix format

---------

Co-authored-by: Vsevolod Kukol <sevoku@microsoft.com>
This commit is contained in:
Laurent Nguyen 2024-01-18 16:13:04 +00:00 committed by GitHub
parent 5d13463bdb
commit 0975591945
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 225 additions and 104 deletions

View File

@ -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;

View File

@ -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 {

View File

@ -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);

View File

@ -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",

View File

@ -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];

View File

@ -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

View 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;
};

View File

@ -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,36 +32,50 @@ 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: MessageTypes.GetAuthorizationToken; type: "initialize";
version: string;
id: string; id: string;
params: GetCosmosTokenMessageOptions[]; message: {
connectionId: string;
};
} }
| { | {
type: MessageTypes.GetAllResourceTokens; type: "authorizationToken";
message: {
id: string;
error: string | undefined;
data: AuthorizationToken | undefined;
};
}
| {
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 = {
@ -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;
} }

View File

@ -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 {

View File

@ -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";
@ -384,9 +384,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;
} }

View File

@ -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)}

View File

@ -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,

View File

@ -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 },
databaseAccount: { ...userContext.databaseAccount },
});
scheduleRefreshDatabaseResourceToken(); scheduleRefreshDatabaseResourceToken();
explorer.refreshAllDatabases(); } 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> => {
return new Promise((resolve) => {
if (timeoutId !== undefined) { if (timeoutId !== undefined) {
clearTimeout(timeoutId); clearTimeout(timeoutId);
timeoutId = undefined; 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);
} }
}; };

View File

@ -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";
@ -47,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;

View File

@ -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,
}, },
}, },
}); });