mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-20 01:11:25 +00:00
Migrated Hosted Explorer to React (#360)
Co-authored-by: Victor Meng <vimeng@microsoft.com> Co-authored-by: Steve Faulkner <stfaul@microsoft.com>
This commit is contained in:
101
src/hooks/useAADAuth.ts
Normal file
101
src/hooks/useAADAuth.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import * as React from "react";
|
||||
import { useBoolean } from "@uifabric/react-hooks";
|
||||
import { UserAgentApplication, Account, Configuration } from "msal";
|
||||
|
||||
const config: Configuration = {
|
||||
cache: {
|
||||
cacheLocation: "localStorage"
|
||||
},
|
||||
auth: {
|
||||
authority: "https://login.microsoftonline.com/common",
|
||||
clientId: "203f1145-856a-4232-83d4-a43568fba23d"
|
||||
}
|
||||
};
|
||||
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
config.auth.redirectUri = "https://dataexplorer-dev.azurewebsites.net";
|
||||
}
|
||||
|
||||
const msal = new UserAgentApplication(config);
|
||||
|
||||
const cachedAccount = msal.getAllAccounts()?.[0];
|
||||
const cachedTenantId = localStorage.getItem("cachedTenantId");
|
||||
|
||||
interface ReturnType {
|
||||
isLoggedIn: boolean;
|
||||
graphToken: string;
|
||||
armToken: string;
|
||||
login: () => void;
|
||||
logout: () => void;
|
||||
tenantId: string;
|
||||
account: Account;
|
||||
switchTenant: (tenantId: string) => void;
|
||||
}
|
||||
|
||||
export function useAADAuth(): ReturnType {
|
||||
const [isLoggedIn, { setTrue: setLoggedIn, setFalse: setLoggedOut }] = useBoolean(
|
||||
Boolean(cachedAccount && cachedTenantId) || false
|
||||
);
|
||||
const [account, setAccount] = React.useState<Account>(cachedAccount);
|
||||
const [tenantId, setTenantId] = React.useState<string>(cachedTenantId);
|
||||
const [graphToken, setGraphToken] = React.useState<string>();
|
||||
const [armToken, setArmToken] = React.useState<string>();
|
||||
|
||||
const login = React.useCallback(async () => {
|
||||
const response = await msal.loginPopup();
|
||||
setLoggedIn();
|
||||
setAccount(response.account);
|
||||
setTenantId(response.tenantId);
|
||||
localStorage.setItem("cachedTenantId", response.tenantId);
|
||||
}, []);
|
||||
|
||||
const logout = React.useCallback(() => {
|
||||
setLoggedOut();
|
||||
localStorage.removeItem("cachedTenantId");
|
||||
msal.logout();
|
||||
}, []);
|
||||
|
||||
const switchTenant = React.useCallback(
|
||||
async id => {
|
||||
const response = await msal.loginPopup({
|
||||
authority: `https://login.microsoftonline.com/${id}`
|
||||
});
|
||||
setTenantId(response.tenantId);
|
||||
setAccount(response.account);
|
||||
},
|
||||
[account, tenantId]
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (account && tenantId) {
|
||||
Promise.all([
|
||||
msal.acquireTokenSilent({
|
||||
// There is a bug in MSALv1 that requires us to refresh the token. Their internal cache is not respecting authority
|
||||
forceRefresh: true,
|
||||
authority: `https://login.microsoftonline.com/${tenantId}`,
|
||||
scopes: ["https://graph.windows.net//.default"]
|
||||
}),
|
||||
msal.acquireTokenSilent({
|
||||
// There is a bug in MSALv1 that requires us to refresh the token. Their internal cache is not respecting authority
|
||||
forceRefresh: true,
|
||||
authority: `https://login.microsoftonline.com/${tenantId}`,
|
||||
scopes: ["https://management.azure.com//.default"]
|
||||
})
|
||||
]).then(([graphTokenResponse, armTokenResponse]) => {
|
||||
setGraphToken(graphTokenResponse.accessToken);
|
||||
setArmToken(armTokenResponse.accessToken);
|
||||
});
|
||||
}
|
||||
}, [account, tenantId]);
|
||||
|
||||
return {
|
||||
account,
|
||||
tenantId,
|
||||
isLoggedIn,
|
||||
graphToken,
|
||||
armToken,
|
||||
login,
|
||||
logout,
|
||||
switchTenant
|
||||
};
|
||||
}
|
||||
38
src/hooks/useDatabaseAccounts.tsx
Normal file
38
src/hooks/useDatabaseAccounts.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import useSWR from "swr";
|
||||
import { DatabaseAccount } from "../Contracts/DataModels";
|
||||
|
||||
interface AccountListResult {
|
||||
nextLink: string;
|
||||
value: DatabaseAccount[];
|
||||
}
|
||||
|
||||
export async function fetchDatabaseAccounts(subscriptionId: string, accessToken: string): Promise<DatabaseAccount[]> {
|
||||
const headers = new Headers();
|
||||
const bearer = `Bearer ${accessToken}`;
|
||||
|
||||
headers.append("Authorization", bearer);
|
||||
|
||||
let accounts: Array<DatabaseAccount> = [];
|
||||
|
||||
let nextLink = `https://management.azure.com/subscriptions/${subscriptionId}/providers/Microsoft.DocumentDB/databaseAccounts?api-version=2020-06-01-preview`;
|
||||
|
||||
while (nextLink) {
|
||||
const response: Response = await fetch(nextLink, { headers });
|
||||
const result: AccountListResult =
|
||||
response.status === 204 || response.status === 304 ? undefined : await response.json();
|
||||
if (!response.ok) {
|
||||
throw result;
|
||||
}
|
||||
nextLink = result.nextLink;
|
||||
accounts = [...accounts, ...result.value];
|
||||
}
|
||||
return accounts;
|
||||
}
|
||||
|
||||
export function useDatabaseAccounts(subscriptionId: string, armToken: string): DatabaseAccount[] | undefined {
|
||||
const { data } = useSWR(
|
||||
() => (armToken && subscriptionId ? ["databaseAccounts", subscriptionId, armToken] : undefined),
|
||||
(_, subscriptionId, armToken) => fetchDatabaseAccounts(subscriptionId, armToken)
|
||||
);
|
||||
return data;
|
||||
}
|
||||
40
src/hooks/useDirectories.tsx
Normal file
40
src/hooks/useDirectories.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Tenant } from "../Contracts/DataModels";
|
||||
|
||||
interface TenantListResult {
|
||||
nextLink: string;
|
||||
value: Tenant[];
|
||||
}
|
||||
|
||||
export async function fetchDirectories(accessToken: string): Promise<Tenant[]> {
|
||||
const headers = new Headers();
|
||||
const bearer = `Bearer ${accessToken}`;
|
||||
|
||||
headers.append("Authorization", bearer);
|
||||
|
||||
let tenents: Array<Tenant> = [];
|
||||
let nextLink = `https://management.azure.com/tenants?api-version=2020-01-01`;
|
||||
|
||||
while (nextLink) {
|
||||
const response = await fetch(nextLink, { headers });
|
||||
const result: TenantListResult =
|
||||
response.status === 204 || response.status === 304 ? undefined : await response.json();
|
||||
if (!response.ok) {
|
||||
throw result;
|
||||
}
|
||||
nextLink = result.nextLink;
|
||||
tenents = [...tenents, ...result.value];
|
||||
}
|
||||
return tenents;
|
||||
}
|
||||
|
||||
export function useDirectories(armToken: string): Tenant[] {
|
||||
const [state, setState] = useState<Tenant[]>();
|
||||
|
||||
useEffect(() => {
|
||||
if (armToken) {
|
||||
fetchDirectories(armToken).then(response => setState(response));
|
||||
}
|
||||
}, [armToken]);
|
||||
return state || [];
|
||||
}
|
||||
29
src/hooks/useGraphPhoto.tsx
Normal file
29
src/hooks/useGraphPhoto.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export async function fetchPhoto(accessToken: string): Promise<Blob | void> {
|
||||
const headers = new Headers();
|
||||
const bearer = `Bearer ${accessToken}`;
|
||||
|
||||
headers.append("Authorization", bearer);
|
||||
headers.append("Content-Type", "image/jpg");
|
||||
|
||||
const options = {
|
||||
method: "GET",
|
||||
headers: headers
|
||||
};
|
||||
|
||||
return fetch("https://graph.windows.net/me/thumbnailPhoto?api-version=1.6", options).then(response =>
|
||||
response.blob()
|
||||
);
|
||||
}
|
||||
|
||||
export function useGraphPhoto(graphToken: string): string {
|
||||
const [photo, setPhoto] = useState<string>();
|
||||
|
||||
useEffect(() => {
|
||||
if (graphToken) {
|
||||
fetchPhoto(graphToken).then(response => setPhoto(URL.createObjectURL(response)));
|
||||
}
|
||||
}, [graphToken]);
|
||||
return photo;
|
||||
}
|
||||
36
src/hooks/usePortalAccessToken.tsx
Normal file
36
src/hooks/usePortalAccessToken.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { ApiEndpoints } from "../Common/Constants";
|
||||
import { configContext } from "../ConfigContext";
|
||||
import { AccessInputMetadata } from "../Contracts/DataModels";
|
||||
|
||||
const url = `${configContext.BACKEND_ENDPOINT}${ApiEndpoints.guestRuntimeProxy}/accessinputmetadata?_=1609359229955`;
|
||||
|
||||
export async function fetchAccessData(portalToken: string): Promise<AccessInputMetadata> {
|
||||
const headers = new Headers();
|
||||
// Portal encrypted token API quirk: The token header must be URL encoded
|
||||
headers.append("x-ms-encrypted-auth-token", encodeURIComponent(portalToken));
|
||||
|
||||
const options = {
|
||||
method: "GET",
|
||||
headers: headers
|
||||
};
|
||||
|
||||
return (
|
||||
fetch(url, options)
|
||||
.then(response => response.json())
|
||||
// Portal encrypted token API quirk: The response is double JSON encoded
|
||||
.then(json => JSON.parse(json))
|
||||
.catch(error => console.error(error))
|
||||
);
|
||||
}
|
||||
|
||||
export function useTokenMetadata(token: string): AccessInputMetadata {
|
||||
const [state, setState] = useState<AccessInputMetadata>();
|
||||
|
||||
useEffect(() => {
|
||||
if (token) {
|
||||
fetchAccessData(token).then(response => setState(response));
|
||||
}
|
||||
}, [token]);
|
||||
return state;
|
||||
}
|
||||
40
src/hooks/useSubscriptions.tsx
Normal file
40
src/hooks/useSubscriptions.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { Subscription } from "../Contracts/DataModels";
|
||||
import useSWR from "swr";
|
||||
|
||||
interface SubscriptionListResult {
|
||||
nextLink: string;
|
||||
value: Subscription[];
|
||||
}
|
||||
|
||||
export async function fetchSubscriptions(accessToken: string): Promise<Subscription[]> {
|
||||
const headers = new Headers();
|
||||
const bearer = `Bearer ${accessToken}`;
|
||||
|
||||
headers.append("Authorization", bearer);
|
||||
|
||||
let subscriptions: Array<Subscription> = [];
|
||||
let nextLink = `https://management.azure.com/subscriptions?api-version=2020-01-01`;
|
||||
|
||||
while (nextLink) {
|
||||
const response = await fetch(nextLink, { headers });
|
||||
const result: SubscriptionListResult =
|
||||
response.status === 204 || response.status === 304 ? undefined : await response.json();
|
||||
if (!response.ok) {
|
||||
throw result;
|
||||
}
|
||||
nextLink = result.nextLink;
|
||||
const validSubscriptions = result.value.filter(
|
||||
sub => sub.state === "Enabled" || sub.state === "Warned" || sub.state === "PastDue"
|
||||
);
|
||||
subscriptions = [...subscriptions, ...validSubscriptions];
|
||||
}
|
||||
return subscriptions;
|
||||
}
|
||||
|
||||
export function useSubscriptions(armToken: string): Subscription[] | undefined {
|
||||
const { data } = useSWR(
|
||||
() => (armToken ? ["subscriptions", armToken] : undefined),
|
||||
(_, armToken) => fetchSubscriptions(armToken)
|
||||
);
|
||||
return data;
|
||||
}
|
||||
Reference in New Issue
Block a user