mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-22 02:11:29 +00:00
Checkpoint
This commit is contained in:
@@ -241,8 +241,6 @@ src/Platform/Hosted/Authorization.ts
|
|||||||
src/Platform/Hosted/DataAccessUtility.ts
|
src/Platform/Hosted/DataAccessUtility.ts
|
||||||
src/Platform/Hosted/ExplorerFactory.ts
|
src/Platform/Hosted/ExplorerFactory.ts
|
||||||
src/Platform/Hosted/Helpers/ConnectionStringParser.test.ts
|
src/Platform/Hosted/Helpers/ConnectionStringParser.test.ts
|
||||||
src/Platform/Hosted/HostedUtils.test.ts
|
|
||||||
src/Platform/Hosted/HostedUtils.ts
|
|
||||||
src/Platform/Hosted/Main.ts
|
src/Platform/Hosted/Main.ts
|
||||||
src/Platform/Hosted/Maint.test.ts
|
src/Platform/Hosted/Maint.test.ts
|
||||||
src/Platform/Hosted/NotificationsClient.ts
|
src/Platform/Hosted/NotificationsClient.ts
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { IContextualMenuProps } from "office-ui-fabric-react/lib/ContextualMenu"
|
|||||||
import { Dropdown, IDropdownProps } from "office-ui-fabric-react/lib/Dropdown";
|
import { Dropdown, IDropdownProps } from "office-ui-fabric-react/lib/Dropdown";
|
||||||
import { useSubscriptions } from "../../../hooks/useSubscriptions";
|
import { useSubscriptions } from "../../../hooks/useSubscriptions";
|
||||||
import { useDatabaseAccounts } from "../../../hooks/useDatabaseAccounts";
|
import { useDatabaseAccounts } from "../../../hooks/useDatabaseAccounts";
|
||||||
|
import { DatabaseAccount } from "../../../Contracts/DataModels";
|
||||||
|
|
||||||
const buttonStyles: IButtonStyles = {
|
const buttonStyles: IButtonStyles = {
|
||||||
root: {
|
root: {
|
||||||
@@ -37,12 +38,27 @@ const buttonStyles: IButtonStyles = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AccountSwitchComponent: React.FunctionComponent<{ armToken: string }> = ({ armToken }) => {
|
const cachedSubscriptionId = localStorage.getItem("cachedSubscriptionId");
|
||||||
|
const cachedDatabaseAccountName = localStorage.getItem("cachedDatabaseAccountName");
|
||||||
|
|
||||||
|
export const AccountSwitchComponent: React.FunctionComponent<{
|
||||||
|
armToken: string;
|
||||||
|
setDatabaseAccount: (account: DatabaseAccount) => void;
|
||||||
|
}> = ({ armToken, setDatabaseAccount }) => {
|
||||||
const subscriptions = useSubscriptions(armToken);
|
const subscriptions = useSubscriptions(armToken);
|
||||||
const cachedSubscriptionId = localStorage.getItem("cachedSubscriptionId");
|
|
||||||
const [selectedSubscriptionId, setSelectedSubscriptionId] = React.useState<string>(cachedSubscriptionId);
|
const [selectedSubscriptionId, setSelectedSubscriptionId] = React.useState<string>(cachedSubscriptionId);
|
||||||
const accounts = useDatabaseAccounts(selectedSubscriptionId, armToken);
|
const accounts = useDatabaseAccounts(selectedSubscriptionId, armToken);
|
||||||
const [selectedAccountName, setSelectedAccoutName] = React.useState<string>();
|
const [selectedAccountName, setSelectedAccoutName] = React.useState<string>(cachedDatabaseAccountName);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (accounts && selectedAccountName) {
|
||||||
|
const account = accounts.find(account => account.name == selectedAccountName);
|
||||||
|
// Only set a new account if one is found
|
||||||
|
if (account) {
|
||||||
|
setDatabaseAccount(account);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [accounts, selectedAccountName]);
|
||||||
|
|
||||||
const menuProps: IContextualMenuProps = {
|
const menuProps: IContextualMenuProps = {
|
||||||
directionalHintFixed: true,
|
directionalHintFixed: true,
|
||||||
@@ -85,30 +101,28 @@ export const AccountSwitchComponent: React.FunctionComponent<{ armToken: string
|
|||||||
{
|
{
|
||||||
key: "switchAccount",
|
key: "switchAccount",
|
||||||
onRender: (item, dismissMenu) => {
|
onRender: (item, dismissMenu) => {
|
||||||
const isLoadingAccounts = false;
|
// const placeHolderText = isLoadingAccounts
|
||||||
|
// ? "Loading Cosmos DB accounts"
|
||||||
const options = accounts.map(account => ({
|
// : !options || !options.length
|
||||||
key: account.name,
|
// ? "No Cosmos DB accounts found"
|
||||||
text: account.name,
|
// : "Select Cosmos DB account from list";
|
||||||
data: account
|
|
||||||
}));
|
|
||||||
|
|
||||||
const placeHolderText = isLoadingAccounts
|
|
||||||
? "Loading Cosmos DB accounts"
|
|
||||||
: !options || !options.length
|
|
||||||
? "No Cosmos DB accounts found"
|
|
||||||
: "Select Cosmos DB account from list";
|
|
||||||
|
|
||||||
const dropdownProps: IDropdownProps = {
|
const dropdownProps: IDropdownProps = {
|
||||||
label: "Cosmos DB Account Name",
|
label: "Cosmos DB Account Name",
|
||||||
className: "accountSwitchAccountDropdown",
|
className: "accountSwitchAccountDropdown",
|
||||||
options,
|
options: accounts.map(account => ({
|
||||||
|
key: account.name,
|
||||||
|
text: account.name,
|
||||||
|
data: account
|
||||||
|
})),
|
||||||
onChange: (event, option) => {
|
onChange: (event, option) => {
|
||||||
|
const accountName = String(option.key);
|
||||||
setSelectedAccoutName(String(option.key));
|
setSelectedAccoutName(String(option.key));
|
||||||
|
localStorage.setItem("cachedDatabaseAccountName", accountName);
|
||||||
dismissMenu();
|
dismissMenu();
|
||||||
},
|
},
|
||||||
defaultSelectedKey: selectedAccountName,
|
defaultSelectedKey: selectedAccountName,
|
||||||
placeholder: placeHolderText,
|
placeholder: "No Cosmos DB accounts found",
|
||||||
styles: {
|
styles: {
|
||||||
callout: "accountSwitchAccountDropdownMenu"
|
callout: "accountSwitchAccountDropdownMenu"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1843,7 +1843,7 @@ export default class Explorer {
|
|||||||
if (inputs != null) {
|
if (inputs != null) {
|
||||||
// In development mode, save the iframe message from the portal in session storage.
|
// In development mode, save the iframe message from the portal in session storage.
|
||||||
// This allows webpack hot reload to funciton properly
|
// This allows webpack hot reload to funciton properly
|
||||||
if (process.env.NODE_ENV === "development") {
|
if (process.env.NODE_ENV === "development" && configContext.platform === Platform.Portal) {
|
||||||
sessionStorage.setItem("portalDataExplorerInitMessage", JSON.stringify(inputs));
|
sessionStorage.setItem("portalDataExplorerInitMessage", JSON.stringify(inputs));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import { useDirectories } from "./hooks/useDirectories";
|
|||||||
import * as Msal from "msal";
|
import * as Msal from "msal";
|
||||||
import { configContext } from "./ConfigContext";
|
import { configContext } from "./ConfigContext";
|
||||||
import { HttpHeaders } from "./Common/Constants";
|
import { HttpHeaders } from "./Common/Constants";
|
||||||
import { GenerateTokenResponse } from "./Contracts/DataModels";
|
import { GenerateTokenResponse, DatabaseAccount } from "./Contracts/DataModels";
|
||||||
import { AuthType } from "./AuthType";
|
import { AuthType } from "./AuthType";
|
||||||
|
|
||||||
initializeIcons();
|
initializeIcons();
|
||||||
@@ -43,6 +43,12 @@ const msal = new Msal.UserAgentApplication({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
interface HostedExplorerChildFrame extends Window {
|
||||||
|
authType: AuthType;
|
||||||
|
databaseAccount: DatabaseAccount;
|
||||||
|
authorizationToken: string;
|
||||||
|
}
|
||||||
|
|
||||||
const cachedAccount = msal.getAllAccounts()?.[0];
|
const cachedAccount = msal.getAllAccounts()?.[0];
|
||||||
const cachedTenantId = localStorage.getItem("cachedTenantId");
|
const cachedTenantId = localStorage.getItem("cachedTenantId");
|
||||||
|
|
||||||
@@ -65,6 +71,9 @@ const App: React.FunctionComponent = () => {
|
|||||||
const [graphToken, setGraphToken] = React.useState<string>();
|
const [graphToken, setGraphToken] = React.useState<string>();
|
||||||
const [armToken, setArmToken] = React.useState<string>();
|
const [armToken, setArmToken] = React.useState<string>();
|
||||||
const [connectionString, setConnectionString] = React.useState<string>("");
|
const [connectionString, setConnectionString] = React.useState<string>("");
|
||||||
|
const [databaseAccount, setDatabaseAccount] = React.useState<DatabaseAccount>();
|
||||||
|
|
||||||
|
const ref = React.useRef<HTMLIFrameElement>();
|
||||||
|
|
||||||
const login = React.useCallback(async () => {
|
const login = React.useCallback(async () => {
|
||||||
const response = await msal.loginPopup();
|
const response = await msal.loginPopup();
|
||||||
@@ -96,6 +105,21 @@ const App: React.FunctionComponent = () => {
|
|||||||
}
|
}
|
||||||
}, [account, tenantId]);
|
}, [account, tenantId]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
// If ref.current is undefined no iframe has been rendered
|
||||||
|
if (ref.current) {
|
||||||
|
const frameWindow = ref.current.contentWindow as HostedExplorerChildFrame;
|
||||||
|
frameWindow.authType = AuthType.AAD;
|
||||||
|
frameWindow.databaseAccount = databaseAccount;
|
||||||
|
frameWindow.authorizationToken = armToken;
|
||||||
|
// const frameWindow = ref.current.contentWindow;
|
||||||
|
// frameWindow.authType = AuthType.EncryptedToken;
|
||||||
|
// frameWindow.encryptedToken = encryptedToken;
|
||||||
|
// frameWindow.encryptedTokenMetadata = encryptedTokenMetadata;
|
||||||
|
// frameWindow.parsedConnectionString = "foo";
|
||||||
|
}
|
||||||
|
}, [ref, encryptedToken, encryptedTokenMetadata, isLoggedIn, databaseAccount]);
|
||||||
|
|
||||||
const photo = useGraphPhoto(graphToken);
|
const photo = useGraphPhoto(graphToken);
|
||||||
const directories = useDirectories(armToken);
|
const directories = useDirectories(armToken);
|
||||||
|
|
||||||
@@ -118,7 +142,7 @@ const App: React.FunctionComponent = () => {
|
|||||||
)}
|
)}
|
||||||
{isLoggedIn && (
|
{isLoggedIn && (
|
||||||
<span className="accountSwitchComponentContainer">
|
<span className="accountSwitchComponentContainer">
|
||||||
<AccountSwitchComponent armToken={armToken} />
|
<AccountSwitchComponent armToken={armToken} setDatabaseAccount={setDatabaseAccount} />
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{!isLoggedIn && encryptedTokenMetadata?.accountName && (
|
{!isLoggedIn && encryptedTokenMetadata?.accountName && (
|
||||||
@@ -202,26 +226,22 @@ const App: React.FunctionComponent = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
{encryptedTokenMetadata && !isLoggedIn && (
|
{databaseAccount && (
|
||||||
|
// Ideally we would import and render data explorer like any other React component, however
|
||||||
|
// because it still has a significant amount of Knockout code, this would lead to memory leaks.
|
||||||
|
// Knockout does not have a way to tear down all of its binding and listeners with a single method.
|
||||||
|
// It's possible this can be changed once all knockout code has been removed.
|
||||||
<iframe
|
<iframe
|
||||||
|
// Setting key is needed so React will re-render this element on any account change
|
||||||
|
key={databaseAccount.id}
|
||||||
|
ref={ref}
|
||||||
id="explorerMenu"
|
id="explorerMenu"
|
||||||
name="explorer"
|
name="explorer"
|
||||||
className="iframe"
|
className="iframe"
|
||||||
title="explorer"
|
title="explorer"
|
||||||
src={`explorer.html?v=1.0.1&platform=Hosted&authType=${AuthType.EncryptedToken}&key=${encodeURIComponent(
|
src="explorer.html?v=1.0.1&platform=Hosted"
|
||||||
encryptedToken
|
|
||||||
)}&metadata=${JSON.stringify(encryptedTokenMetadata)}`}
|
|
||||||
></iframe>
|
></iframe>
|
||||||
)}
|
)}
|
||||||
{/* {!encryptedTokenMetadata && isLoggedIn && (
|
|
||||||
<iframe
|
|
||||||
id="explorerMenu"
|
|
||||||
name="explorer"
|
|
||||||
className="iframe"
|
|
||||||
title="explorer"
|
|
||||||
src={`explorer.html?v=1.0.1&platform=Hosted&authType=${AuthType.AAD}`}
|
|
||||||
></iframe>
|
|
||||||
)} */}
|
|
||||||
{!isLoggedIn && !encryptedTokenMetadata && (
|
{!isLoggedIn && !encryptedTokenMetadata && (
|
||||||
<div id="connectExplorer" className="connectExplorerContainer" style={{ display: "flex" }}>
|
<div id="connectExplorer" className="connectExplorerContainer" style={{ display: "flex" }}>
|
||||||
<div className="connectExplorerFormContainer">
|
<div className="connectExplorerFormContainer">
|
||||||
|
|||||||
44
src/Main.tsx
44
src/Main.tsx
@@ -60,7 +60,7 @@ import { AuthType } from "./AuthType";
|
|||||||
|
|
||||||
import { initializeIcons } from "office-ui-fabric-react/lib/Icons";
|
import { initializeIcons } from "office-ui-fabric-react/lib/Icons";
|
||||||
import { applyExplorerBindings } from "./applyExplorerBindings";
|
import { applyExplorerBindings } from "./applyExplorerBindings";
|
||||||
import { initializeConfiguration, Platform } from "./ConfigContext";
|
import { configContext, initializeConfiguration, Platform } from "./ConfigContext";
|
||||||
import Explorer from "./Explorer/Explorer";
|
import Explorer from "./Explorer/Explorer";
|
||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
@@ -71,6 +71,10 @@ import refreshImg from "../images/refresh-cosmos.svg";
|
|||||||
import arrowLeftImg from "../images/imgarrowlefticon.svg";
|
import arrowLeftImg from "../images/imgarrowlefticon.svg";
|
||||||
import { KOCommentEnd, KOCommentIfStart } from "./koComment";
|
import { KOCommentEnd, KOCommentIfStart } from "./koComment";
|
||||||
import { AccountKind, DefaultAccountExperience, TagNames } from "./Common/Constants";
|
import { AccountKind, DefaultAccountExperience, TagNames } from "./Common/Constants";
|
||||||
|
import { updateUserContext } from "./UserContext";
|
||||||
|
import AuthHeadersUtil from "./Platform/Hosted/Authorization";
|
||||||
|
import { CollectionCreation } from "./Shared/Constants";
|
||||||
|
import { extractFeatures } from "./Platform/Hosted/extractFeatures";
|
||||||
|
|
||||||
// TODO: Encapsulate and reuse all global variables as environment variables
|
// TODO: Encapsulate and reuse all global variables as environment variables
|
||||||
window.authType = AuthType.AAD;
|
window.authType = AuthType.AAD;
|
||||||
@@ -97,7 +101,39 @@ const App: React.FunctionComponent = () => {
|
|||||||
initializeConfiguration().then(config => {
|
initializeConfiguration().then(config => {
|
||||||
let explorer: Explorer;
|
let explorer: Explorer;
|
||||||
if (config.platform === Platform.Hosted) {
|
if (config.platform === Platform.Hosted) {
|
||||||
explorer = Hosted.initializeExplorer();
|
const authType: AuthType = window.authType;
|
||||||
|
explorer = new Explorer();
|
||||||
|
if (window.authType === AuthType.EncryptedToken) {
|
||||||
|
updateUserContext({
|
||||||
|
accessToken: window.encryptedToken
|
||||||
|
});
|
||||||
|
Hosted.initDataExplorerFrameInputs(explorer);
|
||||||
|
} else if (window.authType === AuthType.AAD) {
|
||||||
|
const account = window.databaseAccount;
|
||||||
|
const serverId = AuthHeadersUtil.serverId;
|
||||||
|
const accountResourceId = account.id;
|
||||||
|
const subscriptionId = accountResourceId && accountResourceId.split("subscriptions/")[1].split("/")[0];
|
||||||
|
const resourceGroup = accountResourceId && accountResourceId.split("resourceGroups/")[1].split("/")[0];
|
||||||
|
const inputs: DataExplorerInputsFrame = {
|
||||||
|
databaseAccount: account,
|
||||||
|
subscriptionId,
|
||||||
|
resourceGroup,
|
||||||
|
masterKey: "",
|
||||||
|
hasWriteAccess: true, //TODO: 425017 - support read access
|
||||||
|
authorizationToken: `Bearer ${window.authorizationToken}`,
|
||||||
|
features: extractFeatures(),
|
||||||
|
csmEndpoint: undefined,
|
||||||
|
dnsSuffix: undefined,
|
||||||
|
serverId: serverId,
|
||||||
|
extensionEndpoint: configContext.BACKEND_ENDPOINT,
|
||||||
|
subscriptionType: CollectionCreation.DefaultSubscriptionType,
|
||||||
|
quotaId: undefined,
|
||||||
|
addCollectionDefaultFlight: explorer.flight(),
|
||||||
|
isTryCosmosDBSubscription: explorer.isTryCosmosDBSubscription()
|
||||||
|
};
|
||||||
|
explorer.initDataExplorerWithFrameInputs(inputs);
|
||||||
|
explorer.isAccountReady(true);
|
||||||
|
}
|
||||||
} else if (config.platform === Platform.Emulator) {
|
} else if (config.platform === Platform.Emulator) {
|
||||||
window.authType = AuthType.MasterKey;
|
window.authType = AuthType.MasterKey;
|
||||||
const explorer = new Explorer();
|
const explorer = new Explorer();
|
||||||
@@ -118,7 +154,7 @@ const App: React.FunctionComponent = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener("message", explorer.handleMessage.bind(explorer), false);
|
window.addEventListener("message", message => explorer.handleMessage(message), false);
|
||||||
}
|
}
|
||||||
applyExplorerBindings(explorer);
|
applyExplorerBindings(explorer);
|
||||||
});
|
});
|
||||||
@@ -177,7 +213,7 @@ const App: React.FunctionComponent = () => {
|
|||||||
aria-label="Share url link"
|
aria-label="Share url link"
|
||||||
className="shareLink"
|
className="shareLink"
|
||||||
type="text"
|
type="text"
|
||||||
read-only
|
read-only={true}
|
||||||
data-bind="value: shareAccessUrl"
|
data-bind="value: shareAccessUrl"
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { AccessInputMetadata } from "../../Contracts/DataModels";
|
import { AccessInputMetadata } from "../../Contracts/DataModels";
|
||||||
import { HostedUtils } from "./HostedUtils";
|
import { getDatabaseAccountPropertiesFromMetadata } from "./HostedUtils";
|
||||||
|
|
||||||
describe("getDatabaseAccountPropertiesFromMetadata", () => {
|
describe("getDatabaseAccountPropertiesFromMetadata", () => {
|
||||||
it("should only return an object with the mongoEndpoint key if the apiKind is mongoCompute (5)", () => {
|
it("should only return an object with the mongoEndpoint key if the apiKind is mongoCompute (5)", () => {
|
||||||
let mongoComputeAccount: AccessInputMetadata = {
|
const mongoComputeAccount: AccessInputMetadata = {
|
||||||
accountName: "compute-batch2",
|
accountName: "compute-batch2",
|
||||||
apiEndpoint: "compute-batch2.mongo.cosmos.azure.com:10255",
|
apiEndpoint: "compute-batch2.mongo.cosmos.azure.com:10255",
|
||||||
apiKind: 5,
|
apiKind: 5,
|
||||||
@@ -11,21 +11,21 @@ describe("getDatabaseAccountPropertiesFromMetadata", () => {
|
|||||||
expiryTimestamp: "1234",
|
expiryTimestamp: "1234",
|
||||||
mongoEndpoint: "https://compute-batch2.mongo.cosmos.azure.com:443/"
|
mongoEndpoint: "https://compute-batch2.mongo.cosmos.azure.com:443/"
|
||||||
};
|
};
|
||||||
expect(HostedUtils.getDatabaseAccountPropertiesFromMetadata(mongoComputeAccount)).toEqual({
|
expect(getDatabaseAccountPropertiesFromMetadata(mongoComputeAccount)).toEqual({
|
||||||
mongoEndpoint: mongoComputeAccount.mongoEndpoint,
|
mongoEndpoint: mongoComputeAccount.mongoEndpoint,
|
||||||
documentEndpoint: mongoComputeAccount.documentEndpoint
|
documentEndpoint: mongoComputeAccount.documentEndpoint
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not return an object with the mongoEndpoint key if the apiKind is mongo (1)", () => {
|
it("should not return an object with the mongoEndpoint key if the apiKind is mongo (1)", () => {
|
||||||
let mongoAccount: AccessInputMetadata = {
|
const mongoAccount: AccessInputMetadata = {
|
||||||
accountName: "compute-batch2",
|
accountName: "compute-batch2",
|
||||||
apiEndpoint: "compute-batch2.mongo.cosmos.azure.com:10255",
|
apiEndpoint: "compute-batch2.mongo.cosmos.azure.com:10255",
|
||||||
apiKind: 1,
|
apiKind: 1,
|
||||||
documentEndpoint: "https://compute-batch2.documents.azure.com:443/",
|
documentEndpoint: "https://compute-batch2.documents.azure.com:443/",
|
||||||
expiryTimestamp: "1234"
|
expiryTimestamp: "1234"
|
||||||
};
|
};
|
||||||
expect(HostedUtils.getDatabaseAccountPropertiesFromMetadata(mongoAccount)).toEqual({
|
expect(getDatabaseAccountPropertiesFromMetadata(mongoAccount)).toEqual({
|
||||||
documentEndpoint: mongoAccount.documentEndpoint
|
documentEndpoint: mongoAccount.documentEndpoint
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,8 +3,7 @@ import * as DataModels from "../../Contracts/DataModels";
|
|||||||
import { AccessInputMetadata } from "../../Contracts/DataModels";
|
import { AccessInputMetadata } from "../../Contracts/DataModels";
|
||||||
import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility";
|
import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility";
|
||||||
|
|
||||||
export class HostedUtils {
|
export function getDatabaseAccountPropertiesFromMetadata(metadata: AccessInputMetadata): unknown {
|
||||||
static getDatabaseAccountPropertiesFromMetadata(metadata: AccessInputMetadata): any {
|
|
||||||
let properties = { documentEndpoint: metadata.documentEndpoint };
|
let properties = { documentEndpoint: metadata.documentEndpoint };
|
||||||
const apiExperience: string = DefaultExperienceUtility.getDefaultExperienceFromApiKind(metadata.apiKind);
|
const apiExperience: string = DefaultExperienceUtility.getDefaultExperienceFromApiKind(metadata.apiKind);
|
||||||
|
|
||||||
@@ -31,5 +30,4 @@ export class HostedUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return properties;
|
return properties;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,60 +1,17 @@
|
|||||||
import * as Constants from "../../Common/Constants";
|
|
||||||
import AuthHeadersUtil from "./Authorization";
|
|
||||||
import Q from "q";
|
|
||||||
import {
|
|
||||||
AccessInputMetadata,
|
|
||||||
ApiKind,
|
|
||||||
DatabaseAccount,
|
|
||||||
GenerateTokenResponse,
|
|
||||||
resourceTokenConnectionStringProperties
|
|
||||||
} from "../../Contracts/DataModels";
|
|
||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
import { CollectionCreation } from "../../Shared/Constants";
|
import * as Constants from "../../Common/Constants";
|
||||||
import { DataExplorerInputsFrame } from "../../Contracts/ViewModels";
|
|
||||||
import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility";
|
|
||||||
import { HostedUtils } from "./HostedUtils";
|
|
||||||
import { sendMessage } from "../../Common/MessageHandler";
|
|
||||||
import { SessionStorageUtility, StorageKey } from "../../Shared/StorageUtility";
|
|
||||||
import { SubscriptionUtilMappings } from "../../Shared/Constants";
|
|
||||||
import "../../Explorer/Tables/DataTable/DataTableBindingManager";
|
|
||||||
import Explorer from "../../Explorer/Explorer";
|
|
||||||
import { updateUserContext } from "../../UserContext";
|
|
||||||
import { configContext } from "../../ConfigContext";
|
import { configContext } from "../../ConfigContext";
|
||||||
import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
|
import { ApiKind, DatabaseAccount, resourceTokenConnectionStringProperties } from "../../Contracts/DataModels";
|
||||||
|
import { DataExplorerInputsFrame } from "../../Contracts/ViewModels";
|
||||||
|
import Explorer from "../../Explorer/Explorer";
|
||||||
|
import "../../Explorer/Tables/DataTable/DataTableBindingManager";
|
||||||
|
import { CollectionCreation, SubscriptionUtilMappings } from "../../Shared/Constants";
|
||||||
|
import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility";
|
||||||
|
import AuthHeadersUtil from "./Authorization";
|
||||||
import { extractFeatures } from "./extractFeatures";
|
import { extractFeatures } from "./extractFeatures";
|
||||||
|
import { getDatabaseAccountPropertiesFromMetadata } from "./HostedUtils";
|
||||||
|
|
||||||
export default class Main {
|
export default class Main {
|
||||||
private static _databaseAccountId: string;
|
|
||||||
private static _encryptedToken: string;
|
|
||||||
private static _accessInputMetadata: AccessInputMetadata;
|
|
||||||
|
|
||||||
public static initializeExplorer(): Explorer {
|
|
||||||
const params = new URLSearchParams(window.location.search);
|
|
||||||
let authType: string = params && params.get("authType");
|
|
||||||
|
|
||||||
// Encrypted token flow
|
|
||||||
if (params && params.has("key")) {
|
|
||||||
Main._encryptedToken = encodeURIComponent(params.get("key"));
|
|
||||||
Main._accessInputMetadata = JSON.parse(params.get("metadata"));
|
|
||||||
authType = AuthType.EncryptedToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
const explorer = new Explorer();
|
|
||||||
// workaround to resolve cyclic refs with view // TODO. Is this even needed anymore?
|
|
||||||
explorer.renewExplorerShareAccess = Main.renewExplorerAccess;
|
|
||||||
window.addEventListener("message", explorer.handleMessage.bind(explorer), false);
|
|
||||||
if (authType === AuthType.EncryptedToken) {
|
|
||||||
updateUserContext({
|
|
||||||
accessToken: Main._encryptedToken
|
|
||||||
});
|
|
||||||
Main._initDataExplorerFrameInputs(explorer);
|
|
||||||
} else if (authType === AuthType.AAD) {
|
|
||||||
} else {
|
|
||||||
Main._initDataExplorerFrameInputs(explorer);
|
|
||||||
}
|
|
||||||
return explorer;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static parseResourceTokenConnectionString(connectionString: string): resourceTokenConnectionStringProperties {
|
public static parseResourceTokenConnectionString(connectionString: string): resourceTokenConnectionStringProperties {
|
||||||
let accountEndpoint: string;
|
let accountEndpoint: string;
|
||||||
let collectionId: string;
|
let collectionId: string;
|
||||||
@@ -87,75 +44,7 @@ export default class Main {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static renewExplorerAccess = (explorer: Explorer, connectionString: string): Q.Promise<void> => {
|
public static initDataExplorerFrameInputs(
|
||||||
if (!connectionString) {
|
|
||||||
console.error("Missing or invalid connection string input");
|
|
||||||
Q.reject("Missing or invalid connection string input");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Main._isResourceToken(connectionString)) {
|
|
||||||
return Main._renewExplorerAccessWithResourceToken(explorer, connectionString);
|
|
||||||
}
|
|
||||||
|
|
||||||
const deferred: Q.Deferred<void> = Q.defer<void>();
|
|
||||||
AuthHeadersUtil.generateUnauthenticatedEncryptedTokenForConnectionString(connectionString).then(
|
|
||||||
(encryptedToken: GenerateTokenResponse) => {
|
|
||||||
if (!encryptedToken || !encryptedToken.readWrite) {
|
|
||||||
deferred.reject("Encrypted token is empty or undefined");
|
|
||||||
}
|
|
||||||
|
|
||||||
Main._encryptedToken = encryptedToken.readWrite;
|
|
||||||
window.authType = AuthType.EncryptedToken;
|
|
||||||
|
|
||||||
updateUserContext({
|
|
||||||
accessToken: Main._encryptedToken
|
|
||||||
});
|
|
||||||
Main._getAccessInputMetadata(Main._encryptedToken).then(
|
|
||||||
() => {
|
|
||||||
if (explorer.isConnectExplorerVisible()) {
|
|
||||||
explorer.notificationConsoleData([]);
|
|
||||||
explorer.hideConnectExplorerForm();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Main._accessInputMetadata.apiKind != ApiKind.Graph) {
|
|
||||||
// do not save encrypted token for graphs because we cannot extract master key in the client
|
|
||||||
SessionStorageUtility.setEntryString(StorageKey.EncryptedKeyToken, Main._encryptedToken);
|
|
||||||
window.parent &&
|
|
||||||
window.parent.history.replaceState(
|
|
||||||
{ encryptedToken: encryptedToken },
|
|
||||||
"",
|
|
||||||
`?key=${Main._encryptedToken}${(window.parent && window.parent.location.hash) || ""}`
|
|
||||||
); // replace query params if any
|
|
||||||
} else {
|
|
||||||
SessionStorageUtility.removeEntry(StorageKey.EncryptedKeyToken);
|
|
||||||
window.parent &&
|
|
||||||
window.parent.history.replaceState(
|
|
||||||
{ encryptedToken: encryptedToken },
|
|
||||||
"",
|
|
||||||
`?${(window.parent && window.parent.location.hash) || ""}`
|
|
||||||
); // replace query params if any
|
|
||||||
}
|
|
||||||
|
|
||||||
const masterKey: string = Main._getMasterKeyFromConnectionString(connectionString);
|
|
||||||
Main._setExplorerReady(explorer, masterKey);
|
|
||||||
|
|
||||||
deferred.resolve();
|
|
||||||
},
|
|
||||||
(error: any) => {
|
|
||||||
console.error(error);
|
|
||||||
deferred.reject(error);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
},
|
|
||||||
(error: any) => {
|
|
||||||
deferred.reject(`Failed to generate encrypted token: ${getErrorMessage(error)}`);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return deferred.promise.timeout(Constants.ClientDefaults.requestTimeoutMs);
|
|
||||||
};
|
|
||||||
|
|
||||||
private static _initDataExplorerFrameInputs(
|
|
||||||
explorer: Explorer,
|
explorer: Explorer,
|
||||||
masterKey?: string /* master key extracted from connection string if available */,
|
masterKey?: string /* master key extracted from connection string if available */,
|
||||||
account?: DatabaseAccount,
|
account?: DatabaseAccount,
|
||||||
@@ -187,7 +76,7 @@ export default class Main {
|
|||||||
id: Main._databaseAccountId,
|
id: Main._databaseAccountId,
|
||||||
name: Main._accessInputMetadata.accountName,
|
name: Main._accessInputMetadata.accountName,
|
||||||
kind: this._getDatabaseAccountKindFromExperience(apiExperience),
|
kind: this._getDatabaseAccountKindFromExperience(apiExperience),
|
||||||
properties: HostedUtils.getDatabaseAccountPropertiesFromMetadata(Main._accessInputMetadata),
|
properties: getDatabaseAccountPropertiesFromMetadata(Main._accessInputMetadata),
|
||||||
tags: { defaultExperience: apiExperience }
|
tags: { defaultExperience: apiExperience }
|
||||||
},
|
},
|
||||||
subscriptionId,
|
subscriptionId,
|
||||||
@@ -237,7 +126,7 @@ export default class Main {
|
|||||||
id: Main._databaseAccountId,
|
id: Main._databaseAccountId,
|
||||||
name: Main._accessInputMetadata.accountName,
|
name: Main._accessInputMetadata.accountName,
|
||||||
kind: this._getDatabaseAccountKindFromExperience(apiExperience),
|
kind: this._getDatabaseAccountKindFromExperience(apiExperience),
|
||||||
properties: HostedUtils.getDatabaseAccountPropertiesFromMetadata(Main._accessInputMetadata),
|
properties: getDatabaseAccountPropertiesFromMetadata(Main._accessInputMetadata),
|
||||||
tags: { defaultExperience: apiExperience }
|
tags: { defaultExperience: apiExperience }
|
||||||
},
|
},
|
||||||
subscriptionId,
|
subscriptionId,
|
||||||
@@ -273,11 +162,6 @@ export default class Main {
|
|||||||
return Constants.AccountKind.GlobalDocumentDB;
|
return Constants.AccountKind.GlobalDocumentDB;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async _getAccessInputMetadata(accessInput: string): Promise<void> {
|
|
||||||
const metadata = await AuthHeadersUtil.getAccessInputMetadata(accessInput);
|
|
||||||
Main._accessInputMetadata = metadata;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static _getMasterKeyFromConnectionString(connectionString: string): string {
|
private static _getMasterKeyFromConnectionString(connectionString: string): string {
|
||||||
if (!connectionString || Main._accessInputMetadata == null || Main._accessInputMetadata.apiKind !== ApiKind.Graph) {
|
if (!connectionString || Main._accessInputMetadata == null || Main._accessInputMetadata.apiKind !== ApiKind.Graph) {
|
||||||
// client only needs master key for Graph API
|
// client only needs master key for Graph API
|
||||||
@@ -291,69 +175,4 @@ export default class Main {
|
|||||||
private static _isResourceToken(connectionString: string): boolean {
|
private static _isResourceToken(connectionString: string): boolean {
|
||||||
return connectionString && connectionString.includes("type=resource");
|
return connectionString && connectionString.includes("type=resource");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static _renewExplorerAccessWithResourceToken = (
|
|
||||||
explorer: Explorer,
|
|
||||||
connectionString: string
|
|
||||||
): Q.Promise<void> => {
|
|
||||||
window.authType = AuthType.ResourceToken;
|
|
||||||
|
|
||||||
const properties: resourceTokenConnectionStringProperties = Main.parseResourceTokenConnectionString(
|
|
||||||
connectionString
|
|
||||||
);
|
|
||||||
if (
|
|
||||||
!properties.accountEndpoint ||
|
|
||||||
!properties.resourceToken ||
|
|
||||||
!properties.databaseId ||
|
|
||||||
!properties.collectionId
|
|
||||||
) {
|
|
||||||
console.error("Invalid connection string input");
|
|
||||||
Q.reject("Invalid connection string input");
|
|
||||||
}
|
|
||||||
updateUserContext({
|
|
||||||
resourceToken: properties.resourceToken,
|
|
||||||
endpoint: properties.accountEndpoint
|
|
||||||
});
|
|
||||||
explorer.resourceTokenDatabaseId(properties.databaseId);
|
|
||||||
explorer.resourceTokenCollectionId(properties.collectionId);
|
|
||||||
if (properties.partitionKey) {
|
|
||||||
explorer.resourceTokenPartitionKey(properties.partitionKey);
|
|
||||||
}
|
|
||||||
Main._accessInputMetadata = Main._getAccessInputMetadataFromAccountEndpoint(properties.accountEndpoint);
|
|
||||||
|
|
||||||
if (explorer.isConnectExplorerVisible()) {
|
|
||||||
explorer.notificationConsoleData([]);
|
|
||||||
explorer.hideConnectExplorerForm();
|
|
||||||
}
|
|
||||||
|
|
||||||
Main._setExplorerReady(explorer);
|
|
||||||
return Q.resolve();
|
|
||||||
};
|
|
||||||
|
|
||||||
private static _getAccessInputMetadataFromAccountEndpoint = (accountEndpoint: string): AccessInputMetadata => {
|
|
||||||
const documentEndpoint: string = accountEndpoint;
|
|
||||||
const result: RegExpMatchArray = accountEndpoint.match("https://([^\\.]+)\\..+");
|
|
||||||
const accountName: string = result && result[1];
|
|
||||||
const apiEndpoint: string = accountEndpoint.substring(8);
|
|
||||||
const apiKind: number = ApiKind.SQL;
|
|
||||||
|
|
||||||
return {
|
|
||||||
accountName,
|
|
||||||
apiEndpoint,
|
|
||||||
apiKind,
|
|
||||||
documentEndpoint,
|
|
||||||
expiryTimestamp: ""
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
private static _setExplorerReady(
|
|
||||||
explorer: Explorer,
|
|
||||||
masterKey?: string,
|
|
||||||
account?: DatabaseAccount,
|
|
||||||
authorizationToken?: string
|
|
||||||
) {
|
|
||||||
Main._initDataExplorerFrameInputs(explorer, masterKey, account, authorizationToken);
|
|
||||||
explorer.isAccountReady.valueHasMutated();
|
|
||||||
sendMessage("ready");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user