Improve quickstart teaching bubble telemetries and make the home page a tab (#1299)

This commit is contained in:
victor-meng 2022-07-06 10:54:35 -07:00 committed by GitHub
parent f83634c50d
commit 0996489897
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 104 additions and 175 deletions

View File

@ -3,35 +3,25 @@
*/
import { Coachmark, DirectionalHint, Image, Link, Stack, TeachingBubbleContent, Text } from "@fluentui/react";
import { useCarousel } from "hooks/useCarousel";
import { useTabs } from "hooks/useTabs";
import { ReactTabKind, useTabs } from "hooks/useTabs";
import * as React from "react";
import { Action } from "Shared/Telemetry/TelemetryConstants";
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
import AddDatabaseIcon from "../../../images/AddDatabase.svg";
import NewQueryIcon from "../../../images/AddSqlQuery_16x16.svg";
import NewStoredProcedureIcon from "../../../images/AddStoredProcedure.svg";
import OpenQueryIcon from "../../../images/BrowseQuery.svg";
import ConnectIcon from "../../../images/Connect_color.svg";
import ContainersIcon from "../../../images/Containers.svg";
import LinkIcon from "../../../images/Link_blue.svg";
import NotebookIcon from "../../../images/notebook/Notebook-resource.svg";
import NotebookColorIcon from "../../../images/Notebooks.svg";
import QuickStartIcon from "../../../images/Quickstart_Lightning.svg";
import ScaleAndSettingsIcon from "../../../images/Scale_15x15.svg";
import CollectionIcon from "../../../images/tree-collection.svg";
import { AuthType } from "../../AuthType";
import * as Constants from "../../Common/Constants";
import * as ViewModels from "../../Contracts/ViewModels";
import { useSidePanel } from "../../hooks/useSidePanel";
import { userContext } from "../../UserContext";
import { getCollectionName, getDatabaseName } from "../../Utils/APITypeUtils";
import { getCollectionName } from "../../Utils/APITypeUtils";
import { FeaturePanelLauncher } from "../Controls/FeaturePanel/FeaturePanelLauncher";
import { DataSamplesUtil } from "../DataSamples/DataSamplesUtil";
import Explorer from "../Explorer";
import * as MostRecentActivity from "../MostRecentActivity/MostRecentActivity";
import { useNotebook } from "../Notebook/useNotebook";
import { AddDatabasePanel } from "../Panes/AddDatabasePanel/AddDatabasePanel";
import { BrowseQueriesPane } from "../Panes/BrowseQueriesPane/BrowseQueriesPane";
import { useDatabases } from "../useDatabases";
import { useSelectedNode } from "../useSelectedNode";
@ -234,103 +224,13 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
iconSrc: ConnectIcon,
title: "Connect",
description: "Prefer using your own choice of tooling? Find the connection string you need to connect",
onClick: () => useTabs.getState().openAndActivateConnectTab(),
onClick: () => useTabs.getState().openAndActivateReactTab(ReactTabKind.Connect),
};
heroes.push(connectBtn);
return heroes;
}
private createCommonTaskItems(): SplashScreenItem[] {
const items: SplashScreenItem[] = [];
if (userContext.authType === AuthType.ResourceToken) {
return items;
}
if (!useSelectedNode.getState().isDatabaseNodeOrNoneSelected()) {
if (userContext.apiType === "SQL" || userContext.apiType === "Gremlin") {
items.push({
iconSrc: NewQueryIcon,
onClick: () => {
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
selectedCollection && selectedCollection.onNewQueryClick(selectedCollection, undefined);
},
title: "New SQL Query",
description: undefined,
});
} else if (userContext.apiType === "Mongo") {
items.push({
iconSrc: NewQueryIcon,
onClick: () => {
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection, undefined);
},
title: "New Query",
description: undefined,
});
}
if (userContext.apiType === "SQL") {
items.push({
iconSrc: OpenQueryIcon,
title: "Open Query",
description: undefined,
onClick: () =>
useSidePanel
.getState()
.openSidePanel("Open Saved Queries", <BrowseQueriesPane explorer={this.container} />),
});
}
if (userContext.apiType !== "Cassandra") {
items.push({
iconSrc: NewStoredProcedureIcon,
title: "New Stored Procedure",
description: undefined,
onClick: () => {
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection, undefined);
},
});
}
/* Scale & Settings */
const isShared = useDatabases.getState().findSelectedDatabase()?.isDatabaseShared();
const label = isShared ? "Settings" : "Scale & Settings";
items.push({
iconSrc: ScaleAndSettingsIcon,
title: label,
description: undefined,
onClick: () => {
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
selectedCollection && selectedCollection.onSettingsClick();
},
});
} else {
items.push({
iconSrc: AddDatabaseIcon,
title: "New " + getDatabaseName(),
description: undefined,
onClick: async () => {
const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit;
if (throughputCap && throughputCap !== -1) {
await useDatabases.getState().loadAllOffers();
}
useSidePanel
.getState()
.openSidePanel(
"New " + getDatabaseName(),
<AddDatabasePanel explorer={this.container} buttonElement={document.activeElement as HTMLElement} />
);
},
});
}
return items;
}
private decorateOpenCollectionActivity({ databaseId, collectionId }: MostRecentActivity.OpenCollectionItem) {
return {
iconSrc: CollectionIcon,
@ -372,29 +272,6 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
});
}
private createTipsItems(): SplashScreenItem[] {
return [
{
iconSrc: undefined,
title: "Data Modeling",
description: "Learn more about modeling",
onClick: () => window.open(SplashScreen.dataModelingUrl),
},
{
iconSrc: undefined,
title: "Cost & Throughput Calculation",
description: "Learn more about cost calculation",
onClick: () => window.open(SplashScreen.throughputEstimatorUrl),
},
{
iconSrc: undefined,
title: "Configure automatic failover",
description: "Learn more about Cosmos DB high-availability",
onClick: () => window.open(SplashScreen.failoverUrl),
},
];
}
private onSplashScreenItemKeyPress(event: React.KeyboardEvent, callback: () => void) {
if (event.charCode === Constants.KeyCodes.Space || event.charCode === Constants.KeyCodes.Enter) {
callback();

View File

@ -1,4 +1,6 @@
import { CollectionTabKind } from "Contracts/ViewModels";
import Explorer from "Explorer/Explorer";
import { SplashScreen } from "Explorer/SplashScreen/SplashScreen";
import { ConnectTab } from "Explorer/Tabs/ConnectTab";
import { useTeachingBubble } from "hooks/useTeachingBubble";
import ko from "knockout";
@ -6,28 +8,33 @@ import React, { MutableRefObject, useEffect, useRef, useState } from "react";
import loadingIcon from "../../../images/circular_loader_black_16x16.gif";
import errorIcon from "../../../images/close-black.svg";
import { useObservable } from "../../hooks/useObservable";
import { useTabs } from "../../hooks/useTabs";
import { ReactTabKind, useTabs } from "../../hooks/useTabs";
import TabsBase from "./TabsBase";
type Tab = TabsBase | (TabsBase & { render: () => JSX.Element });
export const Tabs = (): JSX.Element => {
const { openedTabs, activeTab } = useTabs();
const isConnectTabOpen = useTabs((state) => state.isConnectTabOpen);
const isConnectTabActive = useTabs((state) => state.isConnectTabActive);
interface TabsProps {
explorer: Explorer;
}
export const Tabs = ({ explorer }: TabsProps): JSX.Element => {
const { openedTabs, openedReactTabs, activeTab, activeReactTab } = useTabs();
return (
<div className="tabsManagerContainer">
<div id="content" className="flexContainer hideOverflows">
<div className="nav-tabs-margin">
<ul className="nav nav-tabs level navTabHeight" id="navTabs" role="tablist">
{isConnectTabOpen && <TabNav key="connect" tab={undefined} active={isConnectTabActive} />}
{openedReactTabs.map((tab) => (
<TabNav key={ReactTabKind[tab]} active={activeReactTab === tab} tabKind={tab} />
))}
{openedTabs.map((tab) => (
<TabNav key={tab.tabId} tab={tab} active={activeTab === tab} />
))}
</ul>
</div>
<div className="tabPanesContainer">
{isConnectTabActive && <ConnectTab />}
{activeReactTab !== undefined && getReactTabContent(activeReactTab, explorer)}
{openedTabs.map((tab) => (
<TabPane key={tab.tabId} tab={tab} active={activeTab === tab} />
))}
@ -37,7 +44,7 @@ export const Tabs = (): JSX.Element => {
);
};
function TabNav({ tab, active }: { tab: Tab; active: boolean }) {
function TabNav({ tab, active, tabKind }: { tab?: Tab; active: boolean; tabKind?: ReactTabKind }) {
const [hovering, setHovering] = useState(false);
const focusTab = useRef<HTMLLIElement>() as MutableRefObject<HTMLLIElement>;
const tabId = tab ? tab.tabId : "connect";
@ -51,8 +58,20 @@ function TabNav({ tab, active }: { tab: Tab; active: boolean }) {
<li
onMouseOver={() => setHovering(true)}
onMouseLeave={() => setHovering(false)}
onClick={() => (tab ? tab.onTabClick() : useTabs.getState().activateConnectTab())}
onKeyPress={({ nativeEvent: e }) => (tab ? tab.onKeyPressActivate(undefined, e) : onKeyPressConnectTab(e))}
onClick={() => {
if (tab) {
tab.onTabClick();
} else if (tabKind !== undefined) {
useTabs.getState().activateReactTab(tabKind);
}
}}
onKeyPress={({ nativeEvent: e }) => {
if (tab) {
tab.onKeyPressActivate(undefined, e);
} else if (tabKind !== undefined) {
onKeyPressReactTab(e, tabKind);
}
}}
className={active ? "active tabList" : "tabList"}
title={useObservable(tab?.tabPath || ko.observable(""))}
aria-selected={active}
@ -65,16 +84,18 @@ function TabNav({ tab, active }: { tab: Tab; active: boolean }) {
<span className="tabNavContentContainer">
<a data-toggle="tab" href={"#" + tabId} tabIndex={-1}>
<div className="tab_Content">
<span className="statusIconContainer">
<span className="statusIconContainer" style={{ width: tabKind === ReactTabKind.Home ? 0 : 18 }}>
{useObservable(tab?.isExecutionError || ko.observable(false)) && <ErrorIcon tab={tab} active={active} />}
{useObservable(tab?.isExecuting || ko.observable(false)) && (
<img className="loadingIcon" title="Loading" src={loadingIcon} alt="Loading" />
)}
</span>
<span className="tabNavText">{useObservable(tab?.tabTitle || ko.observable("Connect"))}</span>
<span className="tabIconSection">
<CloseButton tab={tab} active={active} hovering={hovering} />
</span>
<span className="tabNavText">{useObservable(tab?.tabTitle || ko.observable(ReactTabKind[tabKind]))}</span>
{tabKind !== ReactTabKind.Home && (
<span className="tabIconSection">
<CloseButton tab={tab} active={active} hovering={hovering} tabKind={tabKind} />
</span>
)}
</div>
</a>
</span>
@ -82,14 +103,24 @@ function TabNav({ tab, active }: { tab: Tab; active: boolean }) {
);
}
const CloseButton = ({ tab, active, hovering }: { tab: Tab; active: boolean; hovering: boolean }) => (
const CloseButton = ({
tab,
active,
hovering,
tabKind,
}: {
tab: Tab;
active: boolean;
hovering: boolean;
tabKind?: ReactTabKind;
}) => (
<span
style={{ display: hovering || active ? undefined : "none" }}
title="Close"
role="button"
aria-label="Close Tab"
className="cancelButton"
onClick={() => (tab ? tab.onCloseTabButtonClick() : useTabs.getState().closeConnectTab())}
onClick={() => (tab ? tab.onCloseTabButtonClick() : useTabs.getState().closeReactTab(tabKind))}
tabIndex={active ? 0 : undefined}
onKeyPress={({ nativeEvent: e }) => tab.onKeyPressClose(undefined, e)}
>
@ -144,9 +175,20 @@ function TabPane({ tab, active }: { tab: Tab; active: boolean }) {
return <div {...attrs} ref={ref} data-bind="html:html" />;
}
const onKeyPressConnectTab = (e: KeyboardEvent): void => {
const onKeyPressReactTab = (e: KeyboardEvent, tabKind: ReactTabKind): void => {
if (e.key === "Enter" || e.key === "Space") {
useTabs.getState().activateConnectTab();
useTabs.getState().activateReactTab(tabKind);
e.stopPropagation();
}
};
const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): JSX.Element => {
switch (activeReactTab) {
case ReactTabKind.Connect:
return <ConnectTab />;
case ReactTabKind.Home:
return <SplashScreen explorer={explorer} />;
default:
throw Error(`Unsupported tab kind ${ReactTabKind[activeReactTab]}`);
}
};

View File

@ -1,9 +1,9 @@
import { Link, Stack, TeachingBubble, Text } from "@fluentui/react";
import { useTabs } from "hooks/useTabs";
import { ReactTabKind, useTabs } from "hooks/useTabs";
import { useTeachingBubble } from "hooks/useTeachingBubble";
import React from "react";
import { Action } from "Shared/Telemetry/TelemetryConstants";
import { traceCancel } from "Shared/Telemetry/TelemetryProcessor";
import { traceCancel, traceSuccess } from "Shared/Telemetry/TelemetryProcessor";
export const QuickstartTutorial: React.FC = (): JSX.Element => {
const { step, isSampleDBExpanded, isDocumentsTabOpened, sampleCollection, setStep } = useTeachingBubble();
@ -146,7 +146,10 @@ export const QuickstartTutorial: React.FC = (): JSX.Element => {
hasCloseButton
primaryButtonProps={{
text: "Launch connect",
onClick: () => useTabs.getState().openAndActivateConnectTab(),
onClick: () => {
traceSuccess(Action.CompleteUITour);
useTabs.getState().openAndActivateReactTab(ReactTabKind.Connect);
},
}}
secondaryButtonProps={{
text: "Previous",

View File

@ -46,12 +46,10 @@ import "./Explorer/Menus/NotificationConsole/NotificationConsole.less";
import { NotificationConsole } from "./Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
import "./Explorer/Panes/PanelComponent.less";
import { SidePanel } from "./Explorer/Panes/PanelContainerComponent";
import { SplashScreen } from "./Explorer/SplashScreen/SplashScreen";
import "./Explorer/SplashScreen/SplashScreen.less";
import { Tabs } from "./Explorer/Tabs/Tabs";
import { useConfig } from "./hooks/useConfig";
import { useKnockoutExplorer } from "./hooks/useKnockoutExplorer";
import { useTabs } from "./hooks/useTabs";
import "./Libs/jquery";
import "./Shared/appInsights";
@ -59,8 +57,6 @@ initializeIcons();
const App: React.FunctionComponent = () => {
const [isLeftPaneExpanded, setIsLeftPaneExpanded] = useState<boolean>(true);
const openedTabs = useTabs((state) => state.openedTabs);
const isConnectTabOpen = useTabs((state) => state.isConnectTabOpen);
const isCarouselOpen = useCarousel((state) => state.shouldOpen);
const config = useConfig();
@ -104,9 +100,7 @@ const App: React.FunctionComponent = () => {
{/* Collections Tree Collapsed - End */}
</div>
</div>
{/* Collections Tree - End */}
{openedTabs.length === 0 && !isConnectTabOpen && <SplashScreen explorer={explorer} />}
<Tabs />
<Tabs explorer={explorer} />
</div>
{/* Collections Tree and Tabs - End */}
<div

View File

@ -130,6 +130,7 @@ export enum Action {
CompleteCarousel,
LaunchUITour,
CancelUITour,
CompleteUITour,
}
export const ActionModifiers = {

View File

@ -6,37 +6,43 @@ import TabsBase from "../Explorer/Tabs/TabsBase";
interface TabsState {
openedTabs: TabsBase[];
openedReactTabs: ReactTabKind[];
activeTab: TabsBase;
isConnectTabOpen: boolean;
isConnectTabActive: boolean;
activeReactTab: ReactTabKind;
activateTab: (tab: TabsBase) => void;
activateNewTab: (tab: TabsBase) => void;
activateReactTab: (tabkind: ReactTabKind) => void;
updateTab: (tab: TabsBase) => void;
getTabs: (tabKind: ViewModels.CollectionTabKind, comparator?: (tab: TabsBase) => boolean) => TabsBase[];
refreshActiveTab: (comparator: (tab: TabsBase) => boolean) => void;
closeTabsByComparator: (comparator: (tab: TabsBase) => boolean) => void;
closeTab: (tab: TabsBase) => void;
closeAllNotebookTabs: (hardClose: boolean) => void;
activateConnectTab: () => void;
openAndActivateConnectTab: () => void;
closeConnectTab: () => void;
openAndActivateReactTab: (tabKind: ReactTabKind) => void;
closeReactTab: (tabKind: ReactTabKind) => void;
}
export enum ReactTabKind {
Connect,
Home,
}
export const useTabs: UseStore<TabsState> = create((set, get) => ({
openedTabs: [],
openedReactTabs: [ReactTabKind.Home],
activeTab: undefined,
isConnectTabOpen: false,
isConnectTabActive: false,
activeReactTab: ReactTabKind.Home,
activateTab: (tab: TabsBase): void => {
if (get().openedTabs.some((openedTab) => openedTab.tabId === tab.tabId)) {
set({ activeTab: tab, isConnectTabActive: false });
set({ activeTab: tab, activeReactTab: undefined });
tab.onActivate();
}
},
activateNewTab: (tab: TabsBase): void => {
set((state) => ({ openedTabs: [...state.openedTabs, tab], activeTab: tab, isConnectTabActive: false }));
set((state) => ({ openedTabs: [...state.openedTabs, tab], activeTab: tab, activeReactTab: undefined }));
tab.onActivate();
},
activateReactTab: (tabKind: ReactTabKind): void => set({ activeTab: undefined, activeReactTab: tabKind }),
updateTab: (tab: TabsBase) => {
if (get().activeTab?.tabId === tab.tabId) {
set({ activeTab: tab });
@ -73,7 +79,7 @@ export const useTabs: UseStore<TabsState> = create((set, get) => ({
return true;
});
if (updatedTabs.length === 0) {
set({ activeTab: undefined, isConnectTabActive: get().isConnectTabOpen });
set({ activeTab: undefined, activeReactTab: ReactTabKind.Home });
}
if (tab.tabId === activeTab.tabId && tabIndex !== -1) {
@ -111,21 +117,27 @@ export const useTabs: UseStore<TabsState> = create((set, get) => ({
});
if (get().openedTabs.length === 0) {
set({ activeTab: undefined, isConnectTabActive: get().isConnectTabOpen });
set({ activeTab: undefined, activeReactTab: ReactTabKind.Home });
}
}
},
activateConnectTab: () => {
if (get().isConnectTabOpen) {
set({ isConnectTabActive: true, activeTab: undefined });
openAndActivateReactTab: (tabKind: ReactTabKind) => {
if (get().openedReactTabs.indexOf(tabKind) === -1) {
set((state) => ({
openedReactTabs: [...state.openedReactTabs, tabKind],
}));
}
set({ activeTab: undefined, activeReactTab: tabKind });
},
openAndActivateConnectTab: () => set({ isConnectTabActive: true, isConnectTabOpen: true, activeTab: undefined }),
closeConnectTab: () => {
const { isConnectTabActive, openedTabs } = get();
if (isConnectTabActive && openedTabs?.length > 0) {
set({ activeTab: openedTabs[0] });
closeReactTab: (tabKind: ReactTabKind) => {
const { activeReactTab, openedTabs, openedReactTabs } = get();
if (activeReactTab === tabKind) {
openedTabs?.length > 0
? set({ activeTab: openedTabs[0], activeReactTab: undefined })
: set({ activeTab: undefined, activeReactTab: openedReactTabs[0] });
}
set({ isConnectTabActive: false, isConnectTabOpen: false });
set({ openedReactTabs: openedReactTabs.filter((tab: ReactTabKind) => tabKind !== tab) });
},
}));