Add connect tab for new quick start (#1273)

* Add connect tab

* Error handling

* Add button to open quick start blade

* Handle scenario where user don't have write access
This commit is contained in:
victor-meng 2022-05-20 16:38:38 -07:00 committed by GitHub
parent dc83bf6fa0
commit 2ab60a7a40
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 289 additions and 19 deletions

View File

@ -34,6 +34,7 @@ export enum MessageTypes {
CreateSparkPool, CreateSparkPool,
RefreshDatabaseAccount, RefreshDatabaseAccount,
CloseTab, CloseTab,
OpenQuickstartBlade,
} }
export { Versions, ActionContracts, Diagnostics }; export { Versions, ActionContracts, Diagnostics };

View File

@ -2,6 +2,7 @@
* Accordion top class * Accordion top class
*/ */
import { Coachmark, DirectionalHint, Image, Link, Stack, TeachingBubbleContent, Text } from "@fluentui/react"; import { Coachmark, DirectionalHint, Image, Link, Stack, TeachingBubbleContent, Text } from "@fluentui/react";
import { useTabs } from "hooks/useTabs";
import * as React from "react"; import * as React from "react";
import AddDatabaseIcon from "../../../images/AddDatabase.svg"; import AddDatabaseIcon from "../../../images/AddDatabase.svg";
import NewQueryIcon from "../../../images/AddSqlQuery_16x16.svg"; import NewQueryIcon from "../../../images/AddSqlQuery_16x16.svg";
@ -237,8 +238,7 @@ export class SplashScreen extends React.Component<SplashScreenProps, SplashScree
iconSrc: ConnectIcon, iconSrc: ConnectIcon,
title: "Connect", title: "Connect",
description: "Prefer using your own choice of tooling? Find the connection string you need to connect", description: "Prefer using your own choice of tooling? Find the connection string you need to connect",
// TODO: replace onClick function onClick: () => useTabs.getState().openAndActivateConnectTab(),
onClick: () => 2,
}; };
heroes.push(connectBtn); heroes.push(connectBtn);
} else { } else {

View File

@ -0,0 +1,232 @@
import {
IconButton,
ITextFieldStyles,
Link,
Pivot,
PivotItem,
PrimaryButton,
Stack,
Text,
TextField,
} from "@fluentui/react";
import { handleError } from "Common/ErrorHandlingUtils";
import { sendMessage } from "Common/MessageHandler";
import { MessageTypes } from "Contracts/ExplorerContracts";
import React, { useEffect, useState } from "react";
import { userContext } from "UserContext";
import { listKeys, listReadOnlyKeys } from "Utils/arm/generatedClients/cosmos/databaseAccounts";
import {
DatabaseAccountListKeysResult,
DatabaseAccountListReadOnlyKeysResult,
} from "Utils/arm/generatedClients/cosmos/types";
export const ConnectTab: React.FC = (): JSX.Element => {
const [primaryMasterKey, setPrimaryMasterKey] = useState<string>("");
const [secondaryMasterKey, setSecondaryMasterKey] = useState<string>("");
const [primaryReadonlyMasterKey, setPrimaryReadonlyMasterKey] = useState<string>("");
const [secondaryReadonlyMasterKey, setSecondaryReadonlyMasterKey] = useState<string>("");
const uri: string = userContext.databaseAccount.properties?.documentEndpoint;
const primaryConnectionStr = `AccountEndpoint=${uri};AccountKey=${primaryMasterKey}`;
const secondaryConnectionStr = `AccountEndpoint=${uri};AccountKey=${secondaryMasterKey}`;
const primaryReadonlyConnectionStr = `AccountEndpoint=${uri};AccountKey=${primaryReadonlyMasterKey}`;
const secondaryReadonlyConnectionStr = `AccountEndpoint=${uri};AccountKey=${secondaryReadonlyMasterKey}`;
useEffect(() => {
fetchKeys();
}, []);
const fetchKeys = async (): Promise<void> => {
try {
if (userContext.hasWriteAccess) {
const listKeysResult: DatabaseAccountListKeysResult = await listKeys(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name
);
setPrimaryMasterKey(listKeysResult.primaryMasterKey);
setSecondaryMasterKey(listKeysResult.secondaryMasterKey);
setPrimaryReadonlyMasterKey(listKeysResult.primaryReadonlyMasterKey);
setSecondaryReadonlyMasterKey(listKeysResult.secondaryReadonlyMasterKey);
} else {
const listReadonlyKeysResult: DatabaseAccountListReadOnlyKeysResult = await listReadOnlyKeys(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name
);
setPrimaryReadonlyMasterKey(listReadonlyKeysResult.primaryReadonlyMasterKey);
setSecondaryReadonlyMasterKey(listReadonlyKeysResult.secondaryReadonlyMasterKey);
}
} catch (error) {
handleError(error, "listKeys", "listKeys request has failed: ");
throw error;
}
};
const onCopyBtnClicked = (selector: string): void => {
const textfield: HTMLInputElement = document.querySelector(selector);
textfield.select();
document.execCommand("copy");
};
const textfieldStyles: Partial<ITextFieldStyles> = {
root: { width: "100%" },
field: { backgroundColor: "rgb(230, 230, 230)" },
fieldGroup: { borderColor: "rgb(138, 136, 134)" },
};
return (
<div style={{ width: "100%", padding: 16 }}>
<Stack style={{ marginLeft: 10 }}>
<Text variant="medium">
Ensure you have the right networking / access configuration before you establish the connection with your app
or 3rd party tool.
</Text>
<Link style={{ fontSize: 14 }} target="_blank" href="">
Configure networking in Azure portal
</Link>
</Stack>
<Pivot>
{userContext.hasWriteAccess && (
<PivotItem headerText="Read-write Keys">
<Stack style={{ margin: 10 }}>
<Stack horizontal verticalAlign="end" style={{ marginBottom: 8 }}>
<TextField label="URI" id="uriTextfield" readOnly value={uri} styles={textfieldStyles} />
<IconButton iconProps={{ iconName: "Copy" }} onClick={() => onCopyBtnClicked("#uriTextfield")} />
</Stack>
<Stack horizontal verticalAlign="end" style={{ marginBottom: 8 }}>
<TextField
label="PRIMARY KEY"
id="primaryKeyTextfield"
readOnly
value={primaryMasterKey}
styles={textfieldStyles}
/>
<IconButton iconProps={{ iconName: "Copy" }} onClick={() => onCopyBtnClicked("#primaryKeyTextfield")} />
</Stack>
<Stack horizontal verticalAlign="end" style={{ marginBottom: 8 }}>
<TextField
label="SECONDARY KEY"
id="secondaryKeyTextfield"
readOnly
value={secondaryMasterKey}
styles={textfieldStyles}
/>
<IconButton
iconProps={{ iconName: "Copy" }}
onClick={() => onCopyBtnClicked("#secondaryKeyTextfield")}
/>
</Stack>
<Stack horizontal verticalAlign="end" style={{ marginBottom: 8 }}>
<TextField
label="PRIMARY CONNECTION STRING"
id="primaryConStrTextfield"
readOnly
value={primaryConnectionStr}
styles={textfieldStyles}
/>
<IconButton
iconProps={{ iconName: "Copy" }}
onClick={() => onCopyBtnClicked("#primaryConStrTextfield")}
/>
</Stack>
<Stack horizontal verticalAlign="end" style={{ marginBottom: 8 }}>
<TextField
label="SECONDARY CONNECTION STRING"
id="secondaryConStrTextfield"
readOnly
value={secondaryConnectionStr}
styles={textfieldStyles}
/>
<IconButton
iconProps={{ iconName: "Copy" }}
onClick={() => onCopyBtnClicked("#secondaryConStrTextfield")}
/>
</Stack>
</Stack>
</PivotItem>
)}
<PivotItem headerText="Read-only Keys">
<Stack style={{ margin: 10 }}>
<Stack horizontal verticalAlign="end" style={{ marginBottom: 8 }}>
<TextField label="URI" id="uriReadOnlyTextfield" readOnly value={uri} styles={textfieldStyles} />
<IconButton iconProps={{ iconName: "Copy" }} onClick={() => onCopyBtnClicked("#uriReadOnlyTextfield")} />
</Stack>
<Stack horizontal verticalAlign="end" style={{ marginBottom: 8 }}>
<TextField
label="PRIMARY READ-ONLY KEY"
id="primaryReadonlyKeyTextfield"
readOnly
value={primaryReadonlyMasterKey}
styles={textfieldStyles}
/>
<IconButton
iconProps={{ iconName: "Copy" }}
onClick={() => onCopyBtnClicked("#primaryReadonlyKeyTextfield")}
/>
</Stack>
<Stack horizontal verticalAlign="end" style={{ marginBottom: 8 }}>
<TextField
label="SECONDARY READ-ONLY KEY"
id="secondaryReadonlyKeyTextfield"
readOnly
value={secondaryReadonlyMasterKey}
styles={textfieldStyles}
/>
<IconButton
iconProps={{ iconName: "Copy" }}
onClick={() => onCopyBtnClicked("#secondaryReadonlyKeyTextfield")}
/>
</Stack>
<Stack horizontal verticalAlign="end" style={{ marginBottom: 8 }}>
<TextField
label="PRIMARY READ-ONLY CONNECTION STRING"
id="primaryReadonlyConStrTextfield"
readOnly
value={primaryReadonlyConnectionStr}
styles={textfieldStyles}
/>
<IconButton
iconProps={{ iconName: "Copy" }}
onClick={() => onCopyBtnClicked("#primaryReadonlyConStrTextfield")}
/>
</Stack>
<Stack horizontal verticalAlign="end" style={{ marginBottom: 8 }}>
<TextField
label="SECONDARY READ-ONLY CONNECTION STRING"
id="secondaryReadonlyConStrTextfield"
value={secondaryReadonlyConnectionStr}
readOnly
styles={textfieldStyles}
/>
<IconButton
iconProps={{ iconName: "Copy" }}
onClick={() => onCopyBtnClicked("#secondaryReadonlyConStrTextfield")}
/>
</Stack>
</Stack>
</PivotItem>
</Pivot>
<Stack style={{ margin: 10 }}>
<Text style={{ fontWeight: 600, marginBottom: 8 }}>Download sample app</Text>
<Text style={{ marginBottom: 8 }}>
Dont have an app ready? No worries, download one of our sample app with a platform of your choice. Connection
string is already included in the app.
</Text>
<PrimaryButton
style={{ width: 185 }}
onClick={() =>
sendMessage({
type: MessageTypes.OpenQuickstartBlade,
})
}
text="Download sample app"
/>
</Stack>
</div>
);
};

View File

@ -1,4 +1,5 @@
import { CollectionTabKind } from "Contracts/ViewModels"; import { CollectionTabKind } from "Contracts/ViewModels";
import { ConnectTab } from "Explorer/Tabs/ConnectTab";
import { useTeachingBubble } from "hooks/useTeachingBubble"; import { useTeachingBubble } from "hooks/useTeachingBubble";
import ko from "knockout"; import ko from "knockout";
import React, { MutableRefObject, useEffect, useRef, useState } from "react"; import React, { MutableRefObject, useEffect, useRef, useState } from "react";
@ -12,17 +13,21 @@ type Tab = TabsBase | (TabsBase & { render: () => JSX.Element });
export const Tabs = (): JSX.Element => { export const Tabs = (): JSX.Element => {
const { openedTabs, activeTab } = useTabs(); const { openedTabs, activeTab } = useTabs();
const isConnectTabOpen = useTabs((state) => state.isConnectTabOpen);
const isConnectTabActive = useTabs((state) => state.isConnectTabActive);
return ( return (
<div className="tabsManagerContainer"> <div className="tabsManagerContainer">
<div id="content" className="flexContainer hideOverflows"> <div id="content" className="flexContainer hideOverflows">
<div className="nav-tabs-margin"> <div className="nav-tabs-margin">
<ul className="nav nav-tabs level navTabHeight" id="navTabs" role="tablist"> <ul className="nav nav-tabs level navTabHeight" id="navTabs" role="tablist">
{isConnectTabOpen && <TabNav key="connect" tab={undefined} active={isConnectTabActive} />}
{openedTabs.map((tab) => ( {openedTabs.map((tab) => (
<TabNav key={tab.tabId} tab={tab} active={activeTab === tab} /> <TabNav key={tab.tabId} tab={tab} active={activeTab === tab} />
))} ))}
</ul> </ul>
</div> </div>
<div className="tabPanesContainer"> <div className="tabPanesContainer">
{isConnectTabActive && <ConnectTab />}
{openedTabs.map((tab) => ( {openedTabs.map((tab) => (
<TabPane key={tab.tabId} tab={tab} active={activeTab === tab} /> <TabPane key={tab.tabId} tab={tab} active={activeTab === tab} />
))} ))}
@ -35,6 +40,7 @@ export const Tabs = (): JSX.Element => {
function TabNav({ tab, active }: { tab: Tab; active: boolean }) { function TabNav({ tab, active }: { tab: Tab; active: boolean }) {
const [hovering, setHovering] = useState(false); const [hovering, setHovering] = useState(false);
const focusTab = useRef<HTMLLIElement>() as MutableRefObject<HTMLLIElement>; const focusTab = useRef<HTMLLIElement>() as MutableRefObject<HTMLLIElement>;
const tabId = tab ? tab.tabId : "connect";
useEffect(() => { useEffect(() => {
if (active && focusTab.current) { if (active && focusTab.current) {
@ -45,27 +51,27 @@ function TabNav({ tab, active }: { tab: Tab; active: boolean }) {
<li <li
onMouseOver={() => setHovering(true)} onMouseOver={() => setHovering(true)}
onMouseLeave={() => setHovering(false)} onMouseLeave={() => setHovering(false)}
onClick={() => tab.onTabClick()} onClick={() => (tab ? tab.onTabClick() : useTabs.getState().activateConnectTab())}
onKeyPress={({ nativeEvent: e }) => tab.onKeyPressActivate(undefined, e)} onKeyPress={({ nativeEvent: e }) => (tab ? tab.onKeyPressActivate(undefined, e) : onKeyPressConnectTab(e))}
className={active ? "active tabList" : "tabList"} className={active ? "active tabList" : "tabList"}
title={useObservable(tab.tabPath)} title={useObservable(tab?.tabPath || ko.observable(""))}
aria-selected={active} aria-selected={active}
aria-expanded={active} aria-expanded={active}
aria-controls={tab.tabId} aria-controls={tabId}
tabIndex={0} tabIndex={0}
role="tab" role="tab"
ref={focusTab} ref={focusTab}
> >
<span className="tabNavContentContainer"> <span className="tabNavContentContainer">
<a data-toggle="tab" href={"#" + tab.tabId} tabIndex={-1}> <a data-toggle="tab" href={"#" + tabId} tabIndex={-1}>
<div className="tab_Content"> <div className="tab_Content">
<span className="statusIconContainer"> <span className="statusIconContainer">
{useObservable(tab.isExecutionError) && <ErrorIcon tab={tab} active={active} />} {useObservable(tab?.isExecutionError || ko.observable(false)) && <ErrorIcon tab={tab} active={active} />}
{useObservable(tab.isExecuting) && ( {useObservable(tab?.isExecuting || ko.observable(false)) && (
<img className="loadingIcon" title="Loading" src={loadingIcon} alt="Loading" /> <img className="loadingIcon" title="Loading" src={loadingIcon} alt="Loading" />
)} )}
</span> </span>
<span className="tabNavText">{useObservable(tab.tabTitle)}</span> <span className="tabNavText">{useObservable(tab?.tabTitle || ko.observable("Connect"))}</span>
<span className="tabIconSection"> <span className="tabIconSection">
<CloseButton tab={tab} active={active} hovering={hovering} /> <CloseButton tab={tab} active={active} hovering={hovering} />
</span> </span>
@ -83,7 +89,7 @@ const CloseButton = ({ tab, active, hovering }: { tab: Tab; active: boolean; hov
role="button" role="button"
aria-label="Close Tab" aria-label="Close Tab"
className="cancelButton" className="cancelButton"
onClick={() => tab.onCloseTabButtonClick()} onClick={() => (tab ? tab.onCloseTabButtonClick() : useTabs.getState().closeConnectTab())}
tabIndex={active ? 0 : undefined} tabIndex={active ? 0 : undefined}
onKeyPress={({ nativeEvent: e }) => tab.onKeyPressClose(undefined, e)} onKeyPress={({ nativeEvent: e }) => tab.onKeyPressClose(undefined, e)}
> >
@ -133,9 +139,18 @@ function TabPane({ tab, active }: { tab: Tab; active: boolean }) {
} }
}, [ref, tab]); }, [ref, tab]);
if ("render" in tab) { if (tab) {
return <div {...attrs}>{tab.render()}</div>; if ("render" in tab) {
return <div {...attrs}>{tab.render()}</div>;
}
} }
return <div {...attrs} ref={ref} data-bind="html:html" />; return <div {...attrs} ref={ref} data-bind="html:html" />;
} }
const onKeyPressConnectTab = (e: KeyboardEvent): void => {
if (e.key === "Enter" || e.key === "Space") {
useTabs.getState().activateConnectTab();
e.stopPropagation();
}
};

View File

@ -1,5 +1,6 @@
import { TeachingBubble } from "@fluentui/react"; import { TeachingBubble } from "@fluentui/react";
import { useDatabases } from "Explorer/useDatabases"; import { useDatabases } from "Explorer/useDatabases";
import { useTabs } from "hooks/useTabs";
import { useTeachingBubble } from "hooks/useTeachingBubble"; import { useTeachingBubble } from "hooks/useTeachingBubble";
import React from "react"; import React from "react";
@ -140,7 +141,7 @@ export const QuickstartTutorial: React.FC = (): JSX.Element => {
hasCloseButton hasCloseButton
primaryButtonProps={{ primaryButtonProps={{
text: "Launch connect", text: "Launch connect",
//onClick: () => setStep(7), onClick: () => useTabs.getState().openAndActivateConnectTab(),
}} }}
secondaryButtonProps={{ secondaryButtonProps={{
text: "Previous", text: "Previous",

View File

@ -60,6 +60,7 @@ initializeIcons();
const App: React.FunctionComponent = () => { const App: React.FunctionComponent = () => {
const [isLeftPaneExpanded, setIsLeftPaneExpanded] = useState<boolean>(true); const [isLeftPaneExpanded, setIsLeftPaneExpanded] = useState<boolean>(true);
const openedTabs = useTabs((state) => state.openedTabs); const openedTabs = useTabs((state) => state.openedTabs);
const isConnectTabOpen = useTabs((state) => state.isConnectTabOpen);
const config = useConfig(); const config = useConfig();
const explorer = useKnockoutExplorer(config?.platform); const explorer = useKnockoutExplorer(config?.platform);
@ -103,7 +104,7 @@ const App: React.FunctionComponent = () => {
</div> </div>
</div> </div>
{/* Collections Tree - End */} {/* Collections Tree - End */}
{openedTabs.length === 0 && <SplashScreen explorer={explorer} />} {openedTabs.length === 0 && !isConnectTabOpen && <SplashScreen explorer={explorer} />}
<Tabs /> <Tabs />
</div> </div>
{/* Collections Tree and Tabs - End */} {/* Collections Tree and Tabs - End */}

View File

@ -7,6 +7,8 @@ import TabsBase from "../Explorer/Tabs/TabsBase";
interface TabsState { interface TabsState {
openedTabs: TabsBase[]; openedTabs: TabsBase[];
activeTab: TabsBase; activeTab: TabsBase;
isConnectTabOpen: boolean;
isConnectTabActive: boolean;
activateTab: (tab: TabsBase) => void; activateTab: (tab: TabsBase) => void;
activateNewTab: (tab: TabsBase) => void; activateNewTab: (tab: TabsBase) => void;
updateTab: (tab: TabsBase) => void; updateTab: (tab: TabsBase) => void;
@ -15,19 +17,24 @@ interface TabsState {
closeTabsByComparator: (comparator: (tab: TabsBase) => boolean) => void; closeTabsByComparator: (comparator: (tab: TabsBase) => boolean) => void;
closeTab: (tab: TabsBase) => void; closeTab: (tab: TabsBase) => void;
closeAllNotebookTabs: (hardClose: boolean) => void; closeAllNotebookTabs: (hardClose: boolean) => void;
activateConnectTab: () => void;
openAndActivateConnectTab: () => void;
closeConnectTab: () => void;
} }
export const useTabs: UseStore<TabsState> = create((set, get) => ({ export const useTabs: UseStore<TabsState> = create((set, get) => ({
openedTabs: [], openedTabs: [],
activeTab: undefined, activeTab: undefined,
isConnectTabOpen: false,
isConnectTabActive: false,
activateTab: (tab: TabsBase): void => { activateTab: (tab: TabsBase): void => {
if (get().openedTabs.some((openedTab) => openedTab.tabId === tab.tabId)) { if (get().openedTabs.some((openedTab) => openedTab.tabId === tab.tabId)) {
set({ activeTab: tab }); set({ activeTab: tab, isConnectTabActive: false });
tab.onActivate(); tab.onActivate();
} }
}, },
activateNewTab: (tab: TabsBase): void => { activateNewTab: (tab: TabsBase): void => {
set((state) => ({ openedTabs: [...state.openedTabs, tab], activeTab: tab })); set((state) => ({ openedTabs: [...state.openedTabs, tab], activeTab: tab, isConnectTabActive: false }));
tab.onActivate(); tab.onActivate();
}, },
updateTab: (tab: TabsBase) => { updateTab: (tab: TabsBase) => {
@ -66,7 +73,7 @@ export const useTabs: UseStore<TabsState> = create((set, get) => ({
return true; return true;
}); });
if (updatedTabs.length === 0) { if (updatedTabs.length === 0) {
set({ activeTab: undefined }); set({ activeTab: undefined, isConnectTabActive: get().isConnectTabOpen });
} }
if (tab.tabId === activeTab.tabId && tabIndex !== -1) { if (tab.tabId === activeTab.tabId && tabIndex !== -1) {
@ -104,8 +111,21 @@ export const useTabs: UseStore<TabsState> = create((set, get) => ({
}); });
if (get().openedTabs.length === 0) { if (get().openedTabs.length === 0) {
set({ activeTab: undefined }); set({ activeTab: undefined, isConnectTabActive: get().isConnectTabOpen });
} }
} }
}, },
activateConnectTab: () => {
if (get().isConnectTabOpen) {
set({ isConnectTabActive: true, activeTab: undefined });
}
},
openAndActivateConnectTab: () => set({ isConnectTabActive: true, isConnectTabOpen: true, activeTab: undefined }),
closeConnectTab: () => {
const { isConnectTabActive, openedTabs } = get();
if (isConnectTabActive && openedTabs?.length > 0) {
set({ activeTab: openedTabs[0] });
}
set({ isConnectTabActive: false, isConnectTabOpen: false });
},
})); }));