Fabric: handle resource tokens (#1667)
* Update contracts for new all resource messages * Add timestamp to token message signature * Reconstruct resource tree with databases and collections parsed from token dictionary keys * Create FabricDatabase and FabricCollection to turn off interaction * Remove unnecessary FabricCollection derived class * Handle resource tokens * Bug fix * Fix linting issues * Fix update document * Fix partitition keys * Remove special case for FabricDatabase tree node * Modify readCollections to follow normal flow with Fabric * Move fabric databases refresh to data access and remove special case in Explorer * Revert Explorer.tsx changes * Disable database context menu and delete container context menu * Remove create database/container button for Fabric * Fix format * Renew token logic * Parallelize read collections calls to speed up * Disable readDatabaseOffer, because it is too slow for now * Reduce TOKEN_VALIDITY_MS a bit to make sure renewal happens before expiration. Receving new tokens new refreshes databases * Add container element for Main app in HTML * Do not handle "openTab" message anymore * Fix style of main div * Simplify conditional load of the fabric .css * Fix format * Fix tsc can't find dynamic less import --------- Co-authored-by: Armando Trejo Oliver <artrejo@microsoft.com>
This commit is contained in:
parent
8075ef2847
commit
2d3048eafe
|
@ -1,6 +1,8 @@
|
||||||
import * as Cosmos from "@azure/cosmos";
|
import * as Cosmos from "@azure/cosmos";
|
||||||
import { sendCachedDataMessage } from "Common/MessageHandler";
|
import { sendCachedDataMessage } from "Common/MessageHandler";
|
||||||
|
import { getAuthorizationTokenUsingResourceTokens } from "Common/getAuthorizationTokenUsingResourceTokens";
|
||||||
import { AuthorizationToken, MessageTypes } from "Contracts/MessageTypes";
|
import { AuthorizationToken, MessageTypes } from "Contracts/MessageTypes";
|
||||||
|
import { checkDatabaseResourceTokensValidity } from "Platform/Fabric/FabricUtil";
|
||||||
import { AuthType } from "../AuthType";
|
import { AuthType } from "../AuthType";
|
||||||
import { PriorityLevel } from "../Common/Constants";
|
import { PriorityLevel } from "../Common/Constants";
|
||||||
import { Platform, configContext } from "../ConfigContext";
|
import { Platform, configContext } from "../ConfigContext";
|
||||||
|
@ -28,12 +30,33 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (configContext.platform === Platform.Fabric) {
|
if (configContext.platform === Platform.Fabric) {
|
||||||
const authorizationToken = await sendCachedDataMessage<AuthorizationToken>(MessageTypes.GetAuthorizationToken, [
|
switch (requestInfo.resourceType) {
|
||||||
requestInfo,
|
case Cosmos.ResourceType.conflicts:
|
||||||
]);
|
case Cosmos.ResourceType.container:
|
||||||
console.log("Response from Fabric: ", authorizationToken);
|
case Cosmos.ResourceType.sproc:
|
||||||
headers[HttpHeaders.msDate] = authorizationToken.XDate;
|
case Cosmos.ResourceType.udf:
|
||||||
return authorizationToken.PrimaryReadWriteToken;
|
case Cosmos.ResourceType.trigger:
|
||||||
|
case Cosmos.ResourceType.item:
|
||||||
|
case Cosmos.ResourceType.pkranges:
|
||||||
|
// User resource tokens
|
||||||
|
headers[HttpHeaders.msDate] = new Date().toUTCString();
|
||||||
|
const resourceTokens = userContext.fabricDatabaseConnectionInfo.resourceTokens;
|
||||||
|
checkDatabaseResourceTokensValidity(userContext.fabricDatabaseConnectionInfo.resourceTokensTimestamp);
|
||||||
|
return getAuthorizationTokenUsingResourceTokens(resourceTokens, requestInfo.path, requestInfo.resourceId);
|
||||||
|
|
||||||
|
case Cosmos.ResourceType.none:
|
||||||
|
case Cosmos.ResourceType.database:
|
||||||
|
case Cosmos.ResourceType.offer:
|
||||||
|
case Cosmos.ResourceType.user:
|
||||||
|
case Cosmos.ResourceType.permission:
|
||||||
|
// User master tokens
|
||||||
|
const authorizationToken = await sendCachedDataMessage<AuthorizationToken>(MessageTypes.GetAuthorizationToken, [
|
||||||
|
requestInfo,
|
||||||
|
]);
|
||||||
|
console.log("Response from Fabric: ", authorizationToken);
|
||||||
|
headers[HttpHeaders.msDate] = authorizationToken.XDate;
|
||||||
|
return decodeURIComponent(authorizationToken.PrimaryReadWriteToken);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userContext.masterKey) {
|
if (userContext.masterKey) {
|
||||||
|
|
|
@ -1,18 +1,50 @@
|
||||||
|
import { ContainerResponse } from "@azure/cosmos";
|
||||||
import { Queries } from "Common/Constants";
|
import { Queries } from "Common/Constants";
|
||||||
|
import { Platform, configContext } from "ConfigContext";
|
||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
import * as DataModels from "../../Contracts/DataModels";
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
|
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||||
import { listCassandraTables } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
|
import { listCassandraTables } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
|
||||||
import { listGremlinGraphs } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
|
import { listGremlinGraphs } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
|
||||||
import { listMongoDBCollections } from "../../Utils/arm/generatedClients/cosmos/mongoDBResources";
|
import { listMongoDBCollections } from "../../Utils/arm/generatedClients/cosmos/mongoDBResources";
|
||||||
import { listSqlContainers } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
|
import { listSqlContainers } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
|
||||||
import { listTables } from "../../Utils/arm/generatedClients/cosmos/tableResources";
|
import { listTables } from "../../Utils/arm/generatedClients/cosmos/tableResources";
|
||||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
|
||||||
import { client } from "../CosmosClient";
|
import { client } from "../CosmosClient";
|
||||||
import { handleError } from "../ErrorHandlingUtils";
|
import { handleError } from "../ErrorHandlingUtils";
|
||||||
|
|
||||||
export async function readCollections(databaseId: string): Promise<DataModels.Collection[]> {
|
export async function readCollections(databaseId: string): Promise<DataModels.Collection[]> {
|
||||||
const clearMessage = logConsoleProgress(`Querying containers for database ${databaseId}`);
|
const clearMessage = logConsoleProgress(`Querying containers for database ${databaseId}`);
|
||||||
|
|
||||||
|
if (
|
||||||
|
configContext.platform === Platform.Fabric &&
|
||||||
|
userContext.fabricDatabaseConnectionInfo &&
|
||||||
|
userContext.fabricDatabaseConnectionInfo.databaseId === databaseId
|
||||||
|
) {
|
||||||
|
const collections: DataModels.Collection[] = [];
|
||||||
|
const promises: Promise<ContainerResponse>[] = [];
|
||||||
|
|
||||||
|
for (const collectionResourceId in userContext.fabricDatabaseConnectionInfo.resourceTokens) {
|
||||||
|
// Dictionary key looks like this: dbs/SampleDB/colls/Container
|
||||||
|
const resourceIdObj = collectionResourceId.split("/");
|
||||||
|
const tokenDatabaseId = resourceIdObj[1];
|
||||||
|
const tokenCollectionId = resourceIdObj[3];
|
||||||
|
|
||||||
|
if (tokenDatabaseId === databaseId) {
|
||||||
|
promises.push(client().database(databaseId).container(tokenCollectionId).read());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const responses = await Promise.all(promises);
|
||||||
|
responses.forEach((response) => {
|
||||||
|
collections.push(response.resource as DataModels.Collection);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sort collections by id before returning
|
||||||
|
collections.sort((a, b) => a.id.localeCompare(b.id));
|
||||||
|
return collections;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (
|
if (
|
||||||
userContext.authType === AuthType.AAD &&
|
userContext.authType === AuthType.AAD &&
|
||||||
|
|
|
@ -1,15 +1,22 @@
|
||||||
|
import { Platform, configContext } from "ConfigContext";
|
||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
import { Offer, ReadDatabaseOfferParams } from "../../Contracts/DataModels";
|
import { Offer, ReadDatabaseOfferParams } from "../../Contracts/DataModels";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
|
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||||
import { getCassandraKeyspaceThroughput } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
|
import { getCassandraKeyspaceThroughput } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
|
||||||
import { getGremlinDatabaseThroughput } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
|
import { getGremlinDatabaseThroughput } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
|
||||||
import { getMongoDBDatabaseThroughput } from "../../Utils/arm/generatedClients/cosmos/mongoDBResources";
|
import { getMongoDBDatabaseThroughput } from "../../Utils/arm/generatedClients/cosmos/mongoDBResources";
|
||||||
import { getSqlDatabaseThroughput } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
|
import { getSqlDatabaseThroughput } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
|
||||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
|
||||||
import { handleError } from "../ErrorHandlingUtils";
|
import { handleError } from "../ErrorHandlingUtils";
|
||||||
import { readOfferWithSDK } from "./readOfferWithSDK";
|
import { readOfferWithSDK } from "./readOfferWithSDK";
|
||||||
|
|
||||||
export const readDatabaseOffer = async (params: ReadDatabaseOfferParams): Promise<Offer> => {
|
export const readDatabaseOffer = async (params: ReadDatabaseOfferParams): Promise<Offer> => {
|
||||||
|
if (configContext.platform === Platform.Fabric) {
|
||||||
|
// TODO This works, but is very slow, because it requests the token, so we skip for now
|
||||||
|
console.error("Skiping readDatabaseOffer for Fabric");
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
const clearMessage = logConsoleProgress(`Querying offer for database ${params.databaseId}`);
|
const clearMessage = logConsoleProgress(`Querying offer for database ${params.databaseId}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -1,17 +1,45 @@
|
||||||
|
import { Platform, configContext } from "ConfigContext";
|
||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
import * as DataModels from "../../Contracts/DataModels";
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
|
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||||
import { listCassandraKeyspaces } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
|
import { listCassandraKeyspaces } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
|
||||||
import { listGremlinDatabases } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
|
import { listGremlinDatabases } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
|
||||||
import { listMongoDBDatabases } from "../../Utils/arm/generatedClients/cosmos/mongoDBResources";
|
import { listMongoDBDatabases } from "../../Utils/arm/generatedClients/cosmos/mongoDBResources";
|
||||||
import { listSqlDatabases } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
|
import { listSqlDatabases } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
|
||||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
|
||||||
import { client } from "../CosmosClient";
|
import { client } from "../CosmosClient";
|
||||||
import { handleError } from "../ErrorHandlingUtils";
|
import { handleError } from "../ErrorHandlingUtils";
|
||||||
|
|
||||||
export async function readDatabases(): Promise<DataModels.Database[]> {
|
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) {
|
||||||
|
const tokensData = userContext.fabricDatabaseConnectionInfo;
|
||||||
|
|
||||||
|
const databaseIdsSet = new Set<string>(); // databaseId
|
||||||
|
|
||||||
|
for (const collectionResourceId in tokensData.resourceTokens) {
|
||||||
|
// Dictionary key looks like this: dbs/SampleDB/colls/Container
|
||||||
|
const resourceIdObj = collectionResourceId.split("/");
|
||||||
|
const databaseId = resourceIdObj[1];
|
||||||
|
|
||||||
|
databaseIdsSet.add(databaseId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const databases: DataModels.Database[] = Array.from(databaseIdsSet.values())
|
||||||
|
.sort((a, b) => a.localeCompare(b))
|
||||||
|
.map((databaseId) => ({
|
||||||
|
_rid: "",
|
||||||
|
_self: "",
|
||||||
|
_etag: "",
|
||||||
|
_ts: 0,
|
||||||
|
id: databaseId,
|
||||||
|
collections: [],
|
||||||
|
}));
|
||||||
|
return databases;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (
|
if (
|
||||||
userContext.authType === AuthType.AAD &&
|
userContext.authType === AuthType.AAD &&
|
||||||
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
export function getAuthorizationTokenUsingResourceTokens(
|
||||||
|
resourceTokens: { [resourceId: string]: string },
|
||||||
|
path: string,
|
||||||
|
resourceId: string,
|
||||||
|
): string {
|
||||||
|
// console.log(`getting token for path: "${path}" and resourceId: "${resourceId}"`);
|
||||||
|
|
||||||
|
if (resourceTokens && Object.keys(resourceTokens).length > 0) {
|
||||||
|
// For database account access(through getDatabaseAccount API), path and resourceId are "",
|
||||||
|
// so in this case we return the first token to be used for creating the auth header as the
|
||||||
|
// service will accept any token in this case
|
||||||
|
if (!path && !resourceId) {
|
||||||
|
return resourceTokens[Object.keys(resourceTokens)[0]];
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have exact resource token for the path use it
|
||||||
|
if (resourceId && resourceTokens[resourceId]) {
|
||||||
|
return resourceTokens[resourceId];
|
||||||
|
}
|
||||||
|
|
||||||
|
// minimum valid path /dbs
|
||||||
|
if (!path || path.length < 4) {
|
||||||
|
console.error(
|
||||||
|
`Unable to get authotization token for Path:"${path}" and resourcerId:"${resourceId}". Invalid path.`,
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
path = trimSlashFromLeftAndRight(path);
|
||||||
|
const pathSegments = (path && path.split("/")) || [];
|
||||||
|
|
||||||
|
// Item path
|
||||||
|
if (pathSegments.length === 6) {
|
||||||
|
// Look for a container token matching the item path
|
||||||
|
const containerPath = pathSegments.slice(0, 4).map(decodeURIComponent).join("/");
|
||||||
|
if (resourceTokens[containerPath]) {
|
||||||
|
return resourceTokens[containerPath];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is legacy behavior that lets someone use a resource token pointing ONLY at an ID
|
||||||
|
// It was used when _rid was exposed by the SDK, but now that we are using user provided ids it is not needed
|
||||||
|
// However removing it now would be a breaking change
|
||||||
|
// if it's an incomplete path like /dbs/db1/colls/, start from the parent resource
|
||||||
|
let index = pathSegments.length % 2 === 0 ? pathSegments.length - 1 : pathSegments.length - 2;
|
||||||
|
for (; index > 0; index -= 2) {
|
||||||
|
const id = decodeURI(pathSegments[index]);
|
||||||
|
if (resourceTokens[id]) {
|
||||||
|
return resourceTokens[id];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error(`Unable to get authotization token for Path:"${path}" and resourcerId:"${resourceId}"`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const trimLeftSlashes = new RegExp("^[/]+");
|
||||||
|
const trimRightSlashes = new RegExp("[/]+$");
|
||||||
|
function trimSlashFromLeftAndRight(inputString: string): string {
|
||||||
|
if (typeof inputString !== "string") {
|
||||||
|
throw new Error("invalid input: input is not string");
|
||||||
|
}
|
||||||
|
|
||||||
|
return inputString.replace(trimLeftSlashes, "").replace(trimRightSlashes, "");
|
||||||
|
}
|
|
@ -64,6 +64,7 @@ let configContext: Readonly<ConfigContext> = {
|
||||||
`^https:\\/\\/.*\\.fabric\\.microsoft\\.com$`,
|
`^https:\\/\\/.*\\.fabric\\.microsoft\\.com$`,
|
||||||
`^https:\\/\\/.*\\.powerbi\\.com$`,
|
`^https:\\/\\/.*\\.powerbi\\.com$`,
|
||||||
`^https:\\/\\/.*\\.analysis-df\\.net$`,
|
`^https:\\/\\/.*\\.analysis-df\\.net$`,
|
||||||
|
`^https:\\/\\/.*\\.analysis-df\\.windows\\.net$`,
|
||||||
], // Webpack injects this at build time
|
], // Webpack injects this at build time
|
||||||
gitSha: process.env.GIT_SHA,
|
gitSha: process.env.GIT_SHA,
|
||||||
hostedExplorerURL: "https://cosmos.azure.com/",
|
hostedExplorerURL: "https://cosmos.azure.com/",
|
||||||
|
|
|
@ -9,14 +9,12 @@ export type FabricMessage =
|
||||||
type: "initialize";
|
type: "initialize";
|
||||||
message: {
|
message: {
|
||||||
endpoint: string | undefined;
|
endpoint: string | undefined;
|
||||||
|
databaseId: string | undefined;
|
||||||
|
resourceTokens: unknown | undefined;
|
||||||
|
resourceTokensTimestamp: number | undefined;
|
||||||
error: string | undefined;
|
error: string | undefined;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
| {
|
|
||||||
type: "openTab";
|
|
||||||
databaseName: string;
|
|
||||||
collectionName: string | undefined;
|
|
||||||
}
|
|
||||||
| {
|
| {
|
||||||
type: "authorizationToken";
|
type: "authorizationToken";
|
||||||
message: {
|
message: {
|
||||||
|
@ -24,6 +22,15 @@ export type FabricMessage =
|
||||||
error: string | undefined;
|
error: string | undefined;
|
||||||
data: AuthorizationToken | undefined;
|
data: AuthorizationToken | undefined;
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "allResourceTokens";
|
||||||
|
message: {
|
||||||
|
endpoint: string | undefined;
|
||||||
|
databaseId: string | undefined;
|
||||||
|
resourceTokens: unknown | undefined;
|
||||||
|
resourceTokensTimestamp: number | undefined;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DataExploreMessage =
|
export type DataExploreMessage =
|
||||||
|
@ -40,6 +47,9 @@ export type DataExploreMessage =
|
||||||
type: MessageTypes.GetAuthorizationToken;
|
type: MessageTypes.GetAuthorizationToken;
|
||||||
id: string;
|
id: string;
|
||||||
params: GetCosmosTokenMessageOptions[];
|
params: GetCosmosTokenMessageOptions[];
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: MessageTypes.GetAllResourceTokens;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GetCosmosTokenMessageOptions = {
|
export type GetCosmosTokenMessageOptions = {
|
||||||
|
@ -55,4 +65,13 @@ export type CosmosDBTokenResponse = {
|
||||||
|
|
||||||
export type CosmosDBConnectionInfoResponse = {
|
export type CosmosDBConnectionInfoResponse = {
|
||||||
endpoint: string;
|
endpoint: string;
|
||||||
|
databaseId: string;
|
||||||
|
resourceTokens: unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface FabricDatabaseConnectionInfo {
|
||||||
|
endpoint: string;
|
||||||
|
databaseId: string;
|
||||||
|
resourceTokens: { [resourceId: string]: string };
|
||||||
|
resourceTokensTimestamp: number;
|
||||||
|
}
|
||||||
|
|
|
@ -40,6 +40,7 @@ export enum MessageTypes {
|
||||||
|
|
||||||
// Data Explorer -> Fabric communication
|
// Data Explorer -> Fabric communication
|
||||||
GetAuthorizationToken,
|
GetAuthorizationToken,
|
||||||
|
GetAllResourceTokens,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AuthorizationToken {
|
export interface AuthorizationToken {
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
declare module "*.less" {
|
||||||
|
const value: string;
|
||||||
|
export default value;
|
||||||
|
}
|
|
@ -129,20 +129,22 @@ export const createCollectionContextMenuButton = (
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
items.push({
|
if (configContext.platform !== Platform.Fabric) {
|
||||||
iconSrc: DeleteCollectionIcon,
|
items.push({
|
||||||
onClick: () => {
|
iconSrc: DeleteCollectionIcon,
|
||||||
useSelectedNode.getState().setSelectedNode(selectedCollection);
|
onClick: () => {
|
||||||
useSidePanel
|
useSelectedNode.getState().setSelectedNode(selectedCollection);
|
||||||
.getState()
|
useSidePanel
|
||||||
.openSidePanel(
|
.getState()
|
||||||
"Delete " + getCollectionName(),
|
.openSidePanel(
|
||||||
<DeleteCollectionConfirmationPane refreshDatabases={() => container.refreshAllDatabases()} />,
|
"Delete " + getCollectionName(),
|
||||||
);
|
<DeleteCollectionConfirmationPane refreshDatabases={() => container.refreshAllDatabases()} />,
|
||||||
},
|
);
|
||||||
label: `Delete ${getCollectionName()}`,
|
},
|
||||||
styleClass: "deleteCollectionMenuItem",
|
label: `Delete ${getCollectionName()}`,
|
||||||
});
|
styleClass: "deleteCollectionMenuItem",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { sendMessage } from "Common/MessageHandler";
|
||||||
import { Platform, configContext } from "ConfigContext";
|
import { Platform, configContext } from "ConfigContext";
|
||||||
import { MessageTypes } from "Contracts/ExplorerContracts";
|
import { MessageTypes } from "Contracts/ExplorerContracts";
|
||||||
import { IGalleryItem } from "Juno/JunoClient";
|
import { IGalleryItem } from "Juno/JunoClient";
|
||||||
|
import { requestDatabaseResourceTokens } from "Platform/Fabric/FabricUtil";
|
||||||
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointValidation";
|
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointValidation";
|
||||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
|
@ -379,6 +380,13 @@ export default class Explorer {
|
||||||
};
|
};
|
||||||
|
|
||||||
public onRefreshResourcesClick = (): void => {
|
public onRefreshResourcesClick = (): void => {
|
||||||
|
if (configContext.platform === Platform.Fabric) {
|
||||||
|
// Requesting the tokens will trigger a refresh of the databases
|
||||||
|
// TODO: Once the id is returned from Fabric, we can await this call and then refresh the databases here
|
||||||
|
requestDatabaseResourceTokens();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
userContext.authType === AuthType.ResourceToken
|
userContext.authType === AuthType.ResourceToken
|
||||||
? this.refreshDatabaseForResourceToken()
|
? this.refreshDatabaseForResourceToken()
|
||||||
: this.refreshAllDatabases();
|
: this.refreshAllDatabases();
|
||||||
|
|
|
@ -50,31 +50,36 @@ export function createStaticCommandBarButtons(
|
||||||
return createStaticCommandBarButtonsForResourceToken(container, selectedNodeState);
|
return createStaticCommandBarButtonsForResourceToken(container, selectedNodeState);
|
||||||
}
|
}
|
||||||
|
|
||||||
const newCollectionBtn = createNewCollectionGroup(container);
|
|
||||||
const buttons: CommandButtonComponentProps[] = [];
|
const buttons: CommandButtonComponentProps[] = [];
|
||||||
|
|
||||||
buttons.push(newCollectionBtn);
|
// Avoid starting with a divider
|
||||||
if (
|
const addDivider = () => {
|
||||||
configContext.platform !== Platform.Fabric &&
|
if (buttons.length > 0) {
|
||||||
userContext.apiType !== "Tables" &&
|
|
||||||
userContext.apiType !== "Cassandra"
|
|
||||||
) {
|
|
||||||
const addSynapseLink = createOpenSynapseLinkDialogButton(container);
|
|
||||||
|
|
||||||
if (addSynapseLink) {
|
|
||||||
buttons.push(createDivider());
|
buttons.push(createDivider());
|
||||||
buttons.push(addSynapseLink);
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (configContext.platform !== Platform.Fabric) {
|
||||||
|
const newCollectionBtn = createNewCollectionGroup(container);
|
||||||
|
buttons.push(newCollectionBtn);
|
||||||
|
if (userContext.apiType !== "Tables" && userContext.apiType !== "Cassandra") {
|
||||||
|
const addSynapseLink = createOpenSynapseLinkDialogButton(container);
|
||||||
|
|
||||||
|
if (addSynapseLink) {
|
||||||
|
addDivider();
|
||||||
|
buttons.push(addSynapseLink);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userContext.apiType !== "Tables") {
|
||||||
|
newCollectionBtn.children = [createNewCollectionGroup(container)];
|
||||||
|
const newDatabaseBtn = createNewDatabase(container);
|
||||||
|
newCollectionBtn.children.push(newDatabaseBtn);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userContext.apiType !== "Tables" && configContext.platform !== Platform.Fabric) {
|
|
||||||
newCollectionBtn.children = [createNewCollectionGroup(container)];
|
|
||||||
const newDatabaseBtn = createNewDatabase(container);
|
|
||||||
newCollectionBtn.children.push(newDatabaseBtn);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (useNotebook.getState().isNotebookEnabled) {
|
if (useNotebook.getState().isNotebookEnabled) {
|
||||||
buttons.push(createDivider());
|
addDivider();
|
||||||
const notebookButtons: CommandButtonComponentProps[] = [];
|
const notebookButtons: CommandButtonComponentProps[] = [];
|
||||||
|
|
||||||
const newNotebookButton = createNewNotebookButton(container);
|
const newNotebookButton = createNewNotebookButton(container);
|
||||||
|
@ -128,7 +133,7 @@ export function createStaticCommandBarButtons(
|
||||||
const isQuerySupported = userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
|
const isQuerySupported = userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
|
||||||
|
|
||||||
if (isQuerySupported) {
|
if (isQuerySupported) {
|
||||||
buttons.push(createDivider());
|
addDivider();
|
||||||
const newSqlQueryBtn = createNewSQLQueryButton(selectedNodeState);
|
const newSqlQueryBtn = createNewSQLQueryButton(selectedNodeState);
|
||||||
buttons.push(newSqlQueryBtn);
|
buttons.push(newSqlQueryBtn);
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,6 @@ import { useDatabases } from "Explorer/useDatabases";
|
||||||
import { useTabs } from "hooks/useTabs";
|
import { useTabs } from "hooks/useTabs";
|
||||||
import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg";
|
import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg";
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
import * as ViewModels from "../../Contracts/ViewModels";
|
||||||
import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory";
|
|
||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
|
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
import { useSelectedNode } from "../useSelectedNode";
|
import { useSelectedNode } from "../useSelectedNode";
|
||||||
|
@ -22,7 +21,7 @@ export const useDatabaseTreeNodes = (container: Explorer, isNotebookEnabled: boo
|
||||||
className: "databaseHeader",
|
className: "databaseHeader",
|
||||||
children: [],
|
children: [],
|
||||||
isSelected: () => useSelectedNode.getState().isDataNodeSelected(database.id()),
|
isSelected: () => useSelectedNode.getState().isDataNodeSelected(database.id()),
|
||||||
contextMenu: ResourceTreeContextMenuButtonFactory.createDatabaseContextMenu(container, database.id()),
|
contextMenu: undefined, // TODO Disable this for now as the actions don't work. ResourceTreeContextMenuButtonFactory.createDatabaseContextMenu(container, database.id()),
|
||||||
onExpanded: async () => {
|
onExpanded: async () => {
|
||||||
useSelectedNode.getState().setSelectedNode(database);
|
useSelectedNode.getState().setSelectedNode(database);
|
||||||
if (!databaseNode.children || databaseNode.children?.length === 0) {
|
if (!databaseNode.children || databaseNode.children?.length === 0) {
|
||||||
|
|
40
src/Main.tsx
40
src/Main.tsx
|
@ -1,13 +1,13 @@
|
||||||
// CSS Dependencies
|
// CSS Dependencies
|
||||||
import { initializeIcons, loadTheme } from "@fluentui/react";
|
import { initializeIcons, loadTheme } from "@fluentui/react";
|
||||||
|
import "bootstrap/dist/css/bootstrap.css";
|
||||||
import { QuickstartCarousel } from "Explorer/Quickstart/QuickstartCarousel";
|
import { QuickstartCarousel } from "Explorer/Quickstart/QuickstartCarousel";
|
||||||
import { MongoQuickstartTutorial } from "Explorer/Quickstart/Tutorials/MongoQuickstartTutorial";
|
import { MongoQuickstartTutorial } from "Explorer/Quickstart/Tutorials/MongoQuickstartTutorial";
|
||||||
import { SQLQuickstartTutorial } from "Explorer/Quickstart/Tutorials/SQLQuickstartTutorial";
|
import { SQLQuickstartTutorial } from "Explorer/Quickstart/Tutorials/SQLQuickstartTutorial";
|
||||||
import { userContext } from "UserContext";
|
|
||||||
import "bootstrap/dist/css/bootstrap.css";
|
|
||||||
import { useCarousel } from "hooks/useCarousel";
|
import { useCarousel } from "hooks/useCarousel";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
|
import { userContext } from "UserContext";
|
||||||
import "../externals/jquery-ui.min.css";
|
import "../externals/jquery-ui.min.css";
|
||||||
import "../externals/jquery-ui.min.js";
|
import "../externals/jquery-ui.min.js";
|
||||||
import "../externals/jquery-ui.structure.min.css";
|
import "../externals/jquery-ui.structure.min.css";
|
||||||
|
@ -16,27 +16,27 @@ import "../externals/jquery.dataTables.min.css";
|
||||||
import "../externals/jquery.typeahead.min.css";
|
import "../externals/jquery.typeahead.min.css";
|
||||||
import "../externals/jquery.typeahead.min.js";
|
import "../externals/jquery.typeahead.min.js";
|
||||||
// Image Dependencies
|
// Image Dependencies
|
||||||
|
import { Platform } from "ConfigContext";
|
||||||
import { QueryCopilotCarousel } from "Explorer/QueryCopilot/CopilotCarousel";
|
import { QueryCopilotCarousel } from "Explorer/QueryCopilot/CopilotCarousel";
|
||||||
import { QueryCopilotFeedbackModal } from "Explorer/QueryCopilot/Modal/QueryCopilotFeedbackModal";
|
import { QueryCopilotFeedbackModal } from "Explorer/QueryCopilot/Modal/QueryCopilotFeedbackModal";
|
||||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
import "../images/CosmosDB_rgb_ui_lighttheme.ico";
|
import "../images/CosmosDB_rgb_ui_lighttheme.ico";
|
||||||
import hdeConnectImage from "../images/HdeConnectCosmosDB.svg";
|
|
||||||
import "../images/favicon.ico";
|
import "../images/favicon.ico";
|
||||||
import "../less/TableStyles/CustomizeColumns.less";
|
import hdeConnectImage from "../images/HdeConnectCosmosDB.svg";
|
||||||
import "../less/TableStyles/EntityEditor.less";
|
|
||||||
import "../less/TableStyles/fulldatatables.less";
|
|
||||||
import "../less/TableStyles/queryBuilder.less";
|
|
||||||
import * as StyleConstants from "./Common/StyleConstants";
|
|
||||||
import { configContext, Platform } from "ConfigContext";
|
|
||||||
import "../less/documentDB.less";
|
import "../less/documentDB.less";
|
||||||
import "../less/forms.less";
|
import "../less/forms.less";
|
||||||
import "../less/infobox.less";
|
import "../less/infobox.less";
|
||||||
import "../less/menus.less";
|
import "../less/menus.less";
|
||||||
import "../less/messagebox.less";
|
import "../less/messagebox.less";
|
||||||
import "../less/resourceTree.less";
|
import "../less/resourceTree.less";
|
||||||
|
import "../less/TableStyles/CustomizeColumns.less";
|
||||||
|
import "../less/TableStyles/EntityEditor.less";
|
||||||
|
import "../less/TableStyles/fulldatatables.less";
|
||||||
|
import "../less/TableStyles/queryBuilder.less";
|
||||||
import "../less/tree.less";
|
import "../less/tree.less";
|
||||||
import { CollapsedResourceTree } from "./Common/CollapsedResourceTree";
|
import { CollapsedResourceTree } from "./Common/CollapsedResourceTree";
|
||||||
import { ResourceTreeContainer } from "./Common/ResourceTreeContainer";
|
import { ResourceTreeContainer } from "./Common/ResourceTreeContainer";
|
||||||
|
import * as StyleConstants from "./Common/StyleConstants";
|
||||||
import "./Explorer/Controls/Accordion/AccordionComponent.less";
|
import "./Explorer/Controls/Accordion/AccordionComponent.less";
|
||||||
import "./Explorer/Controls/CollapsiblePanel/CollapsiblePanelComponent.less";
|
import "./Explorer/Controls/CollapsiblePanel/CollapsiblePanelComponent.less";
|
||||||
import { Dialog } from "./Explorer/Controls/Dialog";
|
import { Dialog } from "./Explorer/Controls/Dialog";
|
||||||
|
@ -55,11 +55,11 @@ import "./Explorer/Panes/PanelComponent.less";
|
||||||
import { SidePanel } from "./Explorer/Panes/PanelContainerComponent";
|
import { SidePanel } from "./Explorer/Panes/PanelContainerComponent";
|
||||||
import "./Explorer/SplashScreen/SplashScreen.less";
|
import "./Explorer/SplashScreen/SplashScreen.less";
|
||||||
import { Tabs } from "./Explorer/Tabs/Tabs";
|
import { Tabs } from "./Explorer/Tabs/Tabs";
|
||||||
import "./Libs/jquery";
|
|
||||||
import "./Shared/appInsights";
|
|
||||||
import { useConfig } from "./hooks/useConfig";
|
import { useConfig } from "./hooks/useConfig";
|
||||||
import { useKnockoutExplorer } from "./hooks/useKnockoutExplorer";
|
import { useKnockoutExplorer } from "./hooks/useKnockoutExplorer";
|
||||||
|
import "./Libs/jquery";
|
||||||
import { appThemeFabric } from "./Platform/Fabric/FabricTheme";
|
import { appThemeFabric } from "./Platform/Fabric/FabricTheme";
|
||||||
|
import "./Shared/appInsights";
|
||||||
|
|
||||||
initializeIcons();
|
initializeIcons();
|
||||||
|
|
||||||
|
@ -72,6 +72,7 @@ const App: React.FunctionComponent = () => {
|
||||||
const config = useConfig();
|
const config = useConfig();
|
||||||
if (config?.platform === Platform.Fabric) {
|
if (config?.platform === Platform.Fabric) {
|
||||||
loadTheme(appThemeFabric);
|
loadTheme(appThemeFabric);
|
||||||
|
import("../less/documentDBFabric.less");
|
||||||
}
|
}
|
||||||
StyleConstants.updateStyles();
|
StyleConstants.updateStyles();
|
||||||
const explorer = useKnockoutExplorer(config?.platform);
|
const explorer = useKnockoutExplorer(config?.platform);
|
||||||
|
@ -91,7 +92,6 @@ const App: React.FunctionComponent = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flexContainer" aria-hidden="false">
|
<div className="flexContainer" aria-hidden="false">
|
||||||
<LoadFabricOverrides />
|
|
||||||
<div id="divExplorer" className="flexContainer hideOverflows">
|
<div id="divExplorer" className="flexContainer hideOverflows">
|
||||||
<div id="freeTierTeachingBubble"> </div>
|
<div id="freeTierTeachingBubble"> </div>
|
||||||
{/* Main Command Bar - Start */}
|
{/* Main Command Bar - Start */}
|
||||||
|
@ -141,20 +141,8 @@ const App: React.FunctionComponent = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
ReactDOM.render(<App />, document.body);
|
const mainElement = document.getElementById("Main");
|
||||||
|
ReactDOM.render(<App />, mainElement);
|
||||||
function LoadFabricOverrides(): JSX.Element {
|
|
||||||
if (configContext.platform === Platform.Fabric) {
|
|
||||||
const FabricStyle = React.lazy(() => import("./Platform/Fabric/FabricPlatform"));
|
|
||||||
return (
|
|
||||||
<React.Suspense fallback={<div></div>}>
|
|
||||||
<FabricStyle />
|
|
||||||
</React.Suspense>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return <></>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function LoadingExplorer(): JSX.Element {
|
function LoadingExplorer(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import "../../../less/documentDBFabric.less";
|
|
||||||
// This is a dummy export, allowing us to conditionally import documentDBFabric.less
|
|
||||||
// by lazy-importing this in Main.tsx (see LoadFabricOverrides() there)
|
|
||||||
export default function InitFabric() {
|
|
||||||
return <></>;
|
|
||||||
}
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
import { sendCachedDataMessage } from "Common/MessageHandler";
|
||||||
|
import { FabricDatabaseConnectionInfo } from "Contracts/FabricContract";
|
||||||
|
import { MessageTypes } from "Contracts/MessageTypes";
|
||||||
|
import Explorer from "Explorer/Explorer";
|
||||||
|
import { updateUserContext } from "UserContext";
|
||||||
|
|
||||||
|
const TOKEN_VALIDITY_MS = (3600 - 600) * 1000; // 1 hour minus 10 minutes to be safe
|
||||||
|
let timeoutId: NodeJS.Timeout;
|
||||||
|
|
||||||
|
// Prevents multiple parallel requests
|
||||||
|
let isRequestPending = false;
|
||||||
|
|
||||||
|
export const requestDatabaseResourceTokens = (): void => {
|
||||||
|
if (isRequestPending) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO Make Fabric return the message id so we can handle this promise
|
||||||
|
isRequestPending = true;
|
||||||
|
sendCachedDataMessage<FabricDatabaseConnectionInfo>(MessageTypes.GetAllResourceTokens, []);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const handleRequestDatabaseResourceTokensResponse = (
|
||||||
|
explorer: Explorer,
|
||||||
|
fabricDatabaseConnectionInfo: FabricDatabaseConnectionInfo,
|
||||||
|
): void => {
|
||||||
|
isRequestPending = false;
|
||||||
|
updateUserContext({ fabricDatabaseConnectionInfo });
|
||||||
|
scheduleRefreshDatabaseResourceToken();
|
||||||
|
explorer.refreshAllDatabases();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check token validity and schedule a refresh if necessary
|
||||||
|
* @param tokenTimestamp
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const scheduleRefreshDatabaseResourceToken = (): void => {
|
||||||
|
if (timeoutId !== undefined) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
timeoutId = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
timeoutId = setTimeout(() => {
|
||||||
|
requestDatabaseResourceTokens();
|
||||||
|
}, TOKEN_VALIDITY_MS);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const checkDatabaseResourceTokensValidity = (tokenTimestamp: number): void => {
|
||||||
|
if (tokenTimestamp + TOKEN_VALIDITY_MS < Date.now()) {
|
||||||
|
requestDatabaseResourceTokens();
|
||||||
|
}
|
||||||
|
};
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { FabricDatabaseConnectionInfo } from "Contracts/FabricContract";
|
||||||
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,6 +48,7 @@ export interface VCoreMongoConnectionParams {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UserContext {
|
interface UserContext {
|
||||||
|
readonly fabricDatabaseConnectionInfo?: FabricDatabaseConnectionInfo;
|
||||||
readonly authType?: AuthType;
|
readonly authType?: AuthType;
|
||||||
readonly masterKey?: string;
|
readonly masterKey?: string;
|
||||||
readonly subscriptionId?: string;
|
readonly subscriptionId?: string;
|
||||||
|
|
|
@ -8,5 +8,7 @@
|
||||||
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
|
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body></body>
|
<body>
|
||||||
|
<div id="Main" style="height: 100%"></div>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
import { createUri } from "Common/UrlUtility";
|
import { createUri } from "Common/UrlUtility";
|
||||||
import { FabricMessage } from "Contracts/FabricContract";
|
import { FabricDatabaseConnectionInfo, FabricMessage } from "Contracts/FabricContract";
|
||||||
import Explorer from "Explorer/Explorer";
|
import Explorer from "Explorer/Explorer";
|
||||||
import { useSelectedNode } from "Explorer/useSelectedNode";
|
import { useSelectedNode } from "Explorer/useSelectedNode";
|
||||||
|
import {
|
||||||
|
handleRequestDatabaseResourceTokensResponse,
|
||||||
|
scheduleRefreshDatabaseResourceToken,
|
||||||
|
} from "Platform/Fabric/FabricUtil";
|
||||||
import { getNetworkSettingsWarningMessage } from "Utils/NetworkUtility";
|
import { getNetworkSettingsWarningMessage } from "Utils/NetworkUtility";
|
||||||
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
@ -98,60 +102,39 @@ async function configureFabric(): Promise<Explorer> {
|
||||||
}
|
}
|
||||||
|
|
||||||
const data: FabricMessage = event.data?.data;
|
const data: FabricMessage = event.data?.data;
|
||||||
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (data.type) {
|
switch (data.type) {
|
||||||
case "initialize": {
|
case "initialize": {
|
||||||
explorer = await configureWithFabric(data.message.endpoint);
|
const fabricDatabaseConnectionInfo: FabricDatabaseConnectionInfo = {
|
||||||
|
endpoint: data.message.endpoint,
|
||||||
|
databaseId: data.message.databaseId,
|
||||||
|
resourceTokens: data.message.resourceTokens as { [resourceId: string]: string },
|
||||||
|
resourceTokensTimestamp: data.message.resourceTokensTimestamp,
|
||||||
|
};
|
||||||
|
explorer = await createExplorerFabric(fabricDatabaseConnectionInfo);
|
||||||
resolve(explorer);
|
resolve(explorer);
|
||||||
|
|
||||||
|
explorer.refreshAllDatabases().then(() => {
|
||||||
|
openFirstContainer(explorer, fabricDatabaseConnectionInfo.databaseId);
|
||||||
|
});
|
||||||
|
scheduleRefreshDatabaseResourceToken();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "newContainer":
|
case "newContainer":
|
||||||
explorer.onNewCollectionClicked();
|
explorer.onNewCollectionClicked();
|
||||||
break;
|
break;
|
||||||
case "openTab": {
|
|
||||||
// Expand database first
|
|
||||||
const databaseName = sessionStorage.getItem("openDatabaseName") ?? data.databaseName;
|
|
||||||
const database = useDatabases.getState().databases.find((db) => db.id() === databaseName);
|
|
||||||
if (database) {
|
|
||||||
await database.expandDatabase();
|
|
||||||
useDatabases.getState().updateDatabase(database);
|
|
||||||
useSelectedNode.getState().setSelectedNode(database);
|
|
||||||
|
|
||||||
let collectionResourceId = data.collectionName;
|
|
||||||
if (collectionResourceId === undefined) {
|
|
||||||
// Pick first collection if collectionName not specified in message
|
|
||||||
collectionResourceId = database.collections()[0]?.id();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (collectionResourceId !== undefined) {
|
|
||||||
// Expand collection
|
|
||||||
const collection = database.collections().find((coll) => coll.id() === collectionResourceId);
|
|
||||||
collection.expandCollection();
|
|
||||||
useSelectedNode.getState().setSelectedNode(collection);
|
|
||||||
|
|
||||||
handleOpenAction(
|
|
||||||
{
|
|
||||||
actionType: ActionType.OpenCollectionTab,
|
|
||||||
databaseResourceId: databaseName,
|
|
||||||
collectionResourceId: data.collectionName,
|
|
||||||
tabKind: TabKind.SQLDocuments,
|
|
||||||
} as DataExplorerAction,
|
|
||||||
useDatabases.getState().databases,
|
|
||||||
explorer,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "authorizationToken": {
|
case "authorizationToken": {
|
||||||
handleCachedDataMessage(data);
|
handleCachedDataMessage(data);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "allResourceTokens": {
|
||||||
|
// TODO call handleCachedDataMessage when Fabric echoes message id back
|
||||||
|
handleRequestDatabaseResourceTokensResponse(explorer, data.message as FabricDatabaseConnectionInfo);
|
||||||
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
console.error(`Unknown Fabric message type: ${JSON.stringify(data)}`);
|
console.error(`Unknown Fabric message type: ${JSON.stringify(data)}`);
|
||||||
break;
|
break;
|
||||||
|
@ -164,6 +147,41 @@ async function configureFabric(): Promise<Explorer> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const openFirstContainer = async (explorer: Explorer, databaseName: string, collectionName?: string) => {
|
||||||
|
// Expand database first
|
||||||
|
databaseName = sessionStorage.getItem("openDatabaseName") ?? databaseName;
|
||||||
|
const database = useDatabases.getState().databases.find((db) => db.id() === databaseName);
|
||||||
|
if (database) {
|
||||||
|
await database.expandDatabase();
|
||||||
|
useDatabases.getState().updateDatabase(database);
|
||||||
|
useSelectedNode.getState().setSelectedNode(database);
|
||||||
|
|
||||||
|
let collectionResourceId = collectionName;
|
||||||
|
if (collectionResourceId === undefined) {
|
||||||
|
// Pick first collection if collectionName not specified in message
|
||||||
|
collectionResourceId = database.collections()[0]?.id();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (collectionResourceId !== undefined) {
|
||||||
|
// Expand collection
|
||||||
|
const collection = database.collections().find((coll) => coll.id() === collectionResourceId);
|
||||||
|
collection.expandCollection();
|
||||||
|
useSelectedNode.getState().setSelectedNode(collection);
|
||||||
|
|
||||||
|
handleOpenAction(
|
||||||
|
{
|
||||||
|
actionType: ActionType.OpenCollectionTab,
|
||||||
|
databaseResourceId: databaseName,
|
||||||
|
collectionResourceId: collectionName,
|
||||||
|
tabKind: TabKind.SQLDocuments,
|
||||||
|
} as DataExplorerAction,
|
||||||
|
useDatabases.getState().databases,
|
||||||
|
explorer,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
async function configureHosted(): Promise<Explorer> {
|
async function configureHosted(): Promise<Explorer> {
|
||||||
const win = window as unknown as HostedExplorerChildFrame;
|
const win = window as unknown as HostedExplorerChildFrame;
|
||||||
let explorer: Explorer;
|
let explorer: Explorer;
|
||||||
|
@ -301,8 +319,9 @@ function configureHostedWithResourceToken(config: ResourceToken): Explorer {
|
||||||
return explorer;
|
return explorer;
|
||||||
}
|
}
|
||||||
|
|
||||||
function configureWithFabric(documentEndpoint: string): Explorer {
|
function createExplorerFabric(fabricDatabaseConnectionInfo: FabricDatabaseConnectionInfo): Explorer {
|
||||||
updateUserContext({
|
updateUserContext({
|
||||||
|
fabricDatabaseConnectionInfo,
|
||||||
authType: AuthType.ConnectionString,
|
authType: AuthType.ConnectionString,
|
||||||
databaseAccount: {
|
databaseAccount: {
|
||||||
id: "",
|
id: "",
|
||||||
|
@ -311,12 +330,11 @@ function configureWithFabric(documentEndpoint: string): Explorer {
|
||||||
name: "Mounted",
|
name: "Mounted",
|
||||||
kind: AccountKind.Default,
|
kind: AccountKind.Default,
|
||||||
properties: {
|
properties: {
|
||||||
documentEndpoint,
|
documentEndpoint: fabricDatabaseConnectionInfo.endpoint,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const explorer = new Explorer();
|
const explorer = new Explorer();
|
||||||
setTimeout(() => explorer.refreshAllDatabases(), 0);
|
|
||||||
return explorer;
|
return explorer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue