mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-23 10:51:30 +00:00
Compare commits
12 Commits
PSQL_Shell
...
test_old_m
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
acd5974024 | ||
|
|
22f5fc13b2 | ||
|
|
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 |
@@ -33,6 +33,8 @@ export interface DatabaseAccountExtendedProperties {
|
||||
privateEndpointConnections?: unknown[];
|
||||
capacity?: { totalThroughputLimit: number };
|
||||
locations?: DatabaseAccountResponseLocation[];
|
||||
postgresqlEndpoint?: string;
|
||||
publicNetworkAccess?: string;
|
||||
}
|
||||
|
||||
export interface DatabaseAccountResponseLocation {
|
||||
@@ -566,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[];
|
||||
|
||||
@@ -85,11 +85,7 @@ export const createCollectionContextMenuButton = (
|
||||
iconSrc: HostedTerminalIcon,
|
||||
onClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
|
||||
if (useNotebook.getState().isShellEnabled) {
|
||||
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
|
||||
} else {
|
||||
selectedCollection && selectedCollection.onNewMongoShellClick();
|
||||
}
|
||||
selectedCollection && selectedCollection.onNewMongoShellClick();
|
||||
},
|
||||
label: useNotebook.getState().isShellEnabled ? "Open Mongo Shell" : "New Shell",
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* Wrapper around Notebook server terminal
|
||||
*/
|
||||
|
||||
import { useTerminal } from "hooks/useTerminal";
|
||||
import postRobot from "post-robot";
|
||||
import * as React from "react";
|
||||
import * as DataModels from "../../../Contracts/DataModels";
|
||||
@@ -40,6 +41,7 @@ export class NotebookTerminalComponent extends React.Component<NotebookTerminalC
|
||||
|
||||
handleFrameLoad(event: React.SyntheticEvent<HTMLIFrameElement, Event>): void {
|
||||
this.terminalWindow = (event.target as HTMLIFrameElement).contentWindow;
|
||||
useTerminal.getState().setTerminal(this.terminalWindow);
|
||||
this.sendPropsToTerminalFrame();
|
||||
}
|
||||
|
||||
@@ -75,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 (this.props.databaseAccount?.properties as any).postgresqlEndpoint;
|
||||
return this.props.databaseAccount?.properties.postgresqlEndpoint;
|
||||
}
|
||||
|
||||
if (terminalEndpoint) {
|
||||
|
||||
@@ -186,9 +186,7 @@ export default class Explorer {
|
||||
useNotebook.getState().setNotebookBasePath(userContext.features.notebookBasePath);
|
||||
}
|
||||
|
||||
if (userContext.apiType !== "Postgres") {
|
||||
this.refreshExplorer();
|
||||
}
|
||||
this.refreshExplorer();
|
||||
}
|
||||
|
||||
public async initiateAndRefreshNotebookList(): Promise<void> {
|
||||
@@ -1249,9 +1247,11 @@ export default class Explorer {
|
||||
}
|
||||
|
||||
public async refreshExplorer(): Promise<void> {
|
||||
userContext.authType === AuthType.ResourceToken
|
||||
? this.refreshDatabaseForResourceToken()
|
||||
: this.refreshAllDatabases();
|
||||
if (userContext.apiType !== "Postgres") {
|
||||
userContext.authType === AuthType.ResourceToken
|
||||
? this.refreshDatabaseForResourceToken()
|
||||
: this.refreshAllDatabases();
|
||||
}
|
||||
await useNotebook.getState().refreshNotebooksEnabledStateForAccount();
|
||||
|
||||
// TODO: remove reference to isNotebookEnabled and isNotebooksEnabledForAccount
|
||||
|
||||
@@ -85,14 +85,11 @@ export function createStaticCommandBarButtons(
|
||||
(userContext.apiType === "Mongo" &&
|
||||
useNotebook.getState().isShellEnabled &&
|
||||
selectedNodeState.isDatabaseNodeOrNoneSelected()) ||
|
||||
userContext.apiType === "Cassandra" ||
|
||||
userContext.apiType === "Postgres"
|
||||
userContext.apiType === "Cassandra"
|
||||
) {
|
||||
notebookButtons.push(createDivider());
|
||||
if (userContext.apiType === "Cassandra") {
|
||||
notebookButtons.push(createOpenCassandraTerminalButton(container));
|
||||
} else if (userContext.apiType === "Postgres") {
|
||||
notebookButtons.push(createOpenPsqlTerminalButton(container));
|
||||
} else {
|
||||
notebookButtons.push(createOpenMongoTerminalButton(container));
|
||||
}
|
||||
@@ -169,11 +166,7 @@ export function createContextCommandBarButtons(
|
||||
iconAlt: label,
|
||||
onCommandClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
||||
if (useNotebook.getState().isShellEnabled) {
|
||||
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
|
||||
} else {
|
||||
selectedCollection && selectedCollection.onNewMongoShellClick();
|
||||
}
|
||||
selectedCollection && selectedCollection.onNewMongoShellClick();
|
||||
},
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
@@ -612,16 +605,7 @@ function createStaticCommandBarButtonsForResourceToken(
|
||||
}
|
||||
|
||||
export function createPostgreButtons(container: Explorer): CommandButtonComponentProps[] {
|
||||
const postgreShellLabel = "Open PostgreSQL Shell";
|
||||
const openPostgreShellBtn = {
|
||||
iconSrc: HostedTerminalIcon,
|
||||
iconAlt: postgreShellLabel,
|
||||
onCommandClick: () => container.openNotebookTerminal(ViewModels.TerminalKind.Mongo),
|
||||
commandButtonLabel: postgreShellLabel,
|
||||
hasPopup: false,
|
||||
disabled: false,
|
||||
ariaLabel: postgreShellLabel,
|
||||
};
|
||||
const openPostgreShellBtn = createOpenPsqlTerminalButton(container);
|
||||
|
||||
return [openPostgreShellBtn];
|
||||
}
|
||||
|
||||
@@ -124,8 +124,9 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
|
||||
}
|
||||
|
||||
const firstWriteLocation =
|
||||
databaseAccount?.properties?.writeLocations &&
|
||||
databaseAccount?.properties?.writeLocations[0]?.locationName.toLowerCase();
|
||||
userContext.apiType === "Postgres"
|
||||
? databaseAccount?.location
|
||||
: databaseAccount?.properties?.writeLocations?.[0]?.locationName.toLowerCase();
|
||||
const disallowedLocationsUri = `${configContext.BACKEND_ENDPOINT}/api/disallowedLocations`;
|
||||
const authorizationHeader = getAuthorizationHeader();
|
||||
try {
|
||||
@@ -313,7 +314,10 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
|
||||
if (dbAccountAllowedInfo.status === HttpStatusCodes.OK) {
|
||||
if (dbAccountAllowedInfo?.type === PhoenixErrorType.PhoenixFlightFallback) {
|
||||
isPhoenixNotebooks = isPublicInternetAllowed && userContext.features.phoenixNotebooks === true;
|
||||
isPhoenixFeatures = isPublicInternetAllowed && userContext.features.phoenixFeatures === true;
|
||||
isPhoenixFeatures =
|
||||
isPublicInternetAllowed &&
|
||||
// phoenix needs to be enabled for Postgres accounts since the PSQL shell requires phoenix containers
|
||||
(userContext.features.phoenixFeatures === true || userContext.apiType === "Postgres");
|
||||
} else {
|
||||
isPhoenixNotebooks = isPhoenixFeatures = isPublicInternetAllowed;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
105
src/Explorer/Quickstart/PostgreQuickstartCommands.ts
Normal file
105
src/Explorer/Quickstart/PostgreQuickstartCommands.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
export const newTableCommand = `DROP SCHEMA IF EXISTS cosmosdb_tutorial CASCADE;
|
||||
CREATE SCHEMA cosmosdb_tutorial;
|
||||
SET search_path to cosmosdb_tutorial;
|
||||
CREATE TABLE github_users
|
||||
(
|
||||
user_id bigint,
|
||||
url text,
|
||||
login text,
|
||||
avatar_url text,
|
||||
gravatar_id text,
|
||||
display_login text
|
||||
);
|
||||
CREATE TABLE github_events
|
||||
(
|
||||
event_id bigint,
|
||||
event_type text,
|
||||
event_public boolean,
|
||||
repo_id bigint,
|
||||
payload jsonb,
|
||||
repo jsonb,
|
||||
user_id bigint,
|
||||
org jsonb,
|
||||
created_at timestamp
|
||||
);
|
||||
CREATE INDEX event_type_index ON github_events (event_type);
|
||||
CREATE INDEX payload_index ON github_events USING GIN (payload jsonb_path_ops);
|
||||
`;
|
||||
|
||||
export const newTableCommandForDisplay = `DROP SCHEMA IF EXISTS cosmosdb_tutorial CASCADE;
|
||||
CREATE SCHEMA cosmosdb_tutorial;
|
||||
|
||||
-- Using schema created for tutorial
|
||||
SET search_path to cosmosdb_tutorial;
|
||||
|
||||
CREATE TABLE github_users
|
||||
(
|
||||
user_id bigint,
|
||||
url text,
|
||||
login text,
|
||||
avatar_url text,
|
||||
gravatar_id text,
|
||||
display_login text
|
||||
);
|
||||
|
||||
CREATE TABLE github_events
|
||||
(
|
||||
event_id bigint,
|
||||
event_type text,
|
||||
event_public boolean,
|
||||
repo_id bigint,
|
||||
payload jsonb,
|
||||
repo jsonb,
|
||||
user_id bigint,
|
||||
org jsonb,
|
||||
created_at timestamp
|
||||
);
|
||||
|
||||
--Create indexes on events table
|
||||
CREATE INDEX event_type_index ON github_events (event_type);
|
||||
CREATE INDEX payload_index ON github_events USING GIN (payload jsonb_path_ops);`;
|
||||
|
||||
export const distributeTableCommand = `SET search_path to cosmosdb_tutorial;
|
||||
SELECT create_distributed_table('github_users', 'user_id');
|
||||
SELECT create_distributed_table('github_events', 'user_id');
|
||||
`;
|
||||
|
||||
export const distributeTableCommandForDisplay = `-- Using schema created for the tutorial
|
||||
SET search_path to cosmosdb_tutorial;
|
||||
|
||||
SELECT create_distributed_table('github_users', 'user_id');
|
||||
SELECT create_distributed_table('github_events', 'user_id');`;
|
||||
|
||||
export const loadDataCommand = `SET search_path to cosmosdb_tutorial;
|
||||
\\COPY github_users FROM PROGRAM 'wget -q -O - "$@" "https://examples.citusdata.com/users.csv"' WITH (FORMAT CSV);
|
||||
\\COPY github_events FROM PROGRAM 'wget -q -O - "$@" "https://examples.citusdata.com/events.csv"' WITH (FORMAT CSV);
|
||||
`;
|
||||
|
||||
export const loadDataCommandForDisplay = `-- Using schema created for the tutorial
|
||||
SET search_path to cosmosdb_tutorial;
|
||||
|
||||
-- download users and store in table
|
||||
\\COPY github_users FROM PROGRAM 'wget -q -O - "$@" "https://examples.citusdata.com/users.csv"' WITH (FORMAT CSV);
|
||||
\\COPY github_events FROM PROGRAM 'wget -q -O - "$@" "https://examples.citusdata.com/events.csv"' WITH (FORMAT CSV);`;
|
||||
|
||||
export const queryCommand = `SET search_path to cosmosdb_tutorial;
|
||||
SELECT count(*) FROM github_users;
|
||||
SELECT created_at, event_type, repo->>'name' AS repo_name
|
||||
FROM github_events
|
||||
WHERE user_id = 3861633;
|
||||
SELECT date_trunc('hour', created_at) AS hour, sum((payload->>'distinct_size')::int) AS num_commits FROM github_events WHERE event_type = 'PushEvent' AND payload @> '{"ref":"refs/heads/master"}' GROUP BY hour ORDER BY hour;
|
||||
`;
|
||||
|
||||
export const queryCommandForDisplay = `-- Using schema created for the tutorial
|
||||
SET search_path to cosmosdb_tutorial;
|
||||
|
||||
-- count all rows (across shards)
|
||||
SELECT count(*) FROM github_users;
|
||||
|
||||
-- Find all events for a single user.
|
||||
SELECT created_at, event_type, repo->>'name' AS repo_name
|
||||
FROM github_events
|
||||
WHERE user_id = 3861633;
|
||||
|
||||
-- Find the number of commits on the master branch per hour
|
||||
SELECT date_trunc('hour', created_at) AS hour, sum((payload->>'distinct_size')::int) AS num_commits FROM github_events WHERE event_type = 'PushEvent' AND payload @> '{"ref":"refs/heads/master"}' GROUP BY hour ORDER BY hour;`;
|
||||
@@ -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>
|
||||
);
|
||||
@@ -11,6 +11,17 @@ import {
|
||||
Text,
|
||||
TextField,
|
||||
} from "@fluentui/react";
|
||||
import {
|
||||
distributeTableCommand,
|
||||
distributeTableCommandForDisplay,
|
||||
loadDataCommand,
|
||||
loadDataCommandForDisplay,
|
||||
newTableCommand,
|
||||
newTableCommandForDisplay,
|
||||
queryCommand,
|
||||
queryCommandForDisplay,
|
||||
} from "Explorer/Quickstart/PostgreQuickstartCommands";
|
||||
import { useTerminal } from "hooks/useTerminal";
|
||||
import React, { useState } from "react";
|
||||
import Youtube from "react-youtube";
|
||||
import Pivot1SelectedIcon from "../../../images/Pivot1_selected.svg";
|
||||
@@ -35,65 +46,6 @@ enum GuideSteps {
|
||||
|
||||
export const QuickstartGuide: React.FC = (): JSX.Element => {
|
||||
const [currentStep, setCurrentStep] = useState<number>(0);
|
||||
const newTableCommand = `DROP SCHEMA IF EXISTS cosmosdb_tutorial CASCADE;
|
||||
CREATE SCHEMA cosmosdb_tutorial;
|
||||
|
||||
-- Using schema created for tutorial
|
||||
SET search_path to cosmosdb_tutorial;
|
||||
|
||||
CREATE TABLE github_users
|
||||
(
|
||||
user_id bigint,
|
||||
url text,
|
||||
login text,
|
||||
avatar_url text,
|
||||
gravatar_id text,
|
||||
display_login text
|
||||
);
|
||||
|
||||
CREATE TABLE github_events
|
||||
(
|
||||
event_id bigint,
|
||||
event_type text,
|
||||
event_public boolean,
|
||||
repo_id bigint,
|
||||
payload jsonb,
|
||||
repo jsonb,
|
||||
user_id bigint,
|
||||
org jsonb,
|
||||
created_at timestamp
|
||||
);
|
||||
|
||||
--Create indexes on events table
|
||||
CREATE INDEX event_type_index ON github_events (event_type);
|
||||
CREATE INDEX payload_index ON github_events USING GIN (payload jsonb_path_ops); `;
|
||||
|
||||
const distributeTableCommand = `-- Using schema created for the tutorial
|
||||
SET search_path to cosmosdb_tutorial;
|
||||
|
||||
SELECT create_distributed_table('github_users', 'user_id');
|
||||
SELECT create_distributed_table('github_events', 'user_id'); `;
|
||||
|
||||
const loadDataCommand = `-- Using schema created for the tutorial
|
||||
SET search_path to cosmosdb_tutorial;
|
||||
|
||||
-- download users and store in table
|
||||
\\COPY github_users FROM PROGRAM 'curl https://examples.citusdata.com/users.csv' WITH (FORMAT CSV)
|
||||
\\COPY github_events FROM PROGRAM 'curl https://examples.citusdata.com/events.csv' WITH (FORMAT CSV) `;
|
||||
|
||||
const queryCommand = `-- Using schema created for the tutorial
|
||||
SET search_path to cosmosdb_tutorial;
|
||||
|
||||
-- count all rows (across shards)
|
||||
SELECT count(*) FROM github_users;
|
||||
|
||||
-- Find all events for a single user.
|
||||
SELECT created_at, event_type, repo->>'name' AS repo_name
|
||||
FROM github_events
|
||||
WHERE user_id = 3861633;
|
||||
|
||||
-- Find the number of commits on the master branch per hour
|
||||
SELECT date_trunc('hour', created_at) AS hour, sum((payload->>'distinct_size')::int) AS num_commits FROM github_events WHERE event_type = 'PushEvent' AND payload @> '{"ref":"refs/heads/master"}' GROUP BY hour ORDER BY hour; `;
|
||||
|
||||
const onCopyBtnClicked = (selector: string): void => {
|
||||
const textfield: HTMLInputElement = document.querySelector(selector);
|
||||
@@ -143,14 +95,19 @@ SELECT date_trunc('hour', created_at) AS hour, sum((payload->>'distinct_size')::
|
||||
<Stack style={{ paddingTop: 8, height: "100%", width: "100%" }}>
|
||||
<Stack style={{ flexGrow: 1, padding: "0 20px", overflow: "auto" }}>
|
||||
<Text variant="xxLarge">Quick start guide</Text>
|
||||
<Text variant="medium">Gettings started in Cosmos DB</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>
|
||||
@@ -158,8 +115,12 @@ SELECT date_trunc('hour', created_at) AS hour, sum((payload->>'distinct_size')::
|
||||
<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="Jvgh64rvdXU" style={{ margin: "20px 0" }} opts={{ width: "90%" }} />
|
||||
<Youtube videoId="nT64dFSfiUo" style={{ margin: "20px 0" }} opts={{ width: "90%" }} />
|
||||
</Stack>
|
||||
</PivotItem>
|
||||
<PivotItem
|
||||
@@ -169,15 +130,20 @@ SELECT date_trunc('hour', created_at) AS hour, sum((payload->>'distinct_size')::
|
||||
onClick={() => setCurrentStep(1)}
|
||||
>
|
||||
<Stack style={{ marginTop: 20 }}>
|
||||
<Text>Let’s create two tables github_users and github_events in “cosmosdb_tutorial” schema.</Text>
|
||||
<DefaultButton style={{ marginTop: 16, width: 150 }}>Create new table</DefaultButton>
|
||||
<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)}
|
||||
>
|
||||
Create new table
|
||||
</DefaultButton>
|
||||
<Stack horizontal style={{ marginTop: 16 }}>
|
||||
<TextField
|
||||
id="newTableCommand"
|
||||
multiline
|
||||
rows={5}
|
||||
readOnly
|
||||
defaultValue={newTableCommand}
|
||||
defaultValue={newTableCommandForDisplay}
|
||||
styles={{
|
||||
root: { width: "90%" },
|
||||
field: {
|
||||
@@ -194,7 +160,7 @@ SELECT date_trunc('hour', created_at) AS hour, sum((payload->>'distinct_size')::
|
||||
onClick={() => onCopyBtnClicked("#newTableCommand")}
|
||||
/>
|
||||
</Stack>
|
||||
<Youtube videoId="Jvgh64rvdXU" style={{ margin: "20px 0" }} opts={{ width: "90%" }} />
|
||||
<Youtube videoId="il_sA6U1WcY" style={{ margin: "20px 0" }} opts={{ width: "90%" }} />
|
||||
</Stack>
|
||||
</PivotItem>
|
||||
<PivotItem
|
||||
@@ -210,14 +176,19 @@ SELECT date_trunc('hour', created_at) AS hour, sum((payload->>'distinct_size')::
|
||||
<br />
|
||||
We are choosing “user_id” as the distribution column for our sample dataset.
|
||||
</Text>
|
||||
<DefaultButton style={{ marginTop: 16, width: 200 }}>Create distributed table</DefaultButton>
|
||||
<DefaultButton
|
||||
style={{ marginTop: 16, width: 200 }}
|
||||
onClick={() => useTerminal.getState().sendMessage(distributeTableCommand)}
|
||||
>
|
||||
Create distributed table
|
||||
</DefaultButton>
|
||||
<Stack horizontal style={{ marginTop: 16 }}>
|
||||
<TextField
|
||||
id="distributeTableCommand"
|
||||
multiline
|
||||
rows={5}
|
||||
readOnly
|
||||
defaultValue={distributeTableCommand}
|
||||
defaultValue={distributeTableCommandForDisplay}
|
||||
styles={{
|
||||
root: { width: "90%" },
|
||||
field: {
|
||||
@@ -234,7 +205,7 @@ SELECT date_trunc('hour', created_at) AS hour, sum((payload->>'distinct_size')::
|
||||
onClick={() => onCopyBtnClicked("#distributeTableCommand")}
|
||||
/>
|
||||
</Stack>
|
||||
<Youtube videoId="Jvgh64rvdXU" style={{ margin: "20px 0" }} opts={{ width: "90%" }} />
|
||||
<Youtube videoId="kCCDRRrN1r0" style={{ margin: "20px 0" }} opts={{ width: "90%" }} />
|
||||
</Stack>
|
||||
</PivotItem>
|
||||
<PivotItem
|
||||
@@ -245,14 +216,19 @@ SELECT date_trunc('hour', created_at) AS hour, sum((payload->>'distinct_size')::
|
||||
>
|
||||
<Stack style={{ marginTop: 20 }}>
|
||||
<Text>Let's load the two tables with a sample dataset generated from the GitHub API.</Text>
|
||||
<DefaultButton style={{ marginTop: 16, width: 110 }}>Load data</DefaultButton>
|
||||
<DefaultButton
|
||||
style={{ marginTop: 16, width: 110 }}
|
||||
onClick={() => useTerminal.getState().sendMessage(loadDataCommand)}
|
||||
>
|
||||
Load data
|
||||
</DefaultButton>
|
||||
<Stack horizontal style={{ marginTop: 16 }}>
|
||||
<TextField
|
||||
id="loadDataCommand"
|
||||
multiline
|
||||
rows={5}
|
||||
readOnly
|
||||
defaultValue={loadDataCommand}
|
||||
defaultValue={loadDataCommandForDisplay}
|
||||
styles={{
|
||||
root: { width: "90%" },
|
||||
field: {
|
||||
@@ -269,7 +245,7 @@ SELECT date_trunc('hour', created_at) AS hour, sum((payload->>'distinct_size')::
|
||||
onClick={() => onCopyBtnClicked("#loadDataCommand")}
|
||||
/>
|
||||
</Stack>
|
||||
<Youtube videoId="Jvgh64rvdXU" style={{ margin: "20px 0" }} opts={{ width: "90%" }} />
|
||||
<Youtube videoId="XSMEE2tujEk" style={{ margin: "20px 0" }} opts={{ width: "90%" }} />
|
||||
</Stack>
|
||||
</PivotItem>
|
||||
<PivotItem
|
||||
@@ -282,14 +258,19 @@ SELECT date_trunc('hour', created_at) AS hour, sum((payload->>'distinct_size')::
|
||||
<Text>
|
||||
Congratulations on creating and distributing your tables. Now, it's time to run your first query!
|
||||
</Text>
|
||||
<DefaultButton style={{ marginTop: 16, width: 115 }}>Try queries</DefaultButton>
|
||||
<DefaultButton
|
||||
style={{ marginTop: 16, width: 115 }}
|
||||
onClick={() => useTerminal.getState().sendMessage(queryCommand)}
|
||||
>
|
||||
Try queries
|
||||
</DefaultButton>
|
||||
<Stack horizontal style={{ marginTop: 16 }}>
|
||||
<TextField
|
||||
id="queryCommand"
|
||||
multiline
|
||||
rows={5}
|
||||
readOnly
|
||||
defaultValue={queryCommand}
|
||||
defaultValue={queryCommandForDisplay}
|
||||
styles={{
|
||||
root: { width: "90%" },
|
||||
field: {
|
||||
@@ -306,7 +287,7 @@ SELECT date_trunc('hour', created_at) AS hour, sum((payload->>'distinct_size')::
|
||||
onClick={() => onCopyBtnClicked("#queryCommand")}
|
||||
/>
|
||||
</Stack>
|
||||
<Youtube videoId="Jvgh64rvdXU" 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,
|
||||
@@ -329,7 +337,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
iconSrc: PowerShellIcon,
|
||||
title: "PostgreSQL Shell",
|
||||
description: "Create table and interact with data using PostgreSQL’s shell interface",
|
||||
onClick: () => this.container.openNotebookTerminal(TerminalKind.Mongo),
|
||||
onClick: () => this.container.openNotebookTerminal(TerminalKind.Postgres),
|
||||
};
|
||||
heroes.push(postgreShellBtn);
|
||||
} else {
|
||||
@@ -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,14 +17,42 @@ interface QuickstartTabProps {
|
||||
|
||||
export const QuickstartTab: React.FC<QuickstartTabProps> = ({ explorer }: QuickstartTabProps): JSX.Element => {
|
||||
const notebookServerInfo = useNotebook((state) => state.notebookServerInfo);
|
||||
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();
|
||||
}, []);
|
||||
const getNotebookServerInfo = (): NotebookWorkspaceConnectionInfo => ({
|
||||
authToken: notebookServerInfo.authToken,
|
||||
notebookServerEndpoint: `${notebookServerInfo.notebookServerEndpoint?.replace(/\/+$/, "")}/mongo`,
|
||||
forwardingId: notebookServerInfo.forwardingId,
|
||||
});
|
||||
|
||||
return (
|
||||
<Stack style={{ width: "100%" }} horizontal>
|
||||
@@ -28,15 +60,24 @@ export const QuickstartTab: React.FC<QuickstartTabProps> = ({ explorer }: Quicks
|
||||
<QuickstartGuide />
|
||||
</Stack>
|
||||
<Stack style={{ width: "50%", borderLeft: "black solid 1px" }}>
|
||||
{notebookServerInfo?.notebookServerEndpoint && (
|
||||
{!isAllPublicIPAddressEnabled && <QuickstartFirewallNotification />}
|
||||
{isAllPublicIPAddressEnabled && notebookServerInfo?.notebookServerEndpoint && (
|
||||
<NotebookTerminalComponent
|
||||
notebookServerInfo={getNotebookServerInfo()}
|
||||
databaseAccount={userContext.databaseAccount}
|
||||
tabId="EmbbedTerminal"
|
||||
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}`;
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
* JupyterLab applications based on jupyterLab components
|
||||
*/
|
||||
import { ServerConnection, TerminalManager } from "@jupyterlab/services";
|
||||
import { IMessage } from "@jupyterlab/services/lib/terminal/terminal";
|
||||
import { IMessage, ITerminalConnection } from "@jupyterlab/services/lib/terminal/terminal";
|
||||
import { Terminal } from "@jupyterlab/terminal";
|
||||
import { Panel, Widget } from "@phosphor/widgets";
|
||||
import { userContext } from "UserContext";
|
||||
@@ -46,7 +46,7 @@ export class JupyterLabAppFactory {
|
||||
}
|
||||
}
|
||||
|
||||
public async createTerminalApp(serverSettings: ServerConnection.ISettings) {
|
||||
public async createTerminalApp(serverSettings: ServerConnection.ISettings): Promise<ITerminalConnection | undefined> {
|
||||
const manager = new TerminalManager({
|
||||
serverSettings: serverSettings,
|
||||
});
|
||||
@@ -68,7 +68,7 @@ export class JupyterLabAppFactory {
|
||||
|
||||
if (!term) {
|
||||
console.error("Failed starting terminal");
|
||||
return;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
term.title.closable = false;
|
||||
@@ -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();
|
||||
@@ -90,5 +93,7 @@ export class JupyterLabAppFactory {
|
||||
window.addEventListener("unload", () => {
|
||||
panel.dispose();
|
||||
});
|
||||
|
||||
return session;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ServerConnection } from "@jupyterlab/services";
|
||||
import { IMessage, ITerminalConnection } from "@jupyterlab/services/lib/terminal/terminal";
|
||||
import "@jupyterlab/terminal/style/index.css";
|
||||
import { MessageTypes } from "Contracts/ExplorerContracts";
|
||||
import postRobot from "post-robot";
|
||||
@@ -41,7 +42,7 @@ const createServerSettings = (props: TerminalProps): ServerConnection.ISettings
|
||||
return ServerConnection.makeSettings(options);
|
||||
};
|
||||
|
||||
const initTerminal = async (props: TerminalProps) => {
|
||||
const initTerminal = async (props: TerminalProps): Promise<ITerminalConnection | undefined> => {
|
||||
// Initialize userContext (only properties which are needed by TelemetryProcessor)
|
||||
updateUserContext({
|
||||
subscriptionId: props.subscriptionId,
|
||||
@@ -55,10 +56,12 @@ const initTerminal = async (props: TerminalProps) => {
|
||||
const startTime = TelemetryProcessor.traceStart(Action.OpenTerminal, data);
|
||||
|
||||
try {
|
||||
await new JupyterLabAppFactory(() => closeTab(props.tabId)).createTerminalApp(serverSettings);
|
||||
const session = await new JupyterLabAppFactory(() => closeTab(props.tabId)).createTerminalApp(serverSettings);
|
||||
TelemetryProcessor.traceSuccess(Action.OpenTerminal, data, startTime);
|
||||
return session;
|
||||
} catch (error) {
|
||||
TelemetryProcessor.traceFailure(Action.OpenTerminal, data, startTime);
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -70,6 +73,7 @@ const closeTab = (tabId: string): void => {
|
||||
};
|
||||
|
||||
const main = async (): Promise<void> => {
|
||||
let session: ITerminalConnection | undefined;
|
||||
postRobot.on(
|
||||
"props",
|
||||
{
|
||||
@@ -80,7 +84,22 @@ const main = async (): Promise<void> => {
|
||||
// Typescript definition for event is wrong. So read props by casting to <any>
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const props = (event as any).data as TerminalProps;
|
||||
await initTerminal(props);
|
||||
session = await initTerminal(props);
|
||||
}
|
||||
);
|
||||
|
||||
postRobot.on(
|
||||
"sendMessage",
|
||||
{
|
||||
window: window.parent,
|
||||
domain: window.location.origin,
|
||||
},
|
||||
async (event) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const message = (event as any).data as IMessage;
|
||||
if (session) {
|
||||
session.send(message);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 { useTabs } from "hooks/useTabs";
|
||||
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";
|
||||
@@ -100,7 +101,11 @@ async function configureHosted(): Promise<Explorer> {
|
||||
}
|
||||
|
||||
if (event.data?.type === MessageTypes.CloseTab) {
|
||||
useTabs.getState().closeTabsByComparator((tab) => tab.tabId === event.data?.data?.tabId);
|
||||
if (event.data?.data?.tabId === "QuickstartPSQLShell") {
|
||||
useTabs.getState().closeReactTab(ReactTabKind.Quickstart);
|
||||
} else {
|
||||
useTabs.getState().closeTabsByComparator((tab) => tab.tabId === event.data?.data?.tabId);
|
||||
}
|
||||
}
|
||||
},
|
||||
false
|
||||
@@ -290,7 +295,11 @@ async function configurePortal(): Promise<Explorer> {
|
||||
} else if (shouldForwardMessage(message, event.origin)) {
|
||||
sendMessage(message);
|
||||
} else if (event.data?.type === MessageTypes.CloseTab) {
|
||||
useTabs.getState().closeTabsByComparator((tab) => tab.tabId === event.data?.data?.tabId);
|
||||
if (event.data?.data?.tabId === "QuickstartPSQLShell") {
|
||||
useTabs.getState().closeReactTab(ReactTabKind.Quickstart);
|
||||
} else {
|
||||
useTabs.getState().closeTabsByComparator((tab) => tab.tabId === event.data?.data?.tabId);
|
||||
}
|
||||
}
|
||||
},
|
||||
false
|
||||
@@ -356,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
|
||||
@@ -373,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 }),
|
||||
}));
|
||||
|
||||
26
src/hooks/useTerminal.ts
Normal file
26
src/hooks/useTerminal.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import postRobot from "post-robot";
|
||||
import create, { UseStore } from "zustand";
|
||||
|
||||
interface TerminalState {
|
||||
terminalWindow: Window;
|
||||
setTerminal: (terminalWindow: Window) => void;
|
||||
sendMessage: (message: string) => void;
|
||||
}
|
||||
|
||||
export const useTerminal: UseStore<TerminalState> = create((set, get) => ({
|
||||
terminalWindow: undefined,
|
||||
setTerminal: (terminalWindow: Window) => {
|
||||
set({ terminalWindow });
|
||||
},
|
||||
sendMessage: (message: string) => {
|
||||
const terminalWindow = get().terminalWindow;
|
||||
postRobot.send(
|
||||
terminalWindow,
|
||||
"sendMessage",
|
||||
{ type: "stdin", content: [message] },
|
||||
{
|
||||
domain: window.location.origin,
|
||||
}
|
||||
);
|
||||
},
|
||||
}));
|
||||
@@ -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