mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-09-19 17:12:47 +01:00
Consolidate hosted login account metadata into a single state
Encrypted-token and direct connection-string logins now write to one accountMetadata state instead of two, which also fixes the connect form staying mounted after a successful SQL/Tables/Gremlin login. Deletes the now-unused useTokenMetadata hook and moves fetchAccessData to Platform/Hosted/Helpers/PortalAccessData.ts.
This commit is contained in:
@@ -42,8 +42,8 @@ export const handleError = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getErrorMessage = (error: unknown = ""): string => {
|
export const getErrorMessage = (error: string | Error = ""): string => {
|
||||||
let errorMessage = typeof error === "string" ? error : (error as Error)?.message;
|
let errorMessage = typeof error === "string" ? error : error.message;
|
||||||
if (!errorMessage) {
|
if (!errorMessage) {
|
||||||
errorMessage = stringifyError(error);
|
errorMessage = stringifyError(error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
jest.mock("./hooks/useAADAuth");
|
jest.mock("./hooks/useAADAuth");
|
||||||
jest.mock("./hooks/useConfig");
|
jest.mock("./hooks/useConfig");
|
||||||
jest.mock("./hooks/usePortalAccessToken");
|
jest.mock("./Platform/Hosted/Helpers/PortalAccessData");
|
||||||
jest.mock("./Platform/Hosted/Components/ConnectExplorer");
|
jest.mock("./Platform/Hosted/Components/ConnectExplorer");
|
||||||
jest.mock("./Shared/appInsights");
|
jest.mock("./Shared/appInsights");
|
||||||
jest.mock("./Platform/Hosted/Components/AccountSwitcher", () => ({
|
jest.mock("./Platform/Hosted/Components/AccountSwitcher", () => ({
|
||||||
@@ -27,13 +27,13 @@ import { act, render } from "@testing-library/react";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { useAADAuth } from "./hooks/useAADAuth";
|
import { useAADAuth } from "./hooks/useAADAuth";
|
||||||
import { useConfig } from "./hooks/useConfig";
|
import { useConfig } from "./hooks/useConfig";
|
||||||
import { useTokenMetadata } from "./hooks/usePortalAccessToken";
|
|
||||||
import { App } from "./HostedExplorer";
|
import { App } from "./HostedExplorer";
|
||||||
import {
|
import {
|
||||||
ConnectExplorer,
|
ConnectExplorer,
|
||||||
fetchEncryptedToken,
|
fetchEncryptedToken,
|
||||||
validateDirectConnectionStringConnectivity,
|
validateDirectConnectionStringConnectivity,
|
||||||
} from "./Platform/Hosted/Components/ConnectExplorer";
|
} from "./Platform/Hosted/Components/ConnectExplorer";
|
||||||
|
import { fetchAccessData } from "./Platform/Hosted/Helpers/PortalAccessData";
|
||||||
|
|
||||||
const mockFetchEncryptedToken = fetchEncryptedToken as jest.MockedFunction<typeof fetchEncryptedToken>;
|
const mockFetchEncryptedToken = fetchEncryptedToken as jest.MockedFunction<typeof fetchEncryptedToken>;
|
||||||
const mockValidateDirectConnectionStringConnectivity =
|
const mockValidateDirectConnectionStringConnectivity =
|
||||||
@@ -60,7 +60,7 @@ beforeEach(() => {
|
|||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
(useAADAuth as jest.Mock).mockReturnValue(defaultAADAuth);
|
(useAADAuth as jest.Mock).mockReturnValue(defaultAADAuth);
|
||||||
(useConfig as jest.Mock).mockReturnValue({});
|
(useConfig as jest.Mock).mockReturnValue({});
|
||||||
(useTokenMetadata as jest.Mock).mockReturnValue(undefined);
|
(fetchAccessData as jest.Mock).mockResolvedValue(undefined);
|
||||||
mockFetchEncryptedToken.mockResolvedValue("encrypted-token");
|
mockFetchEncryptedToken.mockResolvedValue("encrypted-token");
|
||||||
mockValidateDirectConnectionStringConnectivity.mockResolvedValue(undefined);
|
mockValidateDirectConnectionStringConnectivity.mockResolvedValue(undefined);
|
||||||
});
|
});
|
||||||
@@ -75,7 +75,7 @@ const FAKE_ACCOUNT_NAME: string = "-FakeAccount-";
|
|||||||
const FAKE_KEY: string = "<redacted-test-key>";
|
const FAKE_KEY: string = "<redacted-test-key>";
|
||||||
|
|
||||||
describe("HostedExplorer tryCosmosDB postMessage handler", () => {
|
describe("HostedExplorer tryCosmosDB postMessage handler", () => {
|
||||||
it("signs a valid SQL connection string client-side without calling the backend", async () => {
|
it("accepts a valid SQL connection string from an allowed origin", async () => {
|
||||||
render(<App />);
|
render(<App />);
|
||||||
|
|
||||||
const validConnStr = `AccountEndpoint=https://${FAKE_ACCOUNT_NAME}.documents.azure.com:443/;AccountKey=${FAKE_KEY};`;
|
const validConnStr = `AccountEndpoint=https://${FAKE_ACCOUNT_NAME}.documents.azure.com:443/;AccountKey=${FAKE_KEY};`;
|
||||||
@@ -124,7 +124,7 @@ describe("HostedExplorer tryCosmosDB postMessage handler", () => {
|
|||||||
expect(mockFetchEncryptedToken).toHaveBeenCalledWith(cassandraConnStr);
|
expect(mockFetchEncryptedToken).toHaveBeenCalledWith(cassandraConnStr);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("signs a valid Table connection string client-side without calling the backend", async () => {
|
it("accepts a valid Table connection string from an allowed origin", async () => {
|
||||||
render(<App />);
|
render(<App />);
|
||||||
|
|
||||||
const tableConnStr = `DefaultEndpointsProtocol=https;AccountName=${FAKE_ACCOUNT_NAME};AccountKey=${FAKE_KEY};TableEndpoint=https://${FAKE_ACCOUNT_NAME}.table.cosmosdb.azure.com:443/;`;
|
const tableConnStr = `DefaultEndpointsProtocol=https;AccountName=${FAKE_ACCOUNT_NAME};AccountKey=${FAKE_KEY};TableEndpoint=https://${FAKE_ACCOUNT_NAME}.table.cosmosdb.azure.com:443/;`;
|
||||||
@@ -141,7 +141,7 @@ describe("HostedExplorer tryCosmosDB postMessage handler", () => {
|
|||||||
expect(mockValidateDirectConnectionStringConnectivity).toHaveBeenCalled();
|
expect(mockValidateDirectConnectionStringConnectivity).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("signs a valid Gremlin connection string client-side without calling the backend", async () => {
|
it("accepts a valid Gremlin connection string from an allowed origin", async () => {
|
||||||
render(<App />);
|
render(<App />);
|
||||||
|
|
||||||
const gremlinConnStr = `AccountEndpoint=https://${FAKE_ACCOUNT_NAME}.documents.azure.com:443/;AccountKey=${FAKE_KEY};ApiKind=Gremlin;`;
|
const gremlinConnStr = `AccountEndpoint=https://${FAKE_ACCOUNT_NAME}.documents.azure.com:443/;AccountKey=${FAKE_KEY};ApiKind=Gremlin;`;
|
||||||
@@ -158,7 +158,7 @@ describe("HostedExplorer tryCosmosDB postMessage handler", () => {
|
|||||||
expect(mockValidateDirectConnectionStringConnectivity).toHaveBeenCalled();
|
expect(mockValidateDirectConnectionStringConnectivity).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not open the Data Explorer when the Cosmos client cannot connect", async () => {
|
it("does not open Data Explorer when the Cosmos client cannot connect", async () => {
|
||||||
mockValidateDirectConnectionStringConnectivity.mockRejectedValue(new Error("Unable to connect to the account."));
|
mockValidateDirectConnectionStringConnectivity.mockRejectedValue(new Error("Unable to connect to the account."));
|
||||||
const { container } = render(<App />);
|
const { container } = render(<App />);
|
||||||
|
|
||||||
|
|||||||
+15
-13
@@ -23,13 +23,13 @@ import { MeControl } from "./Platform/Hosted/Components/MeControl";
|
|||||||
import { SignInButton } from "./Platform/Hosted/Components/SignInButton";
|
import { SignInButton } from "./Platform/Hosted/Components/SignInButton";
|
||||||
import "./Platform/Hosted/ConnectScreen.less";
|
import "./Platform/Hosted/ConnectScreen.less";
|
||||||
import { parseConnectionString } from "./Platform/Hosted/Helpers/ConnectionStringParser";
|
import { parseConnectionString } from "./Platform/Hosted/Helpers/ConnectionStringParser";
|
||||||
|
import { fetchAccessData } from "./Platform/Hosted/Helpers/PortalAccessData";
|
||||||
import { isResourceTokenConnectionString } from "./Platform/Hosted/Helpers/ResourceTokenUtils";
|
import { isResourceTokenConnectionString } from "./Platform/Hosted/Helpers/ResourceTokenUtils";
|
||||||
import { extractMasterKeyfromConnectionString, isDirectConnectionStringLoginApi } from "./Platform/Hosted/HostedUtils";
|
import { extractMasterKeyfromConnectionString, isDirectConnectionStringLoginApi } from "./Platform/Hosted/HostedUtils";
|
||||||
import "./Shared/appInsights";
|
import "./Shared/appInsights";
|
||||||
import { allowedHostedExplorerEndpoints } from "./Utils/EndpointUtils";
|
import { allowedHostedExplorerEndpoints } from "./Utils/EndpointUtils";
|
||||||
import { useAADAuth } from "./hooks/useAADAuth";
|
import { useAADAuth } from "./hooks/useAADAuth";
|
||||||
import { useConfig } from "./hooks/useConfig";
|
import { useConfig } from "./hooks/useConfig";
|
||||||
import { useTokenMetadata } from "./hooks/usePortalAccessToken";
|
|
||||||
|
|
||||||
initializeIcons();
|
initializeIcons();
|
||||||
|
|
||||||
@@ -37,7 +37,15 @@ const App: React.FunctionComponent = () => {
|
|||||||
// For handling encrypted portal tokens sent via query paramter
|
// For handling encrypted portal tokens sent via query paramter
|
||||||
const params = new URLSearchParams(window.location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
const [encryptedToken, setEncryptedToken] = React.useState<string>(params && params.get("key"));
|
const [encryptedToken, setEncryptedToken] = React.useState<string>(params && params.get("key"));
|
||||||
const encryptedTokenMetadata = useTokenMetadata(encryptedToken);
|
// Encrypted token logins resolve the account metadata through the Portal Backend, while SQL/Tables/Gremlin
|
||||||
|
// connection-string logins derive it client-side, so both paths write to the same value.
|
||||||
|
const [accountMetadata, setAccountMetadata] = React.useState<AccessInputMetadata>();
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (encryptedToken) {
|
||||||
|
fetchAccessData(encryptedToken).then(setAccountMetadata);
|
||||||
|
}
|
||||||
|
}, [encryptedToken]);
|
||||||
|
|
||||||
// For showing/hiding panel
|
// For showing/hiding panel
|
||||||
const [isOpen, { setTrue: openPanel, setFalse: dismissPanel }] = useBoolean(false);
|
const [isOpen, { setTrue: openPanel, setFalse: dismissPanel }] = useBoolean(false);
|
||||||
@@ -49,10 +57,6 @@ const App: React.FunctionComponent = () => {
|
|||||||
const [authType, setAuthType] = React.useState<AuthType>(encryptedToken ? AuthType.EncryptedToken : undefined);
|
const [authType, setAuthType] = React.useState<AuthType>(encryptedToken ? AuthType.EncryptedToken : undefined);
|
||||||
const [connectionString, setConnectionString] = React.useState<string>();
|
const [connectionString, setConnectionString] = React.useState<string>();
|
||||||
const [errorMessage, setErrorMessage] = React.useState<string>();
|
const [errorMessage, setErrorMessage] = React.useState<string>();
|
||||||
// For SQL/Tables/Gremlin connection-string login, the account metadata is derived client-side from the
|
|
||||||
// connection string instead of the Portal Backend, so there is no encrypted token.
|
|
||||||
const [directLoginMetadata, setDirectLoginMetadata] = React.useState<AccessInputMetadata>();
|
|
||||||
const accountMetadata = encryptedTokenMetadata || directLoginMetadata;
|
|
||||||
|
|
||||||
const ref = React.useRef<HTMLIFrameElement>();
|
const ref = React.useRef<HTMLIFrameElement>();
|
||||||
|
|
||||||
@@ -74,7 +78,7 @@ const App: React.FunctionComponent = () => {
|
|||||||
// the Portal Backend proxy and use the metadata derived from the connection string directly.
|
// the Portal Backend proxy and use the metadata derived from the connection string directly.
|
||||||
validateDirectConnectionStringConnectivity(connStr, metadata)
|
validateDirectConnectionStringConnectivity(connStr, metadata)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setDirectLoginMetadata(metadata);
|
setAccountMetadata(metadata);
|
||||||
setAuthType(AuthType.ConnectionString);
|
setAuthType(AuthType.ConnectionString);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@@ -148,7 +152,7 @@ const App: React.FunctionComponent = () => {
|
|||||||
frameWindow.hostedConfig = {
|
frameWindow.hostedConfig = {
|
||||||
authType: AuthType.EncryptedToken,
|
authType: AuthType.EncryptedToken,
|
||||||
encryptedToken,
|
encryptedToken,
|
||||||
encryptedTokenMetadata,
|
encryptedTokenMetadata: accountMetadata,
|
||||||
};
|
};
|
||||||
} else if (authType === AuthType.ConnectionString) {
|
} else if (authType === AuthType.ConnectionString) {
|
||||||
frameWindow.hostedConfig = {
|
frameWindow.hostedConfig = {
|
||||||
@@ -222,9 +226,7 @@ const App: React.FunctionComponent = () => {
|
|||||||
// It's possible this can be changed once all knockout code has been removed.
|
// 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
|
// Setting key is needed so React will re-render this element on any account change
|
||||||
key={
|
key={authType ? `${authType}-${accountMetadata?.accountName || connectionString}` : databaseAccount?.id}
|
||||||
authType ? `${authType}-${encryptedTokenMetadata?.accountName || connectionString}` : databaseAccount?.id
|
|
||||||
}
|
|
||||||
ref={ref}
|
ref={ref}
|
||||||
data-test="DataExplorerFrame"
|
data-test="DataExplorerFrame"
|
||||||
id="explorerMenu"
|
id="explorerMenu"
|
||||||
@@ -234,7 +236,7 @@ const App: React.FunctionComponent = () => {
|
|||||||
src="explorer.html?v=1.0.1&platform=Hosted"
|
src="explorer.html?v=1.0.1&platform=Hosted"
|
||||||
></iframe>
|
></iframe>
|
||||||
)}
|
)}
|
||||||
{!isLoggedIn && !encryptedTokenMetadata && (
|
{!isLoggedIn && !accountMetadata && (
|
||||||
<ConnectExplorer
|
<ConnectExplorer
|
||||||
{...{
|
{...{
|
||||||
login,
|
login,
|
||||||
@@ -242,7 +244,7 @@ const App: React.FunctionComponent = () => {
|
|||||||
setAuthType,
|
setAuthType,
|
||||||
connectionString,
|
connectionString,
|
||||||
setConnectionString,
|
setConnectionString,
|
||||||
setDirectLoginMetadata,
|
setAccountMetadata,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
setErrorMessage,
|
setErrorMessage,
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ export interface AAD {
|
|||||||
export interface ConnectionString {
|
export interface ConnectionString {
|
||||||
authType: AuthType.ConnectionString;
|
authType: AuthType.ConnectionString;
|
||||||
// SQL, Tables, and Gremlin sign data-plane requests client-side with the master key and do not need the
|
// SQL, Tables, and Gremlin sign data-plane requests client-side with the master key and do not need the
|
||||||
// backend proxy, so they carry no encrypted token. Mongo and Cassandra still use the encrypted
|
// proxies, so they carry no encrypted token. Mongo and Cassandra still use the encrypted
|
||||||
// token because their operations go through the backend proxy.
|
// token because their operations go through the proxies.
|
||||||
encryptedToken?: string;
|
encryptedToken?: string;
|
||||||
encryptedTokenMetadata: AccessInputMetadata;
|
encryptedTokenMetadata: AccessInputMetadata;
|
||||||
// Master key is used for the client-side signing path (SQL, Tables, Gremlin). Mongo/Cassandra leave it undefined.
|
// Master key is used for the client-side signing path (SQL, Tables, Gremlin). Mongo/Cassandra leave it undefined.
|
||||||
|
|||||||
@@ -1177,7 +1177,7 @@
|
|||||||
},
|
},
|
||||||
"connectExplorer": {
|
"connectExplorer": {
|
||||||
"errors": {
|
"errors": {
|
||||||
"connectFailed": "Failed to connect using the provided connection string. Please verify it is correct and try again."
|
"connectFailed": "Failed to connect using the provided connection string. Please verify it is correct and that the account is reachable, then try again."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ it("shows the connect form", () => {
|
|||||||
const setConnectionString = jest.fn();
|
const setConnectionString = jest.fn();
|
||||||
const setEncryptedToken = jest.fn();
|
const setEncryptedToken = jest.fn();
|
||||||
const setAuthType = jest.fn();
|
const setAuthType = jest.fn();
|
||||||
const setDirectLoginMetadata = jest.fn();
|
const setAccountMetadata = jest.fn();
|
||||||
const setErrorMessage = jest.fn();
|
const setErrorMessage = jest.fn();
|
||||||
|
|
||||||
render(
|
render(
|
||||||
@@ -31,7 +31,7 @@ it("shows the connect form", () => {
|
|||||||
setAuthType,
|
setAuthType,
|
||||||
connectionString,
|
connectionString,
|
||||||
setConnectionString,
|
setConnectionString,
|
||||||
setDirectLoginMetadata,
|
setAccountMetadata,
|
||||||
setErrorMessage,
|
setErrorMessage,
|
||||||
}}
|
}}
|
||||||
/>,
|
/>,
|
||||||
@@ -47,7 +47,7 @@ it("hides the connection string link when feature.disableConnectionStringLogin i
|
|||||||
const setConnectionString = jest.fn();
|
const setConnectionString = jest.fn();
|
||||||
const setEncryptedToken = jest.fn();
|
const setEncryptedToken = jest.fn();
|
||||||
const setAuthType = jest.fn();
|
const setAuthType = jest.fn();
|
||||||
const setDirectLoginMetadata = jest.fn();
|
const setAccountMetadata = jest.fn();
|
||||||
const setErrorMessage = jest.fn();
|
const setErrorMessage = jest.fn();
|
||||||
const oldFeatures = userContext.features;
|
const oldFeatures = userContext.features;
|
||||||
|
|
||||||
@@ -66,7 +66,7 @@ it("hides the connection string link when feature.disableConnectionStringLogin i
|
|||||||
setAuthType,
|
setAuthType,
|
||||||
connectionString,
|
connectionString,
|
||||||
setConnectionString,
|
setConnectionString,
|
||||||
setDirectLoginMetadata,
|
setAccountMetadata,
|
||||||
setErrorMessage,
|
setErrorMessage,
|
||||||
}}
|
}}
|
||||||
/>,
|
/>,
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ interface Props {
|
|||||||
setEncryptedToken: (token: string) => void;
|
setEncryptedToken: (token: string) => void;
|
||||||
setConnectionString: (connectionString: string) => void;
|
setConnectionString: (connectionString: string) => void;
|
||||||
setAuthType: (authType: AuthType) => void;
|
setAuthType: (authType: AuthType) => void;
|
||||||
setDirectLoginMetadata: (metadata: AccessInputMetadata) => void;
|
setAccountMetadata: (metadata: AccessInputMetadata) => void;
|
||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
setErrorMessage: (message: string) => void;
|
setErrorMessage: (message: string) => void;
|
||||||
}
|
}
|
||||||
@@ -99,7 +99,7 @@ export const ConnectExplorer: React.FunctionComponent<Props> = ({
|
|||||||
setAuthType,
|
setAuthType,
|
||||||
connectionString,
|
connectionString,
|
||||||
setConnectionString,
|
setConnectionString,
|
||||||
setDirectLoginMetadata,
|
setAccountMetadata,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
setErrorMessage,
|
setErrorMessage,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
@@ -144,7 +144,7 @@ export const ConnectExplorer: React.FunctionComponent<Props> = ({
|
|||||||
// we skip the Portal Backend proxy and use the metadata parsed from the connection string.
|
// we skip the Portal Backend proxy and use the metadata parsed from the connection string.
|
||||||
try {
|
try {
|
||||||
await validateDirectConnectionStringConnectivity(connectionString, metadata);
|
await validateDirectConnectionStringConnectivity(connectionString, metadata);
|
||||||
setDirectLoginMetadata(metadata);
|
setAccountMetadata(metadata);
|
||||||
setAuthType(AuthType.ConnectionString);
|
setAuthType(AuthType.ConnectionString);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setErrorMessage(getErrorMessage(error));
|
setErrorMessage(getErrorMessage(error));
|
||||||
|
|||||||
+3
-15
@@ -1,7 +1,6 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { HttpHeaders } from "../../../Common/Constants";
|
||||||
import { HttpHeaders } from "../Common/Constants";
|
import { configContext } from "../../../ConfigContext";
|
||||||
import { configContext } from "../ConfigContext";
|
import { AccessInputMetadata } from "../../../Contracts/DataModels";
|
||||||
import { AccessInputMetadata } from "../Contracts/DataModels";
|
|
||||||
|
|
||||||
export async function fetchAccessData(portalToken: string): Promise<AccessInputMetadata> {
|
export async function fetchAccessData(portalToken: string): Promise<AccessInputMetadata> {
|
||||||
const headers = new Headers();
|
const headers = new Headers();
|
||||||
@@ -18,14 +17,3 @@ export async function fetchAccessData(portalToken: string): Promise<AccessInputM
|
|||||||
.then((response) => response.json())
|
.then((response) => response.json())
|
||||||
.catch((error) => console.error(error));
|
.catch((error) => console.error(error));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useTokenMetadata(token: string): AccessInputMetadata | undefined {
|
|
||||||
const [state, setState] = useState<AccessInputMetadata | undefined>();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (token) {
|
|
||||||
fetchAccessData(token).then((response) => setState(response));
|
|
||||||
}
|
|
||||||
}, [token]);
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
@@ -66,6 +66,7 @@
|
|||||||
"./src/Platform/Hosted/Components/MeControl.test.tsx",
|
"./src/Platform/Hosted/Components/MeControl.test.tsx",
|
||||||
"./src/Platform/Hosted/Components/MeControl.tsx",
|
"./src/Platform/Hosted/Components/MeControl.tsx",
|
||||||
"./src/Platform/Hosted/Components/SignInButton.tsx",
|
"./src/Platform/Hosted/Components/SignInButton.tsx",
|
||||||
|
"./src/Platform/Hosted/Helpers/PortalAccessData.ts",
|
||||||
"./src/Platform/Hosted/HostedUtils.test.ts",
|
"./src/Platform/Hosted/HostedUtils.test.ts",
|
||||||
"./src/Platform/Hosted/HostedUtils.ts",
|
"./src/Platform/Hosted/HostedUtils.ts",
|
||||||
"./src/Platform/Hosted/extractFeatures.test.ts",
|
"./src/Platform/Hosted/extractFeatures.test.ts",
|
||||||
@@ -107,7 +108,6 @@
|
|||||||
"./src/hooks/useDirectories.tsx",
|
"./src/hooks/useDirectories.tsx",
|
||||||
"./src/hooks/useGraphPhoto.tsx",
|
"./src/hooks/useGraphPhoto.tsx",
|
||||||
"./src/hooks/useNotebookSnapshotStore.ts",
|
"./src/hooks/useNotebookSnapshotStore.ts",
|
||||||
"./src/hooks/usePortalAccessToken.tsx",
|
|
||||||
"./src/hooks/useNotificationConsole.ts",
|
"./src/hooks/useNotificationConsole.ts",
|
||||||
"./src/hooks/useObservable.ts",
|
"./src/hooks/useObservable.ts",
|
||||||
"./src/hooks/useSidePanel.ts",
|
"./src/hooks/useSidePanel.ts",
|
||||||
|
|||||||
Reference in New Issue
Block a user