mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-23 02:41:39 +00:00
Compare commits
12 Commits
PSQL_Shell
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6eee0b18bc | ||
|
|
952491a3ad | ||
|
|
5dde66b032 | ||
|
|
5b1db2778c | ||
|
|
1213788f9c | ||
|
|
1ce3adff0f | ||
|
|
00eb07da11 | ||
|
|
afe59c1589 | ||
|
|
53b5ebd39c | ||
|
|
5b365e642f | ||
|
|
333b3de587 | ||
|
|
e909ac43f4 |
BIN
images/firewallRule.png
Normal file
BIN
images/firewallRule.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
2666
package-lock.json
generated
2666
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -34,6 +34,7 @@ export interface DatabaseAccountExtendedProperties {
|
||||
capacity?: { totalThroughputLimit: number };
|
||||
locations?: DatabaseAccountResponseLocation[];
|
||||
postgresqlEndpoint?: string;
|
||||
publicNetworkAccess?: string;
|
||||
}
|
||||
|
||||
export interface DatabaseAccountResponseLocation {
|
||||
@@ -567,6 +568,16 @@ export interface ContainerConnectionInfo {
|
||||
//need to add ram and rom info
|
||||
}
|
||||
|
||||
export interface PostgresFirewallRule {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
properties: {
|
||||
startIpAddress: string;
|
||||
endIpAddress: string;
|
||||
};
|
||||
}
|
||||
|
||||
export enum PhoenixErrorType {
|
||||
MaxAllocationTimeExceeded = "MaxAllocationTimeExceeded",
|
||||
MaxDbAccountsPerUserExceeded = "MaxDbAccountsPerUserExceeded",
|
||||
|
||||
@@ -36,6 +36,8 @@ export enum MessageTypes {
|
||||
CloseTab,
|
||||
OpenQuickstartBlade,
|
||||
OpenPostgreSQLPasswordReset,
|
||||
OpenPostgresNetworkingBlade,
|
||||
OpenCosmosDBNetworkingBlade,
|
||||
}
|
||||
|
||||
export { Versions, ActionContracts, Diagnostics };
|
||||
|
||||
@@ -186,7 +186,6 @@ export interface Collection extends CollectionBase {
|
||||
onDrop(source: Collection, event: { originalEvent: DragEvent }): void;
|
||||
uploadFiles(fileList: FileList): Promise<{ data: UploadDetailsRecord[] }>;
|
||||
|
||||
getLabel(): string;
|
||||
getPendingThroughputSplitNotification(): Promise<DataModels.Notification>;
|
||||
}
|
||||
|
||||
@@ -397,6 +396,7 @@ export interface DataExplorerInputsFrame {
|
||||
dataExplorerVersion?: string;
|
||||
defaultCollectionThroughput?: CollectionCreationDefaults;
|
||||
isPostgresAccount?: boolean;
|
||||
isReplica?: boolean;
|
||||
// TODO: Update this param in the OSS extension to remove isFreeTier, isMarlinServerGroup, and make nodes a flat array instead of an nested array
|
||||
connectionStringParams?: any;
|
||||
flights?: readonly string[];
|
||||
|
||||
@@ -77,7 +77,7 @@ export class NotebookTerminalComponent extends React.Component<NotebookTerminalC
|
||||
} else if (StringUtils.endsWith(notebookServerEndpoint, "cassandra")) {
|
||||
terminalEndpoint = this.props.databaseAccount?.properties.cassandraEndpoint;
|
||||
} else if (StringUtils.endsWith(notebookServerEndpoint, "postgresql")) {
|
||||
return "c.vimeng-postgre-citus.postgres.database.azure.com";
|
||||
return this.props.databaseAccount?.properties.postgresqlEndpoint;
|
||||
}
|
||||
|
||||
if (terminalEndpoint) {
|
||||
|
||||
@@ -1,17 +1,9 @@
|
||||
jest.mock("../hooks/useFullScreenURLs");
|
||||
import "@testing-library/jest-dom";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import React from "react";
|
||||
import { useFullScreenURLs } from "../hooks/useFullScreenURLs";
|
||||
import { OpenFullScreen } from "./OpenFullScreen";
|
||||
|
||||
it("renders the correct URLs", () => {
|
||||
(useFullScreenURLs as jest.Mock).mockReturnValue({
|
||||
readWrite: "read and write url",
|
||||
read: "read only url",
|
||||
});
|
||||
|
||||
render(<OpenFullScreen />);
|
||||
expect(screen.getByLabelText("Read and Write")).toHaveValue("https://cosmos.azure.com/?key=read and write url");
|
||||
expect(screen.getByLabelText("Read Only")).toHaveValue("https://cosmos.azure.com/?key=read only url");
|
||||
expect(screen.getByText("Open")).toBeDefined();
|
||||
});
|
||||
|
||||
@@ -1,66 +1,26 @@
|
||||
import { DefaultButton, PrimaryButton, Spinner, Stack, Text, TextField } from "@fluentui/react";
|
||||
import copyToClipboard from "clipboard-copy";
|
||||
import { PrimaryButton, Stack, Text } from "@fluentui/react";
|
||||
import * as React from "react";
|
||||
import { useFullScreenURLs } from "../hooks/useFullScreenURLs";
|
||||
|
||||
export const OpenFullScreen: React.FunctionComponent = () => {
|
||||
const [isReadUrlCopy, setIsReadUrlCopy] = React.useState<boolean>(false);
|
||||
const [isReadWriteUrlCopy, setIsReadWriteUrlCopy] = React.useState<boolean>(false);
|
||||
const result = useFullScreenURLs();
|
||||
if (!result) {
|
||||
return <Spinner label="Generating URLs..." ariaLive="assertive" labelPosition="right" />;
|
||||
}
|
||||
|
||||
const readWriteUrl = `https://cosmos.azure.com/?key=${result.readWrite}`;
|
||||
const readUrl = `https://cosmos.azure.com/?key=${result.read}`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack tokens={{ childrenGap: 10 }}>
|
||||
<Text>
|
||||
Open this database account in a new browser tab with Cosmos DB Explorer. Or copy the read-write or read only
|
||||
access urls below to share with others. For security purposes, the URLs grant time-bound access to the
|
||||
account. When access expires, you can reconnect, using a valid connection string for the account.
|
||||
</Text>
|
||||
<TextField label="Read and Write" readOnly defaultValue={readWriteUrl} />
|
||||
<Stack horizontal tokens={{ childrenGap: 10 }}>
|
||||
<DefaultButton
|
||||
ariaLabel={isReadWriteUrlCopy ? "Copied url" : "Copy"}
|
||||
onClick={() => {
|
||||
copyToClipboard(readWriteUrl);
|
||||
setIsReadWriteUrlCopy(true);
|
||||
}}
|
||||
text={isReadWriteUrlCopy ? "Copied" : "Copy"}
|
||||
iconProps={{ iconName: "Copy" }}
|
||||
/>
|
||||
<PrimaryButton
|
||||
onClick={() => {
|
||||
window.open(readWriteUrl, "_blank");
|
||||
}}
|
||||
text="Open"
|
||||
iconProps={{ iconName: "OpenInNewWindow" }}
|
||||
/>
|
||||
<div style={{ padding: "34px" }}>
|
||||
<Stack tokens={{ childrenGap: 10 }}>
|
||||
<Text>
|
||||
Open this database account in a new browser tab with Cosmos DB Explorer. You can connect using your
|
||||
Microsoft account or a connection string.
|
||||
</Text>
|
||||
<Stack horizontal tokens={{ childrenGap: 10 }}>
|
||||
<PrimaryButton
|
||||
onClick={() => {
|
||||
window.open("https://cosmos.azure.com/", "_blank");
|
||||
}}
|
||||
text="Open"
|
||||
iconProps={{ iconName: "OpenInNewWindow" }}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<TextField label="Read Only" readOnly defaultValue={readUrl} />
|
||||
<Stack horizontal tokens={{ childrenGap: 10 }}>
|
||||
<DefaultButton
|
||||
ariaLabel={isReadUrlCopy ? "Copied url" : "Copy"}
|
||||
onClick={() => {
|
||||
setIsReadUrlCopy(true);
|
||||
copyToClipboard(readUrl);
|
||||
}}
|
||||
text={isReadUrlCopy ? "Copied" : "Copy"}
|
||||
iconProps={{ iconName: "Copy" }}
|
||||
/>
|
||||
<PrimaryButton
|
||||
onClick={() => {
|
||||
window.open(readUrl, "_blank");
|
||||
}}
|
||||
text="Open"
|
||||
iconProps={{ iconName: "OpenInNewWindow" }}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -94,7 +94,7 @@ const getDescriptionText = (page: number): string => {
|
||||
case 1:
|
||||
return "Azure Cosmos DB is a fully managed NoSQL database service for modern app development. ";
|
||||
case 2:
|
||||
return "Launch the quickstart for a tutotrial to learn how to create a database, add sample data, connect to a sample app and more.";
|
||||
return "Launch the quickstart for a tutorial to learn how to create a database, add sample data, connect to a sample app and more.";
|
||||
case 3:
|
||||
return "Already have an existing app? Connect your database to an app, or tooling of your choice from Data Explorer.";
|
||||
default:
|
||||
|
||||
22
src/Explorer/Quickstart/QuickstartFirewallNotification.tsx
Normal file
22
src/Explorer/Quickstart/QuickstartFirewallNotification.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Image, PrimaryButton, Stack, Text } from "@fluentui/react";
|
||||
import { sendMessage } from "Common/MessageHandler";
|
||||
import { MessageTypes } from "Contracts/ExplorerContracts";
|
||||
import React from "react";
|
||||
import FirewallRuleScreenshot from "../../../images/firewallRule.png";
|
||||
|
||||
export const QuickstartFirewallNotification: React.FC = (): JSX.Element => (
|
||||
<Stack style={{ padding: "16px 20px" }}>
|
||||
<Text block>
|
||||
To use the PostgreSQL shell, you need to add a firewall rule to allow access from all IP addresses
|
||||
(0.0.0.0-255.255.255).
|
||||
</Text>
|
||||
<Text block>We strongly recommend removing this rule once you finish using the PostgreSQL shell.</Text>
|
||||
<Image style={{ margin: "20px 0" }} src={FirewallRuleScreenshot} />
|
||||
<PrimaryButton
|
||||
style={{ width: 150 }}
|
||||
onClick={() => sendMessage({ type: MessageTypes.OpenPostgresNetworkingBlade })}
|
||||
>
|
||||
Add firewall rule
|
||||
</PrimaryButton>
|
||||
</Stack>
|
||||
);
|
||||
@@ -96,12 +96,18 @@ export const QuickstartGuide: React.FC = (): JSX.Element => {
|
||||
<Stack style={{ flexGrow: 1, padding: "0 20px", overflow: "auto" }}>
|
||||
<Text variant="xxLarge">Quick start guide</Text>
|
||||
{currentStep < 5 && (
|
||||
<Pivot style={{ marginTop: 10, width: "100%" }} selectedKey={GuideSteps[currentStep]}>
|
||||
<Pivot
|
||||
style={{ marginTop: 10, width: "100%" }}
|
||||
selectedKey={GuideSteps[currentStep]}
|
||||
onLinkClick={(item?: PivotItem) => setCurrentStep(Object.values(GuideSteps).indexOf(item.props.itemKey))}
|
||||
>
|
||||
<PivotItem
|
||||
headerText="Login"
|
||||
onRenderItemLink={(props, defaultRenderer) => customPivotHeaderRenderer(props, defaultRenderer, 0)}
|
||||
itemKey={GuideSteps[0]}
|
||||
onClick={() => setCurrentStep(0)}
|
||||
onClick={() => {
|
||||
setCurrentStep(0);
|
||||
}}
|
||||
>
|
||||
<Stack style={{ marginTop: 20 }}>
|
||||
<Text>
|
||||
@@ -109,8 +115,12 @@ export const QuickstartGuide: React.FC = (): JSX.Element => {
|
||||
<br />
|
||||
<br />
|
||||
To begin, please enter the cluster's password in the PostgreSQL terminal.
|
||||
<br />
|
||||
<br />
|
||||
Note: If you navigate out of the Quick Start tab (PostgreSQL Shell), the session will be closed and
|
||||
all ongoing commands might be interrupted.
|
||||
</Text>
|
||||
<Youtube videoId="UaBDXHMQAUw" style={{ margin: "20px 0" }} opts={{ width: "90%" }} />
|
||||
<Youtube videoId="nT64dFSfiUo" style={{ margin: "20px 0" }} opts={{ width: "90%" }} />
|
||||
</Stack>
|
||||
</PivotItem>
|
||||
<PivotItem
|
||||
@@ -120,7 +130,7 @@ export const QuickstartGuide: React.FC = (): JSX.Element => {
|
||||
onClick={() => setCurrentStep(1)}
|
||||
>
|
||||
<Stack style={{ marginTop: 20 }}>
|
||||
<Text>Let’s create two tables github_users and github_events in “cosmosdb_tutorial” schema.</Text>
|
||||
<Text>Let's create two tables github_users and github_events in “cosmosdb_tutorial” schema.</Text>
|
||||
<DefaultButton
|
||||
style={{ marginTop: 16, width: 150 }}
|
||||
onClick={() => useTerminal.getState().sendMessage(newTableCommand)}
|
||||
@@ -150,7 +160,7 @@ export const QuickstartGuide: React.FC = (): JSX.Element => {
|
||||
onClick={() => onCopyBtnClicked("#newTableCommand")}
|
||||
/>
|
||||
</Stack>
|
||||
<Youtube videoId="VJqupvSQ-mw" style={{ margin: "20px 0" }} opts={{ width: "90%" }} />
|
||||
<Youtube videoId="il_sA6U1WcY" style={{ margin: "20px 0" }} opts={{ width: "90%" }} />
|
||||
</Stack>
|
||||
</PivotItem>
|
||||
<PivotItem
|
||||
@@ -195,7 +205,7 @@ export const QuickstartGuide: React.FC = (): JSX.Element => {
|
||||
onClick={() => onCopyBtnClicked("#distributeTableCommand")}
|
||||
/>
|
||||
</Stack>
|
||||
<Youtube videoId="Q-AW7q1GLDY" style={{ margin: "20px 0" }} opts={{ width: "90%" }} />
|
||||
<Youtube videoId="kCCDRRrN1r0" style={{ margin: "20px 0" }} opts={{ width: "90%" }} />
|
||||
</Stack>
|
||||
</PivotItem>
|
||||
<PivotItem
|
||||
@@ -235,7 +245,7 @@ export const QuickstartGuide: React.FC = (): JSX.Element => {
|
||||
onClick={() => onCopyBtnClicked("#loadDataCommand")}
|
||||
/>
|
||||
</Stack>
|
||||
<Youtube videoId="h15fvLKXzRo" style={{ margin: "20px 0" }} opts={{ width: "90%" }} />
|
||||
<Youtube videoId="XSMEE2tujEk" style={{ margin: "20px 0" }} opts={{ width: "90%" }} />
|
||||
</Stack>
|
||||
</PivotItem>
|
||||
<PivotItem
|
||||
@@ -277,7 +287,7 @@ export const QuickstartGuide: React.FC = (): JSX.Element => {
|
||||
onClick={() => onCopyBtnClicked("#queryCommand")}
|
||||
/>
|
||||
</Stack>
|
||||
<Youtube videoId="p46nRnE4b8Y" style={{ margin: "20px 0" }} opts={{ width: "90%" }} />
|
||||
<Youtube videoId="k_EanjMtaPg" style={{ margin: "20px 0" }} opts={{ width: "90%" }} />
|
||||
</Stack>
|
||||
</PivotItem>
|
||||
</Pivot>
|
||||
|
||||
@@ -14,10 +14,6 @@
|
||||
padding-right: 16px;
|
||||
max-width: 1168px;
|
||||
|
||||
> * {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
> .title {
|
||||
position: relative; // To attach FeaturePanelLauncher as absolute
|
||||
color: @BaseHigh;
|
||||
|
||||
@@ -186,7 +186,10 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
headline="Create your password"
|
||||
target={"#mainButton-quickstartDescription"}
|
||||
hasCloseButton
|
||||
onDismiss={() => usePostgres.getState().setShowResetPasswordBubble(false)}
|
||||
onDismiss={() => {
|
||||
localStorage.setItem(userContext.databaseAccount.id, "true");
|
||||
usePostgres.getState().setShowResetPasswordBubble(false);
|
||||
}}
|
||||
calloutProps={{
|
||||
directionalHint: DirectionalHint.bottomRightEdge,
|
||||
directionalHintFixed: true,
|
||||
@@ -197,14 +200,15 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
primaryButtonProps={{
|
||||
text: "Create",
|
||||
onClick: () => {
|
||||
localStorage.setItem(userContext.databaseAccount.id, "true");
|
||||
sendMessage({
|
||||
type: MessageTypes.OpenQuickstartBlade,
|
||||
type: MessageTypes.OpenPostgreSQLPasswordReset,
|
||||
});
|
||||
usePostgres.getState().setShowResetPasswordBubble(false);
|
||||
},
|
||||
}}
|
||||
>
|
||||
This password will be used to connect to the database.
|
||||
If you haven't changed your password yet, change it now.
|
||||
</TeachingBubble>
|
||||
)}
|
||||
</div>
|
||||
@@ -300,7 +304,11 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
public createMainItems(): SplashScreenItem[] {
|
||||
const heroes: SplashScreenItem[] = [];
|
||||
|
||||
if (userContext.apiType === "SQL" || userContext.apiType === "Mongo" || userContext.apiType === "Postgres") {
|
||||
if (
|
||||
userContext.apiType === "SQL" ||
|
||||
userContext.apiType === "Mongo" ||
|
||||
(userContext.apiType === "Postgres" && !userContext.isReplica)
|
||||
) {
|
||||
const launchQuickstartBtn = {
|
||||
id: "quickstartDescription",
|
||||
iconSrc: QuickStartIcon,
|
||||
@@ -347,10 +355,10 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
|
||||
const connectBtn = {
|
||||
iconSrc: ConnectIcon,
|
||||
title: userContext.apiType === "Postgres" ? "Connect with PG Admin" : "Connect",
|
||||
title: userContext.apiType === "Postgres" ? "Connect with pgAdmin" : "Connect",
|
||||
description:
|
||||
userContext.apiType === "Postgres"
|
||||
? "Prefer PGadmin? Find your connection strings here"
|
||||
? "Prefer pgAdmin? Find your connection strings here"
|
||||
: "Prefer using your own choice of tooling? Find the connection string you need to connect",
|
||||
onClick: () => useTabs.getState().openAndActivateReactTab(ReactTabKind.Connect),
|
||||
};
|
||||
|
||||
@@ -120,7 +120,7 @@ You can enable or disable public IP addresses on the worker nodes on 'Networking
|
||||
</Stack>
|
||||
|
||||
<Label>Secure connections</Label>
|
||||
<Text>
|
||||
<Text style={{ marginBottom: 8 }}>
|
||||
Only secure connections are supported. For production use cases, we recommend using the 'verify-full'
|
||||
mode to enforce TLS certificate verification. You will need to download the Hyperscale (Citus) certificate, and
|
||||
provide it when connecting to the database.{" "}
|
||||
@@ -128,6 +128,18 @@ You can enable or disable public IP addresses on the worker nodes on 'Networking
|
||||
Learn more
|
||||
</Link>
|
||||
</Text>
|
||||
|
||||
<Label>Connect with pgAdmin</Label>
|
||||
<Text>
|
||||
Refer to our{" "}
|
||||
<Link
|
||||
href="https://learn.microsoft.com/en-us/azure/postgresql/hyperscale/howto-connect?tabs=pgadmin"
|
||||
target="_blank"
|
||||
>
|
||||
guide
|
||||
</Link>{" "}
|
||||
to help you connect via pgAdmin.
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { Spinner, SpinnerSize, Stack } from "@fluentui/react";
|
||||
import { NotebookWorkspaceConnectionInfo } from "Contracts/DataModels";
|
||||
import { Spinner, SpinnerSize, Stack, Text } from "@fluentui/react";
|
||||
import { configContext } from "ConfigContext";
|
||||
import { NotebookWorkspaceConnectionInfo, PostgresFirewallRule } from "Contracts/DataModels";
|
||||
import { NotebookTerminalComponent } from "Explorer/Controls/Notebook/NotebookTerminalComponent";
|
||||
import Explorer from "Explorer/Explorer";
|
||||
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
||||
import { QuickstartFirewallNotification } from "Explorer/Quickstart/QuickstartFirewallNotification";
|
||||
import { QuickstartGuide } from "Explorer/Quickstart/QuickstartGuide";
|
||||
import React, { useEffect } from "react";
|
||||
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { userContext } from "UserContext";
|
||||
import { armRequest } from "Utils/arm/request";
|
||||
|
||||
interface QuickstartTabProps {
|
||||
explorer: Explorer;
|
||||
@@ -13,30 +17,67 @@ interface QuickstartTabProps {
|
||||
|
||||
export const QuickstartTab: React.FC<QuickstartTabProps> = ({ explorer }: QuickstartTabProps): JSX.Element => {
|
||||
const notebookServerInfo = useNotebook((state) => state.notebookServerInfo);
|
||||
useEffect(() => {
|
||||
explorer.allocateContainer();
|
||||
}, []);
|
||||
const [isAllPublicIPAddressEnabled, setIsAllPublicIPAddressEnabled] = useState<boolean>(true);
|
||||
|
||||
const getNotebookServerInfo = (): NotebookWorkspaceConnectionInfo => ({
|
||||
authToken: notebookServerInfo.authToken,
|
||||
notebookServerEndpoint: `${notebookServerInfo.notebookServerEndpoint?.replace(/\/+$/, "")}/postgresql`,
|
||||
forwardingId: notebookServerInfo.forwardingId,
|
||||
});
|
||||
|
||||
const checkFirewallRules = async (): Promise<void> => {
|
||||
const firewallRulesUri = `${userContext.databaseAccount.id}/firewallRules`;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const response: any = await armRequest({
|
||||
host: configContext.ARM_ENDPOINT,
|
||||
path: firewallRulesUri,
|
||||
method: "GET",
|
||||
apiVersion: "2020-10-05-privatepreview",
|
||||
});
|
||||
const firewallRules: PostgresFirewallRule[] = response?.data?.value || response?.value || [];
|
||||
const isEnabled = firewallRules.some(
|
||||
(rule) => rule.properties.startIpAddress === "0.0.0.0" && rule.properties.endIpAddress === "255.255.255.255"
|
||||
);
|
||||
setIsAllPublicIPAddressEnabled(isEnabled);
|
||||
|
||||
// If the firewall rule is not added, check every 30 seconds to see if the user has added the rule
|
||||
if (!isEnabled && useTabs.getState().activeReactTab === ReactTabKind.Quickstart) {
|
||||
setTimeout(checkFirewallRules, 30000);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
checkFirewallRules();
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
explorer.allocateContainer();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Stack style={{ width: "100%" }} horizontal>
|
||||
<Stack style={{ width: "50%" }}>
|
||||
<QuickstartGuide />
|
||||
</Stack>
|
||||
<Stack style={{ width: "50%", borderLeft: "black solid 1px" }}>
|
||||
{notebookServerInfo?.notebookServerEndpoint && (
|
||||
{!isAllPublicIPAddressEnabled && <QuickstartFirewallNotification />}
|
||||
{isAllPublicIPAddressEnabled && notebookServerInfo?.notebookServerEndpoint && (
|
||||
<NotebookTerminalComponent
|
||||
notebookServerInfo={getNotebookServerInfo()}
|
||||
databaseAccount={userContext.databaseAccount}
|
||||
tabId="QuickstartPSQLShell"
|
||||
/>
|
||||
)}
|
||||
{!notebookServerInfo?.notebookServerEndpoint && (
|
||||
<Spinner styles={{ root: { marginTop: 10 } }} size={SpinnerSize.large}></Spinner>
|
||||
{isAllPublicIPAddressEnabled && !notebookServerInfo?.notebookServerEndpoint && (
|
||||
<Stack style={{ margin: "auto 0" }}>
|
||||
<Text block style={{ margin: "auto" }}>
|
||||
Connecting to the PostgreSQL shell.
|
||||
</Text>
|
||||
<Text block style={{ margin: "auto" }}>
|
||||
If the cluster was just created, this could take up to a minute.
|
||||
</Text>
|
||||
<Spinner styles={{ root: { marginTop: 16 } }} size={SpinnerSize.large}></Spinner>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { MessageBar, MessageBarButton, MessageBarType } from "@fluentui/react";
|
||||
import { sendMessage } from "Common/MessageHandler";
|
||||
import { MessageTypes } from "Contracts/ExplorerContracts";
|
||||
import { CollectionTabKind } from "Contracts/ViewModels";
|
||||
import Explorer from "Explorer/Explorer";
|
||||
import { SplashScreen } from "Explorer/SplashScreen/SplashScreen";
|
||||
@@ -21,10 +24,24 @@ interface TabsProps {
|
||||
}
|
||||
|
||||
export const Tabs = ({ explorer }: TabsProps): JSX.Element => {
|
||||
const { openedTabs, openedReactTabs, activeTab, activeReactTab } = useTabs();
|
||||
const { openedTabs, openedReactTabs, activeTab, activeReactTab, showNetworkSettingsWarning } = useTabs();
|
||||
|
||||
return (
|
||||
<div className="tabsManagerContainer">
|
||||
{showNetworkSettingsWarning && (
|
||||
<MessageBar
|
||||
messageBarType={MessageBarType.warning}
|
||||
actions={
|
||||
<MessageBarButton onClick={() => sendMessage({ type: MessageTypes.OpenCosmosDBNetworkingBlade })}>
|
||||
Change network settings
|
||||
</MessageBarButton>
|
||||
}
|
||||
messageBarIconProps={{ iconName: "WarningSolid", className: "messageBarWarningIcon" }}
|
||||
>
|
||||
The Network settings for this account are preventing access from Data Explorer. Please allow access from Azure
|
||||
Portal to proceed.
|
||||
</MessageBar>
|
||||
)}
|
||||
<div id="content" className="flexContainer hideOverflows">
|
||||
<div className="nav-tabs-margin">
|
||||
<ul className="nav nav-tabs level navTabHeight" id="navTabs" role="tablist">
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { Spinner, SpinnerSize } from "@fluentui/react";
|
||||
import { configContext } from "ConfigContext";
|
||||
import { QuickstartFirewallNotification } from "Explorer/Quickstart/QuickstartFirewallNotification";
|
||||
import * as ko from "knockout";
|
||||
import * as React from "react";
|
||||
import { armRequest } from "Utils/arm/request";
|
||||
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
@@ -26,10 +29,15 @@ class NotebookTerminalComponentAdapter implements ReactAdapter {
|
||||
constructor(
|
||||
private getNotebookServerInfo: () => DataModels.NotebookWorkspaceConnectionInfo,
|
||||
private getDatabaseAccount: () => DataModels.DatabaseAccount,
|
||||
private getTabId: () => string
|
||||
private getTabId: () => string,
|
||||
private isAllPublicIPAddressesEnabled: ko.Observable<boolean>
|
||||
) {}
|
||||
|
||||
public renderComponent(): JSX.Element {
|
||||
if (!this.isAllPublicIPAddressesEnabled()) {
|
||||
return <QuickstartFirewallNotification />;
|
||||
}
|
||||
|
||||
return this.parameters() ? (
|
||||
<NotebookTerminalComponent
|
||||
notebookServerInfo={this.getNotebookServerInfo()}
|
||||
@@ -46,25 +54,33 @@ export default class TerminalTab extends TabsBase {
|
||||
public readonly html = '<div style="height: 100%" data-bind="react:notebookTerminalComponentAdapter"></div> ';
|
||||
private container: Explorer;
|
||||
private notebookTerminalComponentAdapter: NotebookTerminalComponentAdapter;
|
||||
private isAllPublicIPAddressesEnabled: ko.Observable<boolean>;
|
||||
|
||||
constructor(options: TerminalTabOptions) {
|
||||
super(options);
|
||||
this.container = options.container;
|
||||
this.isAllPublicIPAddressesEnabled = ko.observable(true);
|
||||
this.notebookTerminalComponentAdapter = new NotebookTerminalComponentAdapter(
|
||||
() => this.getNotebookServerInfo(options),
|
||||
() => userContext?.databaseAccount,
|
||||
() => this.tabId
|
||||
() => this.tabId,
|
||||
this.isAllPublicIPAddressesEnabled
|
||||
);
|
||||
this.notebookTerminalComponentAdapter.parameters = ko.computed<boolean>(() => {
|
||||
if (
|
||||
this.isTemplateReady() &&
|
||||
useNotebook.getState().isNotebookEnabled &&
|
||||
useNotebook.getState().notebookServerInfo?.notebookServerEndpoint
|
||||
useNotebook.getState().notebookServerInfo?.notebookServerEndpoint &&
|
||||
this.isAllPublicIPAddressesEnabled()
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (options.kind === ViewModels.TerminalKind.Postgres) {
|
||||
this.checkPostgresFirewallRules();
|
||||
}
|
||||
}
|
||||
|
||||
public getContainer(): Explorer {
|
||||
@@ -110,4 +126,25 @@ export default class TerminalTab extends TabsBase {
|
||||
forwardingId: info.forwardingId,
|
||||
};
|
||||
}
|
||||
|
||||
private async checkPostgresFirewallRules(): Promise<void> {
|
||||
const firewallRulesUri = `${userContext.databaseAccount.id}/firewallRules`;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const response: any = await armRequest({
|
||||
host: configContext.ARM_ENDPOINT,
|
||||
path: firewallRulesUri,
|
||||
method: "GET",
|
||||
apiVersion: "2020-10-05-privatepreview",
|
||||
});
|
||||
const firewallRules: DataModels.PostgresFirewallRule[] = response?.data?.value || response?.value || [];
|
||||
const isEnabled = firewallRules.some(
|
||||
(rule) => rule.properties.startIpAddress === "0.0.0.0" && rule.properties.endIpAddress === "255.255.255.255"
|
||||
);
|
||||
this.isAllPublicIPAddressesEnabled(isEnabled);
|
||||
|
||||
// If the firewall rule is not added, check every 30 seconds to see if the user has added the rule
|
||||
if (!isEnabled) {
|
||||
setTimeout(() => this.checkPostgresFirewallRules(), 30000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1160,23 +1160,6 @@ export default class Collection implements ViewModels.Collection {
|
||||
this.onDocumentDBDocumentsClick();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get correct collection label depending on account API
|
||||
*/
|
||||
public getLabel(): string {
|
||||
if (userContext.apiType === "Tables") {
|
||||
return "Entities";
|
||||
} else if (userContext.apiType === "Cassandra") {
|
||||
return "Rows";
|
||||
} else if (userContext.apiType === "Gremlin") {
|
||||
return "Graph";
|
||||
} else if (userContext.apiType === "Mongo") {
|
||||
return "Documents";
|
||||
}
|
||||
|
||||
return "Items";
|
||||
}
|
||||
|
||||
public getDatabase(): ViewModels.Database {
|
||||
return useDatabases.getState().findDatabaseWithId(this.databaseId);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react";
|
||||
import * as React from "react";
|
||||
import { getItemName } from "Utils/APITypeUtils";
|
||||
import shallow from "zustand/shallow";
|
||||
import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg";
|
||||
import DeleteIcon from "../../../images/delete.svg";
|
||||
@@ -497,7 +498,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
|
||||
const buildCollectionNode = (database: ViewModels.Database, collection: ViewModels.Collection): TreeNode => {
|
||||
const children: TreeNode[] = [];
|
||||
children.push({
|
||||
label: collection.getLabel(),
|
||||
label: getItemName(),
|
||||
id: collection.isSampleCollection ? "sampleItems" : "",
|
||||
onClick: () => {
|
||||
collection.openTab();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react";
|
||||
import * as ko from "knockout";
|
||||
import * as React from "react";
|
||||
import { getItemName } from "Utils/APITypeUtils";
|
||||
import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg";
|
||||
import DeleteIcon from "../../../images/delete.svg";
|
||||
import GalleryIcon from "../../../images/GalleryIcon.svg";
|
||||
@@ -254,7 +255,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
||||
private buildCollectionNode(database: ViewModels.Database, collection: ViewModels.Collection): TreeNode {
|
||||
const children: TreeNode[] = [];
|
||||
children.push({
|
||||
label: collection.getLabel(),
|
||||
label: getItemName(),
|
||||
onClick: () => {
|
||||
collection.openTab();
|
||||
// push to most recent
|
||||
|
||||
@@ -45,7 +45,11 @@ export class PhoenixClient {
|
||||
}
|
||||
|
||||
public async allocateContainer(provisionData: IProvisionData): Promise<IResponse<IPhoenixServiceInfo>> {
|
||||
return this.executeContainerAssignmentOperation(provisionData, "allocate");
|
||||
return promiseRetry(() => this.executeContainerAssignmentOperation(provisionData, "allocate"), {
|
||||
retries: 4,
|
||||
maxTimeout: 20000,
|
||||
minTimeout: 20000,
|
||||
});
|
||||
}
|
||||
|
||||
public async resetContainer(provisionData: IProvisionData): Promise<IResponse<IPhoenixServiceInfo>> {
|
||||
@@ -80,9 +84,12 @@ export class PhoenixClient {
|
||||
}
|
||||
const phoenixError = responseJson as IPhoenixError;
|
||||
if (response.status === HttpStatusCodes.Forbidden) {
|
||||
throw new Error(this.ConvertToForbiddenErrorString(phoenixError));
|
||||
if (phoenixError.message === "Sequence contains no elements") {
|
||||
throw Error("Phoenix container allocation failed, please try again later.");
|
||||
}
|
||||
throw new AbortError(this.ConvertToForbiddenErrorString(phoenixError));
|
||||
}
|
||||
throw new Error(phoenixError.message);
|
||||
throw new AbortError(phoenixError.message);
|
||||
} catch (error) {
|
||||
error.status = response?.status;
|
||||
throw error;
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import * as Constants from "../../Common/Constants";
|
||||
import { configContext } from "../../ConfigContext";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility";
|
||||
import { userContext } from "../../UserContext";
|
||||
|
||||
export default class AuthHeadersUtil {
|
||||
public static async generateEncryptedToken(readOnly: boolean = false): Promise<DataModels.GenerateTokenResponse> {
|
||||
const url = configContext.BACKEND_ENDPOINT + "/api/tokens/generateToken" + AuthHeadersUtil._generateResourceUrl();
|
||||
const headers: any = { authorization: userContext.authorizationToken };
|
||||
headers[Constants.HttpHeaders.getReadOnlyKey] = readOnly;
|
||||
|
||||
const response = await fetch(url, { method: "POST", headers });
|
||||
const result = await response.json();
|
||||
// This API has a quirk where the response must be parsed to JSON twice
|
||||
return JSON.parse(result);
|
||||
}
|
||||
|
||||
private static _generateResourceUrl(): string {
|
||||
const { databaseAccount, resourceGroup, subscriptionId } = userContext;
|
||||
const apiKind: DataModels.ApiKind = DefaultExperienceUtility.getApiKindFromDefaultExperience(userContext.apiType);
|
||||
const accountEndpoint = databaseAccount?.properties?.documentEndpoint || "";
|
||||
const sid = subscriptionId || "";
|
||||
const rg = resourceGroup || "";
|
||||
const dba = databaseAccount?.name || "";
|
||||
const resourceUrl = encodeURIComponent(accountEndpoint);
|
||||
const rid = "";
|
||||
const rtype = "";
|
||||
return `?resourceUrl=${resourceUrl}&rid=${rid}&rtype=${rtype}&sid=${sid}&rg=${rg}&dba=${dba}&api=${apiKind}`;
|
||||
}
|
||||
}
|
||||
@@ -81,6 +81,9 @@ export class JupyterLabAppFactory {
|
||||
// Attach the widget to the dom.
|
||||
Widget.attach(panel, document.body);
|
||||
|
||||
// Switch focus to the terminal
|
||||
term.activate();
|
||||
|
||||
// Handle resize events.
|
||||
window.addEventListener("resize", () => {
|
||||
panel.update();
|
||||
|
||||
@@ -42,7 +42,7 @@ const createServerSettings = (props: TerminalProps): ServerConnection.ISettings
|
||||
return ServerConnection.makeSettings(options);
|
||||
};
|
||||
|
||||
const initTerminal = async (props: TerminalProps): Promise<ITerminalConnection> => {
|
||||
const initTerminal = async (props: TerminalProps): Promise<ITerminalConnection | undefined> => {
|
||||
// Initialize userContext (only properties which are needed by TelemetryProcessor)
|
||||
updateUserContext({
|
||||
subscriptionId: props.subscriptionId,
|
||||
|
||||
@@ -67,6 +67,7 @@ interface UserContext {
|
||||
partitionKey?: string;
|
||||
};
|
||||
readonly postgresConnectionStrParams?: PostgresConnectionStrParams;
|
||||
readonly isReplica?: boolean;
|
||||
collectionCreationDefaults: CollectionCreationDefaults;
|
||||
}
|
||||
|
||||
@@ -109,16 +110,19 @@ function updateUserContext(newContext: Partial<UserContext>): void {
|
||||
);
|
||||
|
||||
if (!localStorage.getItem(newContext.databaseAccount.id)) {
|
||||
if (newContext.apiType === "Postgres") {
|
||||
if (newContext.isTryCosmosDBSubscription || isNewAccount) {
|
||||
if (newContext.apiType === "Postgres" && !newContext.isReplica) {
|
||||
usePostgres.getState().setShowResetPasswordBubble(true);
|
||||
usePostgres.getState().setShowPostgreTeachingBubble(true);
|
||||
} else {
|
||||
useCarousel.getState().setShouldOpen(true);
|
||||
localStorage.setItem(newContext.databaseAccount.id, "true");
|
||||
traceOpen(Action.OpenCarousel);
|
||||
}
|
||||
} else if (newContext.apiType === "Postgres") {
|
||||
usePostgres.getState().setShowPostgreTeachingBubble(true);
|
||||
localStorage.setItem(newContext.databaseAccount.id, "true");
|
||||
}
|
||||
if (userContext.isTryCosmosDBSubscription || isNewAccount) {
|
||||
useCarousel.getState().setShouldOpen(true);
|
||||
usePostgres.getState().setShowResetPasswordBubble(true);
|
||||
localStorage.setItem(newContext.databaseAccount.id, "true");
|
||||
traceOpen(Action.OpenCarousel);
|
||||
}
|
||||
}
|
||||
}
|
||||
Object.assign(userContext, newContext);
|
||||
@@ -129,30 +133,28 @@ function apiType(account: DatabaseAccount | undefined): ApiType {
|
||||
return "SQL";
|
||||
}
|
||||
|
||||
return "Postgres";
|
||||
|
||||
// const capabilities = account.properties?.capabilities;
|
||||
// if (capabilities) {
|
||||
// if (capabilities.find((c) => c.name === "EnableCassandra")) {
|
||||
// return "Cassandra";
|
||||
// }
|
||||
// if (capabilities.find((c) => c.name === "EnableGremlin")) {
|
||||
// return "Gremlin";
|
||||
// }
|
||||
// if (capabilities.find((c) => c.name === "EnableMongo")) {
|
||||
// return "Mongo";
|
||||
// }
|
||||
// if (capabilities.find((c) => c.name === "EnableTable")) {
|
||||
// return "Tables";
|
||||
// }
|
||||
// }
|
||||
// if (account.kind === "MongoDB" || account.kind === "Parse") {
|
||||
// return "Mongo";
|
||||
// }
|
||||
// if (account.kind === "Postgres") {
|
||||
// return "Postgres";
|
||||
// }
|
||||
// return "SQL";
|
||||
const capabilities = account.properties?.capabilities;
|
||||
if (capabilities) {
|
||||
if (capabilities.find((c) => c.name === "EnableCassandra")) {
|
||||
return "Cassandra";
|
||||
}
|
||||
if (capabilities.find((c) => c.name === "EnableGremlin")) {
|
||||
return "Gremlin";
|
||||
}
|
||||
if (capabilities.find((c) => c.name === "EnableMongo")) {
|
||||
return "Mongo";
|
||||
}
|
||||
if (capabilities.find((c) => c.name === "EnableTable")) {
|
||||
return "Tables";
|
||||
}
|
||||
}
|
||||
if (account.kind === "MongoDB" || account.kind === "Parse") {
|
||||
return "Mongo";
|
||||
}
|
||||
if (account.kind === "Postgres") {
|
||||
return "Postgres";
|
||||
}
|
||||
return "SQL";
|
||||
}
|
||||
|
||||
export { userContext, updateUserContext };
|
||||
|
||||
@@ -74,3 +74,18 @@ export const getApiShortDisplayName = (): string => {
|
||||
return "Table API";
|
||||
}
|
||||
};
|
||||
|
||||
export const getItemName = (): string => {
|
||||
switch (userContext.apiType) {
|
||||
case "Tables":
|
||||
return "Entities";
|
||||
case "Cassandra":
|
||||
return "Rows";
|
||||
case "Gremlin":
|
||||
return "Graph";
|
||||
case "Mongo":
|
||||
return "Documents";
|
||||
default:
|
||||
return "Items";
|
||||
}
|
||||
};
|
||||
|
||||
40
src/Utils/NetworkUtility.ts
Normal file
40
src/Utils/NetworkUtility.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { userContext } from "UserContext";
|
||||
|
||||
const PortalIPs: { [key: string]: string[] } = {
|
||||
prod1: ["104.42.195.92", "40.76.54.131"],
|
||||
prod2: ["104.42.196.69"],
|
||||
mooncake: ["139.217.8.252"],
|
||||
blackforest: ["51.4.229.218"],
|
||||
fairfax: ["52.244.48.71"],
|
||||
ussec: ["29.26.26.67", "29.26.26.66"],
|
||||
usnat: ["7.28.202.68"],
|
||||
};
|
||||
|
||||
export const doNetworkSettingsAllowDataExplorerAccess = (): boolean => {
|
||||
const accountProperties = userContext.databaseAccount?.properties;
|
||||
|
||||
if (!accountProperties) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// public network access is disabled
|
||||
if (accountProperties.publicNetworkAccess !== "Enabled") {
|
||||
return false;
|
||||
}
|
||||
|
||||
const ipRules = accountProperties.ipRules;
|
||||
// public network access is set to "All networks"
|
||||
if (ipRules.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const portalIPs = PortalIPs[userContext.portalEnv];
|
||||
let numberOfMatches = 0;
|
||||
ipRules.forEach((ipRule) => {
|
||||
if (portalIPs.indexOf(ipRule.ipAddressOrRange) !== -1) {
|
||||
numberOfMatches++;
|
||||
}
|
||||
});
|
||||
|
||||
return numberOfMatches === portalIPs.length;
|
||||
};
|
||||
@@ -1,18 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { GenerateTokenResponse } from "../Contracts/DataModels";
|
||||
import AuthHeadersUtil from "../Platform/Hosted/Authorization";
|
||||
|
||||
export function useFullScreenURLs(): GenerateTokenResponse | undefined {
|
||||
const [state, setState] = useState<GenerateTokenResponse>();
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([AuthHeadersUtil.generateEncryptedToken(), AuthHeadersUtil.generateEncryptedToken(true)]).then(
|
||||
([readWriteResponse, readOnlyResponse]) =>
|
||||
setState({
|
||||
readWrite: readWriteResponse.readWrite,
|
||||
read: readOnlyResponse.read,
|
||||
})
|
||||
);
|
||||
}, []);
|
||||
return state;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
||||
import { useEffect, useState } from "react";
|
||||
import { doNetworkSettingsAllowDataExplorerAccess } from "Utils/NetworkUtility";
|
||||
import { applyExplorerBindings } from "../applyExplorerBindings";
|
||||
import { AuthType } from "../AuthType";
|
||||
import { AccountKind, Flights } from "../Common/Constants";
|
||||
@@ -364,7 +365,7 @@ function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) {
|
||||
});
|
||||
|
||||
if (inputs.isPostgresAccount) {
|
||||
updateUserContext({ apiType: "Postgres" });
|
||||
updateUserContext({ apiType: "Postgres", isReplica: !!inputs.isReplica });
|
||||
|
||||
if (inputs.connectionStringParams) {
|
||||
// TODO: Remove after the nodes param has been updated to be a flat array in the OSS extension
|
||||
@@ -381,6 +382,9 @@ function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) {
|
||||
}
|
||||
}
|
||||
|
||||
const isDataExplorerAccessAllowed = doNetworkSettingsAllowDataExplorerAccess();
|
||||
useTabs.getState().setShowNetworkSettingsWarning(!isDataExplorerAccessAllowed);
|
||||
|
||||
if (inputs.features) {
|
||||
Object.assign(userContext.features, extractFeatures(new URLSearchParams(inputs.features)));
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ interface TabsState {
|
||||
openedReactTabs: ReactTabKind[];
|
||||
activeTab: TabsBase | undefined;
|
||||
activeReactTab: ReactTabKind | undefined;
|
||||
showNetworkSettingsWarning: boolean;
|
||||
activateTab: (tab: TabsBase) => void;
|
||||
activateNewTab: (tab: TabsBase) => void;
|
||||
activateReactTab: (tabkind: ReactTabKind) => void;
|
||||
@@ -20,6 +21,7 @@ interface TabsState {
|
||||
closeAllNotebookTabs: (hardClose: boolean) => void;
|
||||
openAndActivateReactTab: (tabKind: ReactTabKind) => void;
|
||||
closeReactTab: (tabKind: ReactTabKind) => void;
|
||||
setShowNetworkSettingsWarning: (showWarning: boolean) => void;
|
||||
}
|
||||
|
||||
export enum ReactTabKind {
|
||||
@@ -33,6 +35,7 @@ export const useTabs: UseStore<TabsState> = create((set, get) => ({
|
||||
openedReactTabs: [ReactTabKind.Home],
|
||||
activeTab: undefined,
|
||||
activeReactTab: ReactTabKind.Home,
|
||||
showNetworkSettingsWarning: false,
|
||||
activateTab: (tab: TabsBase): void => {
|
||||
if (get().openedTabs.some((openedTab) => openedTab.tabId === tab.tabId)) {
|
||||
set({ activeTab: tab, activeReactTab: undefined });
|
||||
@@ -142,4 +145,5 @@ export const useTabs: UseStore<TabsState> = create((set, get) => ({
|
||||
|
||||
set({ openedReactTabs: updatedOpenedReactTabs });
|
||||
},
|
||||
setShowNetworkSettingsWarning: (showWarning: boolean) => set({ showNetworkSettingsWarning: showWarning }),
|
||||
}));
|
||||
|
||||
@@ -82,7 +82,6 @@
|
||||
"./src/Explorer/Tree/AccessibleVerticalList.ts",
|
||||
"./src/GitHub/GitHubConnector.ts",
|
||||
"./src/HostedExplorerChildFrame.ts",
|
||||
"./src/Platform/Hosted/Authorization.ts",
|
||||
"./src/Platform/Hosted/Components/MeControl.test.tsx",
|
||||
"./src/Platform/Hosted/Components/MeControl.tsx",
|
||||
"./src/Platform/Hosted/Components/SignInButton.tsx",
|
||||
@@ -126,7 +125,6 @@
|
||||
"./src/Utils/WindowUtils.ts",
|
||||
"./src/hooks/useConfig.ts",
|
||||
"./src/hooks/useDirectories.tsx",
|
||||
"./src/hooks/useFullScreenURLs.tsx",
|
||||
"./src/hooks/useGraphPhoto.tsx",
|
||||
"./src/hooks/useNotebookSnapshotStore.ts",
|
||||
"./src/hooks/usePortalAccessToken.tsx",
|
||||
@@ -165,4 +163,4 @@
|
||||
"src/Terminal/**/*",
|
||||
"src/Utils/arm/**/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user