From 0996489897203659f0f06dae394186ae00e59623 Mon Sep 17 00:00:00 2001 From: victor-meng <56978073+victor-meng@users.noreply.github.com> Date: Wed, 6 Jul 2022 10:54:35 -0700 Subject: [PATCH] Improve quickstart teaching bubble telemetries and make the home page a tab (#1299) --- src/Explorer/SplashScreen/SplashScreen.tsx | 129 +----------------- src/Explorer/Tabs/Tabs.tsx | 80 ++++++++--- src/Explorer/Tutorials/QuickstartTutorial.tsx | 9 +- src/Main.tsx | 8 +- src/Shared/Telemetry/TelemetryConstants.ts | 1 + src/hooks/useTabs.ts | 52 ++++--- 6 files changed, 104 insertions(+), 175 deletions(-) diff --git a/src/Explorer/SplashScreen/SplashScreen.tsx b/src/Explorer/SplashScreen/SplashScreen.tsx index 31f472754..26ce9d3dc 100644 --- a/src/Explorer/SplashScreen/SplashScreen.tsx +++ b/src/Explorer/SplashScreen/SplashScreen.tsx @@ -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 { 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", ), - }); - } - - 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(), - - ); - }, - }); - } - - return items; - } - private decorateOpenCollectionActivity({ databaseId, collectionId }: MostRecentActivity.OpenCollectionItem) { return { iconSrc: CollectionIcon, @@ -372,29 +272,6 @@ export class SplashScreen extends React.Component { }); } - 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(); diff --git a/src/Explorer/Tabs/Tabs.tsx b/src/Explorer/Tabs/Tabs.tsx index 9d980a44b..1439b2fd2 100644 --- a/src/Explorer/Tabs/Tabs.tsx +++ b/src/Explorer/Tabs/Tabs.tsx @@ -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 (
- {isConnectTabActive && } + {activeReactTab !== undefined && getReactTabContent(activeReactTab, explorer)} {openedTabs.map((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() as MutableRefObject; const tabId = tab ? tab.tabId : "connect"; @@ -51,8 +58,20 @@ function TabNav({ tab, active }: { tab: Tab; active: boolean }) {
  • 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 }) {
    - + {useObservable(tab?.isExecutionError || ko.observable(false)) && } {useObservable(tab?.isExecuting || ko.observable(false)) && ( Loading )} - {useObservable(tab?.tabTitle || ko.observable("Connect"))} - - - + {useObservable(tab?.tabTitle || ko.observable(ReactTabKind[tabKind]))} + {tabKind !== ReactTabKind.Home && ( + + + + )}
    @@ -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; +}) => ( (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
    ; } -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 ; + case ReactTabKind.Home: + return ; + default: + throw Error(`Unsupported tab kind ${ReactTabKind[activeReactTab]}`); + } +}; diff --git a/src/Explorer/Tutorials/QuickstartTutorial.tsx b/src/Explorer/Tutorials/QuickstartTutorial.tsx index 5fcb44026..516a3a518 100644 --- a/src/Explorer/Tutorials/QuickstartTutorial.tsx +++ b/src/Explorer/Tutorials/QuickstartTutorial.tsx @@ -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", diff --git a/src/Main.tsx b/src/Main.tsx index b68254204..20a7340ee 100644 --- a/src/Main.tsx +++ b/src/Main.tsx @@ -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(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 */}
  • - {/* Collections Tree - End */} - {openedTabs.length === 0 && !isConnectTabOpen && } - +
    {/* Collections Tree and Tabs - End */}
    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 = 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 = 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 = 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) }); }, }));