Add condition for showing quick start carousel (#1278)

* Add condition for showing quick start carousel

* Show coach mark when carousel is closed

* Add condition for showing quick start carousel and other UI changes

* Fix compile error

* Fix issue with coach mark

* Fix test

* Add new sample data, fix link url, fix e2e tests

* Fix e2e tests
This commit is contained in:
victor-meng 2022-05-23 20:52:21 -07:00 committed by GitHub
parent d13b7a50ad
commit 46ca952955
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 241 additions and 207 deletions

View File

@ -1,26 +1,25 @@
{ {
"databaseId": "SampleDB",
"offerThroughput": 400,
"databaseLevelThroughput": false,
"collectionId": "Persons",
"createNewDatabase": true,
"partitionKey": { "kind": "Hash", "paths": ["/firstname"], "version": 1 },
"data": [ "data": [
{ { "address": "2007, NE 37TH PL" },
"firstname": "Eva", { "address": "11635, SE MAY CREEK PARK DR" },
"age": 44 { "address": "8923, 133RD AVE SE" },
}, { "address": "1124, N 33RD ST" },
{ { "address": "4288, 131ST PL SE" },
"firstname": "Véronique", { "address": "10900, SE 66TH ST" },
"age": 50 { "address": "6260, 139TH AVE NE" },
}, { "address": "13427, NE SPRING BLVD" },
{ { "address": "13812, NE SPRING BLVD" },
"firstname": "亜妃子", { "address": "5029, 159TH PL SE" },
"age": 5 { "address": "8604, 117TH AVE SE" },
}, { "address": "1561, 139TH LN NE" },
{ { "address": "1575, 139TH CT NE" },
"firstname": "John", { "address": "13901, NE 15TH CT" },
"age": 23 { "address": "16365, NE 12TH PL" },
} { "address": "12226, NE 37TH ST" },
{ "address": "4021, 129TH CT SE" },
{ "address": "1455, 159TH PL NE" },
{ "address": "15825, NE 14TH RD" },
{ "address": "1418, 157TH CT NE" },
{ "address": "889, 131ST PL NE" }
] ]
} }

View File

@ -86,6 +86,7 @@ export interface Database extends TreeNode {
offer: ko.Observable<DataModels.Offer>; offer: ko.Observable<DataModels.Offer>;
isDatabaseExpanded: ko.Observable<boolean>; isDatabaseExpanded: ko.Observable<boolean>;
isDatabaseShared: ko.Computed<boolean>; isDatabaseShared: ko.Computed<boolean>;
isSampleDB?: boolean;
selectedSubnodeKind: ko.Observable<CollectionTabKind>; selectedSubnodeKind: ko.Observable<CollectionTabKind>;
@ -112,6 +113,7 @@ export interface CollectionBase extends TreeNode {
selectedSubnodeKind: ko.Observable<CollectionTabKind>; selectedSubnodeKind: ko.Observable<CollectionTabKind>;
children: ko.ObservableArray<TreeNode>; children: ko.ObservableArray<TreeNode>;
isCollectionExpanded: ko.Observable<boolean>; isCollectionExpanded: ko.Observable<boolean>;
isSampleCollection?: boolean;
onDocumentDBDocumentsClick(): void; onDocumentDBDocumentsClick(): void;
onNewQueryClick(source: any, event?: MouseEvent, queryText?: string): void; onNewQueryClick(source: any, event?: MouseEvent, queryText?: string): void;

View File

@ -113,7 +113,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
this.state = { this.state = {
createNewDatabase: userContext.apiType !== "Tables" && !this.props.databaseId, createNewDatabase: userContext.apiType !== "Tables" && !this.props.databaseId,
newDatabaseId: props.isQuickstart ? "SampleDB" : "", newDatabaseId: props.isQuickstart ? this.getSampleDBName() : "",
isSharedThroughputChecked: this.getSharedThroughputDefault(), isSharedThroughputChecked: this.getSharedThroughputDefault(),
selectedDatabaseId: selectedDatabaseId:
userContext.apiType === "Tables" ? CollectionCreation.TablesAPIDefaultDatabase : this.props.databaseId, userContext.apiType === "Tables" ? CollectionCreation.TablesAPIDefaultDatabase : this.props.databaseId,
@ -173,7 +173,19 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
onDismiss={() => this.setState({ teachingBubbleStep: 0 })} onDismiss={() => this.setState({ teachingBubbleStep: 0 })}
footerContent="Step 1 of 4" footerContent="Step 1 of 4"
> >
Database is the parent of a container, create a new database / use an existing one <Stack>
<Text style={{ color: "white" }}>
Database is the parent of a container. You can create a new database or use an existing one. In this
tutorial we are creating a new database named SampleDB.
</Text>
<Link
style={{ color: "white", fontWeight: 600 }}
target="_blank"
href="https://aka.ms/TeachingbubbleResources"
>
Learn more about resources.
</Link>
</Stack>
</TeachingBubble> </TeachingBubble>
)} )}
@ -187,8 +199,15 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
onDismiss={() => this.setState({ teachingBubbleStep: 0 })} onDismiss={() => this.setState({ teachingBubbleStep: 0 })}
footerContent="Step 2 of 4" footerContent="Step 2 of 4"
> >
<Stack>
<Text style={{ color: "white" }}>
Cosmos DB recommends sharing throughput across database. Autoscale will give you a flexible amount of Cosmos DB recommends sharing throughput across database. Autoscale will give you a flexible amount of
throughput based on the max RU/s set throughput based on the max RU/s set (Request Units).
</Text>
<Link style={{ color: "white", fontWeight: 600 }} target="_blank" href="https://aka.ms/teachingbubbleRU">
Learn more about RU/s.
</Link>
</Stack>
</TeachingBubble> </TeachingBubble>
)} )}
@ -773,18 +792,23 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
headline="Creating sample container" headline="Creating sample container"
target={"#loadingScreen"} target={"#loadingScreen"}
onDismiss={() => this.setState({ teachingBubbleStep: 0 })} onDismiss={() => this.setState({ teachingBubbleStep: 0 })}
footerContent={ styles={{ footer: { width: "100%" } }}
<ProgressIndicator
styles={{ itemName: { color: "rgb(255, 255, 255)" } }}
label="Adding sample data set"
/>
}
> >
A sample container is now being created and we are adding sample data for you. It should take about 1 A sample container is now being created and we are adding sample data for you. It should take about 1
minute. minute.
<br /> <br />
<br /> <br />
Once the sample container is created, review your sample dataset and follow next steps Once the sample container is created, review your sample dataset and follow next steps
<br />
<br />
<ProgressIndicator
styles={{
itemName: { color: "white" },
progressTrack: { backgroundColor: "#A6A6A6" },
progressBar: { background: "white" },
}}
label="Adding sample data set"
/>
</TeachingBubble> </TeachingBubble>
)} )}
</div> </div>
@ -1102,6 +1126,23 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
document.getElementById("collapsibleSectionContent")?.scrollIntoView(); document.getElementById("collapsibleSectionContent")?.scrollIntoView();
} }
private getSampleDBName(): string {
const existingSampleDBs = useDatabases
.getState()
.databases?.filter((database) => database.id().startsWith("SampleDB"));
const existingSampleDBNames = existingSampleDBs?.map((database) => database.id());
if (!existingSampleDBNames || existingSampleDBNames.length === 0) {
return "SampleDB";
}
let i = 1;
while (existingSampleDBNames.indexOf(`SampleDB${i}`) !== -1) {
i++;
}
return `SampleDB${i}`;
}
private async submit(event?: React.FormEvent<HTMLFormElement>): Promise<void> { private async submit(event?: React.FormEvent<HTMLFormElement>): Promise<void> {
event?.preventDefault(); event?.preventDefault();
@ -1198,11 +1239,14 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
await createCollection(createCollectionParams); await createCollection(createCollectionParams);
await this.props.explorer.refreshAllDatabases(); await this.props.explorer.refreshAllDatabases();
if (this.props.isQuickstart) { if (this.props.isQuickstart) {
const database = useDatabases.getState().findDatabaseWithId("SampleDB"); const database = useDatabases.getState().findDatabaseWithId(databaseId);
if (database) { if (database) {
database.isSampleDB = true;
// populate sample container with sample data // populate sample container with sample data
await database.loadCollections(); await database.loadCollections();
const collection = database.findCollectionWithId("SampleContainer"); const collection = database.findCollectionWithId(collectionId);
collection.isSampleCollection = true;
useTeachingBubble.getState().setSampleCollection(collection);
const sampleGenerator = await ContainerSampleGenerator.createSampleGeneratorAsync(this.props.explorer); const sampleGenerator = await ContainerSampleGenerator.createSampleGeneratorAsync(this.props.explorer);
await sampleGenerator.populateContainerAsync(collection); await sampleGenerator.populateContainerAsync(collection);
// auto-expand sample database + container and show teaching bubble // auto-expand sample database + container and show teaching bubble

View File

@ -9,31 +9,6 @@ const createExplorer = () => {
}; };
describe("SplashScreen", () => { describe("SplashScreen", () => {
it("allows sample collection creation for supported api's", () => {
const explorer = createExplorer();
const dataSampleUtil = new DataSamplesUtil(explorer);
const createStub = jest
.spyOn(dataSampleUtil, "createGeneratorAsync")
.mockImplementation(() => Promise.reject(undefined));
// Sample is supported
jest.spyOn(dataSampleUtil, "isSampleContainerCreationSupported").mockImplementation(() => true);
const splashScreen = new SplashScreen({ explorer });
jest.spyOn(splashScreen, "createDataSampleUtil").mockImplementation(() => dataSampleUtil);
const mainButtons = splashScreen.createMainItems();
// Press all buttons and make sure create gets called
mainButtons.forEach((button) => {
try {
button.onClick();
} catch (e) {
// noop
}
});
expect(createStub).toHaveBeenCalled();
});
it("does not allow sample collection creation for non-supported api's", () => { it("does not allow sample collection creation for non-supported api's", () => {
const explorerStub = createExplorer(); const explorerStub = createExplorer();
const dataSampleUtil = new DataSamplesUtil(explorerStub); const dataSampleUtil = new DataSamplesUtil(explorerStub);

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 { useCarousel } from "hooks/useCarousel";
import { useTabs } from "hooks/useTabs"; 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";
@ -10,9 +11,6 @@ import NewStoredProcedureIcon from "../../../images/AddStoredProcedure.svg";
import OpenQueryIcon from "../../../images/BrowseQuery.svg"; import OpenQueryIcon from "../../../images/BrowseQuery.svg";
import ConnectIcon from "../../../images/Connect_color.svg"; import ConnectIcon from "../../../images/Connect_color.svg";
import ContainersIcon from "../../../images/Containers.svg"; import ContainersIcon from "../../../images/Containers.svg";
import NewContainerIcon from "../../../images/Hero-new-container.svg";
import NewNotebookIcon from "../../../images/Hero-new-notebook.svg";
import SampleIcon from "../../../images/Hero-sample.svg";
import LinkIcon from "../../../images/Link_blue.svg"; import LinkIcon from "../../../images/Link_blue.svg";
import NotebookIcon from "../../../images/notebook/Notebook-resource.svg"; import NotebookIcon from "../../../images/notebook/Notebook-resource.svg";
import NotebookColorIcon from "../../../images/Notebooks.svg"; import NotebookColorIcon from "../../../images/Notebooks.svg";
@ -49,11 +47,7 @@ export interface SplashScreenProps {
explorer: Explorer; explorer: Explorer;
} }
export interface SplashScreenState { export class SplashScreen extends React.Component<SplashScreenProps> {
showCoachmark: boolean;
}
export class SplashScreen extends React.Component<SplashScreenProps, SplashScreenState> {
private static readonly seeMoreItemTitle: string = "See more Cosmos DB documentation"; private static readonly seeMoreItemTitle: string = "See more Cosmos DB documentation";
private static readonly seeMoreItemUrl: string = "https://aka.ms/cosmosdbdocument"; private static readonly seeMoreItemUrl: string = "https://aka.ms/cosmosdbdocument";
private static readonly dataModelingUrl = "https://docs.microsoft.com/azure/cosmos-db/modeling-data"; private static readonly dataModelingUrl = "https://docs.microsoft.com/azure/cosmos-db/modeling-data";
@ -67,10 +61,6 @@ export class SplashScreen extends React.Component<SplashScreenProps, SplashScree
super(props); super(props);
this.container = props.explorer; this.container = props.explorer;
this.subscriptions = []; this.subscriptions = [];
this.state = {
showCoachmark: userContext.features.enableNewQuickstart,
};
} }
public componentWillUnmount(): void { public componentWillUnmount(): void {
@ -87,7 +77,13 @@ export class SplashScreen extends React.Component<SplashScreenProps, SplashScree
(state) => state.isNotebookEnabled (state) => state.isNotebookEnabled
), ),
}, },
{ dispose: useSelectedNode.subscribe(() => this.setState({})) } { dispose: useSelectedNode.subscribe(() => this.setState({})) },
{
dispose: useCarousel.subscribe(
() => this.setState({}),
(state) => state.showCoachMark
),
}
); );
} }
@ -129,17 +125,14 @@ export class SplashScreen extends React.Component<SplashScreenProps, SplashScree
{item.showLinkIcon && <Image style={{ marginLeft: 8, width: 16 }} src={LinkIcon} />} {item.showLinkIcon && <Image style={{ marginLeft: 8, width: 16 }} src={LinkIcon} />}
</Stack> </Stack>
<div <div id={item.id} className="newDescription">
id={item.id}
className={userContext.features.enableNewQuickstart ? "newDescription" : "description"}
>
{item.description} {item.description}
</div> </div>
</div> </div>
</Stack> </Stack>
))} ))}
</div> </div>
{this.state.showCoachmark && ( {useCarousel.getState().showCoachMark && (
<Coachmark <Coachmark
target="#quickstartDescription" target="#quickstartDescription"
positioningContainerProps={{ directionalHint: DirectionalHint.rightTopEdge }} positioningContainerProps={{ directionalHint: DirectionalHint.rightTopEdge }}
@ -152,34 +145,33 @@ export class SplashScreen extends React.Component<SplashScreenProps, SplashScree
primaryButtonProps={{ primaryButtonProps={{
text: "Get started", text: "Get started",
onClick: () => { onClick: () => {
this.setState({ showCoachmark: false }); useCarousel.getState().setShowCoachMark(false);
this.container.onNewCollectionClicked({ isQuickstart: true }); this.container.onNewCollectionClicked({ isQuickstart: true });
}, },
}} }}
secondaryButtonProps={{ text: "Cancel", onClick: () => this.setState({ showCoachmark: false }) }} secondaryButtonProps={{
onDismiss={() => this.setState({ showCoachmark: false })} text: "Cancel",
onClick: () => useCarousel.getState().setShowCoachMark(false),
}}
onDismiss={() => useCarousel.getState().setShowCoachMark(false)}
> >
You will be guided to create a sample container with sample data, then we will give you a tour of You will be guided to create a sample container with sample data, then we will give you a tour of
data explorer You can also cancel launching this tour and explore yourself data explorer. You can also cancel launching this tour and explore yourself
</TeachingBubbleContent> </TeachingBubbleContent>
</Coachmark> </Coachmark>
)} )}
<div className="moreStuffContainer"> <div className="moreStuffContainer">
<div className="moreStuffColumn commonTasks"> <div className="moreStuffColumn commonTasks">
<div className="title">{userContext.features.enableNewQuickstart ? "Recents" : "Common Tasks"}</div> <div className="title">Recents</div>
{userContext.features.enableNewQuickstart ? this.getRecentItems() : this.getCommonTasksItems()} {this.getRecentItems()}
</div> </div>
<div className="moreStuffColumn"> <div className="moreStuffColumn">
<div className="title"> <div className="title">Top 3 things you need to know</div>
{userContext.features.enableNewQuickstart ? "Top 3 things you need to know" : "Recents"} {this.top3Items()}
</div>
{userContext.features.enableNewQuickstart ? this.top3Items() : this.getRecentItems()}
</div> </div>
<div className="moreStuffColumn tipsContainer"> <div className="moreStuffColumn tipsContainer">
<div className="title"> <div className="title">Learning Resources</div>
{userContext.features.enableNewQuickstart ? "Learning Resources" : "Tips"} {this.getLearningResourceItems()}
</div>
{userContext.features.enableNewQuickstart ? this.getLearningResourceItems() : this.getTipItems()}
</div> </div>
</div> </div>
</div> </div>
@ -202,7 +194,6 @@ export class SplashScreen extends React.Component<SplashScreenProps, SplashScree
public createMainItems(): SplashScreenItem[] { public createMainItems(): SplashScreenItem[] {
const heroes: SplashScreenItem[] = []; const heroes: SplashScreenItem[] = [];
if (userContext.features.enableNewQuickstart) {
if (userContext.apiType === "SQL" || userContext.apiType === "Mongo") { if (userContext.apiType === "SQL" || userContext.apiType === "Mongo") {
const launchQuickstartBtn = { const launchQuickstartBtn = {
id: "quickstartDescription", id: "quickstartDescription",
@ -241,33 +232,6 @@ export class SplashScreen extends React.Component<SplashScreenProps, SplashScree
onClick: () => useTabs.getState().openAndActivateConnectTab(), onClick: () => useTabs.getState().openAndActivateConnectTab(),
}; };
heroes.push(connectBtn); heroes.push(connectBtn);
} else {
const dataSampleUtil = this.createDataSampleUtil();
if (dataSampleUtil.isSampleContainerCreationSupported()) {
heroes.push({
iconSrc: SampleIcon,
title: "Start with Sample",
description: "Get started with a sample provided by Cosmos DB",
onClick: () => dataSampleUtil.createSampleContainerAsync(),
});
}
heroes.push({
iconSrc: NewContainerIcon,
title: `New ${getCollectionName()}`,
description: "Create a new container for storage and throughput",
onClick: () => this.container.onNewCollectionClicked(),
});
if (useNotebook.getState().isPhoenixNotebooks) {
heroes.push({
iconSrc: NewNotebookIcon,
title: "New Notebook",
description: "Create a notebook to start querying, visualizing, and modeling your data",
onClick: () => this.container.onNewNotebookClicked(),
});
}
}
return heroes; return heroes;
} }

View File

@ -1,14 +1,4 @@
import { import { IconButton, ITextFieldStyles, Pivot, PivotItem, PrimaryButton, Stack, Text, TextField } from "@fluentui/react";
IconButton,
ITextFieldStyles,
Link,
Pivot,
PivotItem,
PrimaryButton,
Stack,
Text,
TextField,
} from "@fluentui/react";
import { handleError } from "Common/ErrorHandlingUtils"; import { handleError } from "Common/ErrorHandlingUtils";
import { sendMessage } from "Common/MessageHandler"; import { sendMessage } from "Common/MessageHandler";
import { MessageTypes } from "Contracts/ExplorerContracts"; import { MessageTypes } from "Contracts/ExplorerContracts";
@ -76,16 +66,6 @@ export const ConnectTab: React.FC = (): JSX.Element => {
return ( return (
<div style={{ width: "100%", padding: 16 }}> <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> <Pivot>
{userContext.hasWriteAccess && ( {userContext.hasWriteAccess && (
<PivotItem headerText="Read-write Keys"> <PivotItem headerText="Read-write Keys">

View File

@ -121,11 +121,7 @@ function TabPane({ tab, active }: { tab: Tab; active: boolean }) {
}; };
useEffect((): (() => void) | void => { useEffect((): (() => void) | void => {
if ( if (tab.tabKind === CollectionTabKind.Documents && tab.collection?.isSampleCollection) {
tab.tabKind === CollectionTabKind.Documents &&
tab.collection?.databaseId === "SampleDB" &&
tab.collection?.id() === "SampleContainer"
) {
useTeachingBubble.getState().setIsDocumentsTabOpened(true); useTeachingBubble.getState().setIsDocumentsTabOpened(true);
} }

View File

@ -97,6 +97,7 @@ export default class Collection implements ViewModels.Collection {
public storedProceduresFocused: ko.Observable<boolean>; public storedProceduresFocused: ko.Observable<boolean>;
public userDefinedFunctionsFocused: ko.Observable<boolean>; public userDefinedFunctionsFocused: ko.Observable<boolean>;
public triggersFocused: ko.Observable<boolean>; public triggersFocused: ko.Observable<boolean>;
public isSampleCollection: boolean;
private isOfferRead: boolean; private isOfferRead: boolean;
constructor(container: Explorer, databaseId: string, data: DataModels.Collection) { constructor(container: Explorer, databaseId: string, data: DataModels.Collection) {
@ -216,6 +217,7 @@ export default class Collection implements ViewModels.Collection {
this.isStoredProceduresExpanded = ko.observable<boolean>(false); this.isStoredProceduresExpanded = ko.observable<boolean>(false);
this.isUserDefinedFunctionsExpanded = ko.observable<boolean>(false); this.isUserDefinedFunctionsExpanded = ko.observable<boolean>(false);
this.isTriggersExpanded = ko.observable<boolean>(false); this.isTriggersExpanded = ko.observable<boolean>(false);
this.isSampleCollection = false;
this.isOfferRead = false; this.isOfferRead = false;
} }

View File

@ -37,6 +37,7 @@ export default class Database implements ViewModels.Database {
public isDatabaseShared: ko.Computed<boolean>; public isDatabaseShared: ko.Computed<boolean>;
public selectedSubnodeKind: ko.Observable<ViewModels.CollectionTabKind>; public selectedSubnodeKind: ko.Observable<ViewModels.CollectionTabKind>;
public junoClient: JunoClient; public junoClient: JunoClient;
public isSampleDB: boolean;
private isOfferRead: boolean; private isOfferRead: boolean;
constructor(container: Explorer, data: DataModels.Database) { constructor(container: Explorer, data: DataModels.Database) {
@ -54,6 +55,7 @@ export default class Database implements ViewModels.Database {
return this.offer && !!this.offer(); return this.offer && !!this.offer();
}); });
this.junoClient = new JunoClient(); this.junoClient = new JunoClient();
this.isSampleDB = false;
this.isOfferRead = false; this.isOfferRead = false;
} }

View File

@ -1,5 +1,4 @@
import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react"; import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react";
import { useTeachingBubble } from "hooks/useTeachingBubble";
import * as React from "react"; import * as React from "react";
import shallow from "zustand/shallow"; import shallow from "zustand/shallow";
import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg"; import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg";
@ -462,7 +461,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
if (database.isDatabaseShared()) { if (database.isDatabaseShared()) {
databaseNode.children.push({ databaseNode.children.push({
id: database.id() === "SampleDB" ? "sampleScaleSettings" : "", id: database.isSampleDB ? "sampleScaleSettings" : "",
label: "Scale", label: "Scale",
isSelected: () => isSelected: () =>
useSelectedNode useSelectedNode
@ -499,7 +498,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
const children: TreeNode[] = []; const children: TreeNode[] = [];
children.push({ children.push({
label: collection.getLabel(), label: collection.getLabel(),
id: collection.databaseId === "SampleDB" && collection.id() === "SampleContainer" ? "sampleItems" : "", id: collection.isSampleCollection ? "sampleItems" : "",
onClick: () => { onClick: () => {
collection.openTab(); collection.openTab();
// push to most recent // push to most recent
@ -533,10 +532,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
if (userContext.apiType !== "Cassandra" || !isServerlessAccount()) { if (userContext.apiType !== "Cassandra" || !isServerlessAccount()) {
children.push({ children.push({
id: id: collection.isSampleCollection && !database.isDatabaseShared() ? "sampleScaleSettings" : "",
collection.databaseId === "SampleDB" && collection.id() === "SampleContainer" && !database.isDatabaseShared()
? "sampleScaleSettings"
: "",
label: database.isDatabaseShared() || isServerlessAccount() ? "Settings" : "Scale & Settings", label: database.isDatabaseShared() || isServerlessAccount() ? "Settings" : "Scale & Settings",
onClick: collection.onSettingsClick.bind(collection), onClick: collection.onSettingsClick.bind(collection),
isSelected: () => isSelected: () =>
@ -593,10 +589,6 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
); );
}, },
onExpanded: () => { onExpanded: () => {
// TODO: For testing purpose only, remove after
if (collection.databaseId === "SampleDB" && collection.id() === "SampleContainer") {
useTeachingBubble.getState().setIsSampleDBExpanded(true);
}
if (showScriptNodes) { if (showScriptNodes) {
collection.loadStoredProcedures(); collection.loadStoredProcedures();
collection.loadUserDefinedFunctions(); collection.loadUserDefinedFunctions();

View File

@ -1,6 +1,8 @@
import { DefaultButton, IconButton, Image, Modal, PrimaryButton, Stack, Text } from "@fluentui/react"; import { DefaultButton, IconButton, Image, Modal, PrimaryButton, Stack, Text } from "@fluentui/react";
import { useCarousel } from "hooks/useCarousel";
import React, { useState } from "react"; import React, { useState } from "react";
import Youtube from "react-youtube"; import Youtube from "react-youtube";
import { userContext } from "UserContext";
import Image1 from "../../../images/CarouselImage1.svg"; import Image1 from "../../../images/CarouselImage1.svg";
import Image2 from "../../../images/CarouselImage2.svg"; import Image2 from "../../../images/CarouselImage2.svg";
@ -13,7 +15,11 @@ export const QuickstartCarousel: React.FC<QuickstartCarouselProps> = ({
}: QuickstartCarouselProps): JSX.Element => { }: QuickstartCarouselProps): JSX.Element => {
const [page, setPage] = useState<number>(1); const [page, setPage] = useState<number>(1);
return ( return (
<Modal styles={{ main: { width: 640 } }} isOpen={isOpen && page < 4}> <Modal
styles={{ main: { width: 640 } }}
isOpen={isOpen && page < 4}
onDismissed={() => userContext.apiType === "SQL" && useCarousel.getState().setShowCoachMark(true)}
>
<Stack> <Stack>
<Stack horizontal horizontalAlign="space-between" style={{ padding: 16 }}> <Stack horizontal horizontalAlign="space-between" style={{ padding: 16 }}>
<Text variant="xLarge">{getHeaderText(page)}</Text> <Text variant="xLarge">{getHeaderText(page)}</Text>
@ -28,9 +34,23 @@ export const QuickstartCarousel: React.FC<QuickstartCarouselProps> = ({
<DefaultButton text="Previous" style={{ margin: "16px 8px 16px 0" }} onClick={() => setPage(page - 1)} /> <DefaultButton text="Previous" style={{ margin: "16px 8px 16px 0" }} onClick={() => setPage(page - 1)} />
)} )}
<PrimaryButton <PrimaryButton
id="carouselNextBtn"
style={{ margin: "16px 16px 16px 0" }} style={{ margin: "16px 16px 16px 0" }}
text={page === 3 ? "Finish" : "Next"} text={page === 3 ? "Finish" : "Next"}
onClick={() => setPage(page + 1)} onClick={() => {
if (
userContext.apiType === "Cassandra" ||
userContext.apiType === "Tables" ||
userContext.apiType === "Gremlin"
) {
setPage(page + 2);
} else {
if (page === 3 && userContext.apiType === "SQL") {
useCarousel.getState().setShowCoachMark(true);
}
setPage(page + 1);
}
}}
/> />
</Stack> </Stack>
</Stack> </Stack>

View File

@ -1,11 +1,10 @@
import { TeachingBubble } from "@fluentui/react"; import { Link, Stack, TeachingBubble, Text } from "@fluentui/react";
import { useDatabases } from "Explorer/useDatabases";
import { useTabs } from "hooks/useTabs"; import { useTabs } from "hooks/useTabs";
import { useTeachingBubble } from "hooks/useTeachingBubble"; import { useTeachingBubble } from "hooks/useTeachingBubble";
import React from "react"; import React from "react";
export const QuickstartTutorial: React.FC = (): JSX.Element => { export const QuickstartTutorial: React.FC = (): JSX.Element => {
const { step, isSampleDBExpanded, isDocumentsTabOpened, setStep } = useTeachingBubble(); const { step, isSampleDBExpanded, isDocumentsTabOpened, sampleCollection, setStep } = useTeachingBubble();
switch (step) { switch (step) {
case 1: case 1:
@ -17,8 +16,7 @@ export const QuickstartTutorial: React.FC = (): JSX.Element => {
primaryButtonProps={{ primaryButtonProps={{
text: "Open Items", text: "Open Items",
onClick: () => { onClick: () => {
const sampleContainer = useDatabases.getState().findCollection("SampleDB", "SampleContainer"); sampleCollection.openTab();
sampleContainer.openTab();
setStep(2); setStep(2);
}, },
}} }}
@ -70,7 +68,7 @@ export const QuickstartTutorial: React.FC = (): JSX.Element => {
onDismiss={() => setStep(0)} onDismiss={() => setStep(0)}
footerContent="Step 3 of 7" footerContent="Step 3 of 7"
> >
Add new item by copy / pasting jsons; or uploading a json Add new item by copy / pasting JSON; or uploading a JSON
</TeachingBubble> </TeachingBubble>
); );
case 4: case 4:
@ -120,7 +118,7 @@ export const QuickstartTutorial: React.FC = (): JSX.Element => {
target={"#newNotebookBtn"} target={"#newNotebookBtn"}
hasCloseButton hasCloseButton
primaryButtonProps={{ primaryButtonProps={{
text: "Finish", text: "Next",
onClick: () => setStep(7), onClick: () => setStep(7),
}} }}
secondaryButtonProps={{ secondaryButtonProps={{
@ -150,8 +148,15 @@ export const QuickstartTutorial: React.FC = (): JSX.Element => {
onDismiss={() => setStep(0)} onDismiss={() => setStep(0)}
footerContent="Step 7 of 7" footerContent="Step 7 of 7"
> >
<Stack>
<Text style={{ color: "white" }}>
You have finished the tour in data explorer. For next steps, you may want to launch connect and start You have finished the tour in data explorer. For next steps, you may want to launch connect and start
connecting with your current app connecting with your current app.
</Text>
<Link style={{ color: "white", fontWeight: 600 }} target="_blank" href="https://aka.ms/cosmosdbsurvey">
Share your feedback
</Link>
</Stack>
</TeachingBubble> </TeachingBubble>
); );
default: default:

View File

@ -3,9 +3,9 @@ import { initializeIcons } from "@fluentui/react";
import "bootstrap/dist/css/bootstrap.css"; import "bootstrap/dist/css/bootstrap.css";
import { QuickstartCarousel } from "Explorer/Tutorials/QuickstartCarousel"; import { QuickstartCarousel } from "Explorer/Tutorials/QuickstartCarousel";
import { QuickstartTutorial } from "Explorer/Tutorials/QuickstartTutorial"; import { QuickstartTutorial } from "Explorer/Tutorials/QuickstartTutorial";
import { useCarousel } from "hooks/useCarousel";
import React, { useState } from "react"; import React, { useState } from "react";
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import { userContext } from "UserContext";
import "../externals/jquery-ui.min.css"; import "../externals/jquery-ui.min.css";
import "../externals/jquery-ui.min.js"; import "../externals/jquery-ui.min.js";
import "../externals/jquery-ui.structure.min.css"; import "../externals/jquery-ui.structure.min.css";
@ -61,6 +61,7 @@ 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 isConnectTabOpen = useTabs((state) => state.isConnectTabOpen);
const isCarouselOpen = useCarousel((state) => state.shouldOpen);
const config = useConfig(); const config = useConfig();
const explorer = useKnockoutExplorer(config?.platform); const explorer = useKnockoutExplorer(config?.platform);
@ -119,8 +120,8 @@ const App: React.FunctionComponent = () => {
</div> </div>
<SidePanel /> <SidePanel />
<Dialog /> <Dialog />
{userContext.features.enableNewQuickstart && <QuickstartCarousel isOpen={true} />} {<QuickstartCarousel isOpen={isCarouselOpen} />}
{userContext.features.enableNewQuickstart && <QuickstartTutorial />} {<QuickstartTutorial />}
</div> </div>
); );
}; };

View File

@ -29,7 +29,6 @@ export type Features = {
readonly mongoProxyEndpoint?: string; readonly mongoProxyEndpoint?: string;
readonly mongoProxyAPIs?: string; readonly mongoProxyAPIs?: string;
readonly enableThroughputCap: boolean; readonly enableThroughputCap: boolean;
readonly enableNewQuickstart: boolean;
// can be set via both flight and feature flag // can be set via both flight and feature flag
autoscaleDefault: boolean; autoscaleDefault: boolean;
@ -91,7 +90,6 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
partitionKeyDefault2: "true" === get("pkpartitionkeytest"), partitionKeyDefault2: "true" === get("pkpartitionkeytest"),
notebooksDownBanner: "true" === get("notebooksDownBanner"), notebooksDownBanner: "true" === get("notebooksDownBanner"),
enableThroughputCap: "true" === get("enablethroughputcap"), enableThroughputCap: "true" === get("enablethroughputcap"),
enableNewQuickstart: "true" === get("enablenewquickstart"),
}; };
} }

View File

@ -14,4 +14,5 @@ export enum StorageKey {
MostRecentActivity, MostRecentActivity,
SetPartitionKeyUndefined, SetPartitionKeyUndefined,
GalleryCalloutDismissed, GalleryCalloutDismissed,
VisitedAccounts,
} }

View File

@ -1,3 +1,4 @@
import { useCarousel } from "hooks/useCarousel";
import { AuthType } from "./AuthType"; import { AuthType } from "./AuthType";
import { DatabaseAccount } from "./Contracts/DataModels"; import { DatabaseAccount } from "./Contracts/DataModels";
import { SubscriptionType } from "./Contracts/SubscriptionType"; import { SubscriptionType } from "./Contracts/SubscriptionType";
@ -73,6 +74,10 @@ const userContext: UserContext = {
function updateUserContext(newContext: Partial<UserContext>): void { function updateUserContext(newContext: Partial<UserContext>): void {
if (newContext.databaseAccount) { if (newContext.databaseAccount) {
newContext.apiType = apiType(newContext.databaseAccount); newContext.apiType = apiType(newContext.databaseAccount);
if (!localStorage.getItem(newContext.databaseAccount.id)) {
useCarousel.getState().setShouldOpen(true);
localStorage.setItem(newContext.databaseAccount.id, "true");
}
} }
Object.assign(userContext, newContext); Object.assign(userContext, newContext);
} }

15
src/hooks/useCarousel.ts Normal file
View File

@ -0,0 +1,15 @@
import create, { UseStore } from "zustand";
interface CarouselState {
shouldOpen: boolean;
showCoachMark: boolean;
setShouldOpen: (shouldOpen: boolean) => void;
setShowCoachMark: (showCoachMark: boolean) => void;
}
export const useCarousel: UseStore<CarouselState> = create((set) => ({
shouldOpen: false,
showCoachMark: false,
setShouldOpen: (shouldOpen: boolean) => set({ shouldOpen }),
setShowCoachMark: (showCoachMark: boolean) => set({ showCoachMark }),
}));

View File

@ -1,19 +1,24 @@
import { Collection } from "Contracts/ViewModels";
import create, { UseStore } from "zustand"; import create, { UseStore } from "zustand";
interface TeachingBubbleState { interface TeachingBubbleState {
step: number; step: number;
isSampleDBExpanded: boolean; isSampleDBExpanded: boolean;
isDocumentsTabOpened: boolean; isDocumentsTabOpened: boolean;
sampleCollection: Collection;
setStep: (step: number) => void; setStep: (step: number) => void;
setIsSampleDBExpanded: (isReady: boolean) => void; setIsSampleDBExpanded: (isReady: boolean) => void;
setIsDocumentsTabOpened: (isOpened: boolean) => void; setIsDocumentsTabOpened: (isOpened: boolean) => void;
setSampleCollection: (sampleCollection: Collection) => void;
} }
export const useTeachingBubble: UseStore<TeachingBubbleState> = create((set) => ({ export const useTeachingBubble: UseStore<TeachingBubbleState> = create((set) => ({
step: 1, step: 1,
isSampleDBExpanded: false, isSampleDBExpanded: false,
isDocumentsTabOpened: false, isDocumentsTabOpened: false,
sampleCollection: undefined,
setStep: (step: number) => set({ step }), setStep: (step: number) => set({ step }),
setIsSampleDBExpanded: (isSampleDBExpanded: boolean) => set({ isSampleDBExpanded }), setIsSampleDBExpanded: (isSampleDBExpanded: boolean) => set({ isSampleDBExpanded }),
setIsDocumentsTabOpened: (isDocumentsTabOpened: boolean) => set({ isDocumentsTabOpened }), setIsDocumentsTabOpened: (isDocumentsTabOpened: boolean) => set({ isDocumentsTabOpened }),
setSampleCollection: (sampleCollection: Collection) => set({ sampleCollection }),
})); }));

View File

@ -13,6 +13,10 @@ test("Cassandra keyspace and table CRUD", async () => {
await page.waitForSelector("iframe"); await page.waitForSelector("iframe");
const explorer = await waitForExplorer(); const explorer = await waitForExplorer();
// Click through quick start carousel
await explorer.click("#carouselNextBtn");
await explorer.click("#carouselNextBtn");
await explorer.click('[data-test="New Table"]'); await explorer.click('[data-test="New Table"]');
await explorer.click('[aria-label="Keyspace id"]'); await explorer.click('[aria-label="Keyspace id"]');
await explorer.fill('[aria-label="Keyspace id"]', keyspaceId); await explorer.fill('[aria-label="Keyspace id"]', keyspaceId);

View File

@ -12,6 +12,10 @@ test("Graph CRUD", async () => {
await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-gremlin-runner"); await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-gremlin-runner");
const explorer = await waitForExplorer(); const explorer = await waitForExplorer();
// Click through quick start carousel
await explorer.click("#carouselNextBtn");
await explorer.click("#carouselNextBtn");
// Create new database and graph // Create new database and graph
await explorer.click('[data-test="New Graph"]'); await explorer.click('[data-test="New Graph"]');
await explorer.fill('[aria-label="New database id"]', databaseId); await explorer.fill('[aria-label="New database id"]', databaseId);

View File

@ -12,6 +12,11 @@ test("Mongo CRUD", async () => {
await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-mongo-runner"); await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-mongo-runner");
const explorer = await waitForExplorer(); const explorer = await waitForExplorer();
// Click through quick start carousel
await explorer.click("#carouselNextBtn");
await explorer.click("#carouselNextBtn");
await explorer.click("#carouselNextBtn");
// Create new database and collection // Create new database and collection
await explorer.click('[data-test="New Collection"]'); await explorer.click('[data-test="New Collection"]');
await explorer.fill('[aria-label="New database id"]', databaseId); await explorer.fill('[aria-label="New database id"]', databaseId);

View File

@ -12,6 +12,11 @@ test("Mongo CRUD", async () => {
await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-mongo32-runner"); await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-mongo32-runner");
const explorer = await waitForExplorer(); const explorer = await waitForExplorer();
// Click through quick start carousel
await explorer.click("#carouselNextBtn");
await explorer.click("#carouselNextBtn");
await explorer.click("#carouselNextBtn");
// Create new database and collection // Create new database and collection
await explorer.click('[data-test="New Collection"]'); await explorer.click('[data-test="New Collection"]');
await explorer.fill('[aria-label="New database id"]', databaseId); await explorer.fill('[aria-label="New database id"]', databaseId);

View File

@ -11,6 +11,12 @@ test("SQL CRUD", async () => {
await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-sql-runner-west-us"); await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-sql-runner-west-us");
const explorer = await waitForExplorer(); const explorer = await waitForExplorer();
// Click through quick start carousel
await explorer.click("#carouselNextBtn");
await explorer.click("#carouselNextBtn");
await explorer.click("#carouselNextBtn");
await explorer.click('[data-test="New Container"]'); await explorer.click('[data-test="New Container"]');
await explorer.fill('[aria-label="New database id"]', databaseId); await explorer.fill('[aria-label="New database id"]', databaseId);
await explorer.fill('[aria-label="Container id"]', containerId); await explorer.fill('[aria-label="Container id"]', containerId);

View File

@ -12,6 +12,10 @@ test("Tables CRUD", async () => {
await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-tables-runner"); await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-tables-runner");
const explorer = await waitForExplorer(); const explorer = await waitForExplorer();
// Click through quick start carousel
await explorer.click("#carouselNextBtn");
await explorer.click("#carouselNextBtn");
await page.waitForSelector('text="Querying databases"', { state: "detached" }); await page.waitForSelector('text="Querying databases"', { state: "detached" });
await explorer.click('[data-test="New Table"]'); await explorer.click('[data-test="New Table"]');
await explorer.fill('[aria-label="Table id"]', tableId); await explorer.fill('[aria-label="Table id"]', tableId);