mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-19 17:01:13 +00:00
Initial Move from Azure DevOps to GitHub
This commit is contained in:
177
src/Explorer/SplashScreen/SplashScreenComponent.less
Normal file
177
src/Explorer/SplashScreen/SplashScreenComponent.less
Normal file
@@ -0,0 +1,177 @@
|
||||
@import "../../../less/Common/Constants";
|
||||
|
||||
.splashScreenContainer {
|
||||
width: 100%;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
|
||||
.splashScreen {
|
||||
.flex-display();
|
||||
.flex-direction();
|
||||
text-align: left;
|
||||
margin: auto;
|
||||
padding-left: 21px;
|
||||
padding-right: 16px;
|
||||
max-width: 1168px;;
|
||||
|
||||
>* {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
>.title {
|
||||
color: @BaseHigh;
|
||||
font-size: 48px;
|
||||
padding-left: 0px;
|
||||
margin: 16px auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
>.subtitle {
|
||||
color: @BaseHigh;
|
||||
font-size: 18px;
|
||||
padding-left: 0px;
|
||||
margin: 0px auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mainButtonsContainer {
|
||||
.flex-display();
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
margin: 40px auto;
|
||||
|
||||
>.mainButton {
|
||||
min-width: 124px;
|
||||
max-width: 296px;
|
||||
padding: 32px 16px;
|
||||
display: flex;
|
||||
background-color: @BaseLight;
|
||||
border: 1px solid #E5E5E5;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
|
||||
border-radius: 4px;
|
||||
|
||||
>.legendContainer {
|
||||
margin-left: 16px;
|
||||
text-align: left;
|
||||
|
||||
.legend {
|
||||
font-family: @SemiboldFont;
|
||||
margin-bottom: @DefaultSpace;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
>:nth-child(n+2) {
|
||||
margin-left: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.moreStuffContainer {
|
||||
.flex-display();
|
||||
justify-content: space-between;
|
||||
|
||||
.moreStuffColumn {
|
||||
flex-grow: 1;
|
||||
flex-basis: 0;
|
||||
min-width: 124px;
|
||||
max-width: 296px;
|
||||
|
||||
>.title {
|
||||
font-size: 18px;
|
||||
font-family: @SemiboldFont;
|
||||
color: @BaseDark;
|
||||
padding: 0px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
>ul {
|
||||
list-style: none;
|
||||
padding-left: 0px;
|
||||
margin-bottom: 0px;
|
||||
|
||||
li {
|
||||
padding: @DefaultSpace;
|
||||
.flex-display();
|
||||
align-items: flex-start;
|
||||
|
||||
>img {
|
||||
margin-right: @DefaultSpace;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.oneLineContent {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.twoLineContent {
|
||||
margin-top: -5px;
|
||||
|
||||
:nth-child(2) {
|
||||
font-size: 9px;
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 10px;
|
||||
color: @BaseMediumHigh;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tipContainer {
|
||||
padding: 8px 16px;
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
.flex-display();
|
||||
.flex-direction();
|
||||
|
||||
>.title {
|
||||
color: @BaseDark;
|
||||
padding: 0px;
|
||||
font-size: 12px;
|
||||
}
|
||||
>.description {
|
||||
color: @BaseDark;
|
||||
}
|
||||
|
||||
&:not(:hover):not(:focus) {
|
||||
background-color: @BaseLow;
|
||||
}
|
||||
}
|
||||
|
||||
&.commonTasks {
|
||||
li {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
&.tipsContainer {
|
||||
li {
|
||||
margin: 2px 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.focusable {
|
||||
&:hover {
|
||||
.hover();
|
||||
}
|
||||
|
||||
&:focus {
|
||||
.focus();
|
||||
}
|
||||
|
||||
&:active {
|
||||
.active();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
129
src/Explorer/SplashScreen/SplashScreenComponent.tsx
Normal file
129
src/Explorer/SplashScreen/SplashScreenComponent.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* Accordion top class
|
||||
*/
|
||||
|
||||
import * as React from "react";
|
||||
import * as Constants from "../../Common/Constants";
|
||||
import { Link } from "office-ui-fabric-react/lib/Link";
|
||||
|
||||
export interface SplashScreenItem {
|
||||
iconSrc: string;
|
||||
title: string;
|
||||
info?: string;
|
||||
description: string;
|
||||
onClick: () => void;
|
||||
}
|
||||
export interface SplashScreenComponentProps {
|
||||
mainItems: SplashScreenItem[];
|
||||
commonTaskItems: SplashScreenItem[];
|
||||
recentItems: SplashScreenItem[];
|
||||
tipsItems: SplashScreenItem[];
|
||||
onClearRecent: () => void;
|
||||
}
|
||||
|
||||
export class SplashScreenComponent extends React.Component<SplashScreenComponentProps> {
|
||||
private static readonly seeMoreItemTitle: string = "See more Cosmos DB documentation";
|
||||
private static readonly seeMoreItemUrl: string = "https://aka.ms/cosmosdbdocument";
|
||||
|
||||
public render(): JSX.Element {
|
||||
return (
|
||||
<div className="splashScreenContainer">
|
||||
<div className="splashScreen">
|
||||
<div className="title">Welcome to Cosmos DB</div>
|
||||
<div className="subtitle">Globally distributed, multi-model database service for any scale</div>
|
||||
<div className="mainButtonsContainer">
|
||||
{this.props.mainItems.map((item: SplashScreenItem) => (
|
||||
<div
|
||||
className="mainButton focusable"
|
||||
key={`${item.title}`}
|
||||
onClick={item.onClick}
|
||||
onKeyPress={(event: React.KeyboardEvent) => this.onSplashScreenItemKeyPress(event, item.onClick)}
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
>
|
||||
<img src={item.iconSrc} alt={item.title} />
|
||||
<div className="legendContainer">
|
||||
<div className="legend">{item.title}</div>
|
||||
<div className="description">{item.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="moreStuffContainer">
|
||||
<div className="moreStuffColumn commonTasks">
|
||||
<div className="title">Common Tasks</div>
|
||||
<ul>
|
||||
{this.props.commonTaskItems.map((item: SplashScreenItem) => (
|
||||
<li
|
||||
className="focusable"
|
||||
key={`${item.title}${item.description}`}
|
||||
onClick={item.onClick}
|
||||
onKeyPress={(event: React.KeyboardEvent) => this.onSplashScreenItemKeyPress(event, item.onClick)}
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
>
|
||||
<img src={item.iconSrc} alt={item.title} />
|
||||
<span className="oneLineContent" title={item.info}>
|
||||
{item.title}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="moreStuffColumn">
|
||||
<div className="title">Recents</div>
|
||||
<ul>
|
||||
{this.props.recentItems.map((item: SplashScreenItem, index: number) => (
|
||||
<li key={`${item.title}${item.description}${index}`}>
|
||||
<img src={item.iconSrc} alt={item.title} />
|
||||
<span className="twoLineContent">
|
||||
<Link onClick={item.onClick} title={item.info}>
|
||||
{item.title}
|
||||
</Link>
|
||||
<div className="description">{item.description}</div>
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{this.props.recentItems.length > 0 && (
|
||||
<Link onClick={() => this.props.onClearRecent()}>Clear Recents</Link>
|
||||
)}
|
||||
</div>
|
||||
<div className="moreStuffColumn tipsContainer">
|
||||
<div className="title">Tips</div>
|
||||
<ul>
|
||||
{this.props.tipsItems.map((item: SplashScreenItem) => (
|
||||
<li
|
||||
className="tipContainer focusable"
|
||||
key={`${item.title}${item.description}`}
|
||||
onClick={item.onClick}
|
||||
onKeyPress={(event: React.KeyboardEvent) => this.onSplashScreenItemKeyPress(event, item.onClick)}
|
||||
tabIndex={0}
|
||||
role="link"
|
||||
>
|
||||
<div className="title" title={item.info}>
|
||||
{item.title}
|
||||
</div>
|
||||
<div className="description">{item.description}</div>
|
||||
</li>
|
||||
))}
|
||||
<li>
|
||||
<a role="link" href={SplashScreenComponent.seeMoreItemUrl} target="_blank" tabIndex={0}>
|
||||
{SplashScreenComponent.seeMoreItemTitle}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private onSplashScreenItemKeyPress(event: React.KeyboardEvent, callback: () => void) {
|
||||
if (event.charCode === Constants.KeyCodes.Space || event.charCode === Constants.KeyCodes.Enter) {
|
||||
callback();
|
||||
event.stopPropagation();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import * as ko from "knockout";
|
||||
import { DataSamplesUtil } from "../DataSamples/DataSamplesUtil";
|
||||
import { SplashScreenComponentAdapter } from "./SplashScreenComponentApdapter";
|
||||
import Explorer from "../Explorer";
|
||||
jest.mock("../Explorer");
|
||||
|
||||
const createExplorer = () => {
|
||||
const mock = new Explorer({} as any);
|
||||
mock.openedTabs = ko.observableArray([]);
|
||||
mock.selectedNode = ko.observable();
|
||||
mock.isNotebookEnabled = ko.observable(false);
|
||||
mock.addCollectionText = ko.observable("add collection");
|
||||
return mock as jest.Mocked<Explorer>;
|
||||
};
|
||||
|
||||
describe("SplashScreenComponentAdapter", () => {
|
||||
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 splashScreenAdapter = new SplashScreenComponentAdapter(explorer);
|
||||
jest.spyOn(splashScreenAdapter, "createDataSampleUtil").mockImplementation(() => dataSampleUtil);
|
||||
const mainButtons = splashScreenAdapter.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", () => {
|
||||
const explorerStub = createExplorer();
|
||||
const dataSampleUtil = new DataSamplesUtil(explorerStub);
|
||||
const createStub = jest
|
||||
.spyOn(dataSampleUtil, "createGeneratorAsync")
|
||||
.mockImplementation(() => Promise.reject(undefined));
|
||||
|
||||
// Sample is not supported
|
||||
jest.spyOn(dataSampleUtil, "isSampleContainerCreationSupported").mockImplementation(() => false);
|
||||
|
||||
const splashScreenAdapter = new SplashScreenComponentAdapter(explorerStub);
|
||||
jest.spyOn(splashScreenAdapter, "createDataSampleUtil").mockImplementation(() => dataSampleUtil);
|
||||
const mainButtons = splashScreenAdapter.createMainItems();
|
||||
|
||||
// Press all buttons and make sure create doesn't get called
|
||||
mainButtons.forEach(button => {
|
||||
try {
|
||||
button.onClick();
|
||||
} catch (e) {
|
||||
// noop
|
||||
}
|
||||
});
|
||||
expect(createStub).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
228
src/Explorer/SplashScreen/SplashScreenComponentApdapter.tsx
Normal file
228
src/Explorer/SplashScreen/SplashScreenComponentApdapter.tsx
Normal file
@@ -0,0 +1,228 @@
|
||||
/**
|
||||
* Accordion top class
|
||||
*/
|
||||
import * as ko from "knockout";
|
||||
import * as React from "react";
|
||||
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { CosmosClient } from "../../Common/CosmosClient";
|
||||
|
||||
import NewContainerIcon from "../../../images/Hero-new-container.svg";
|
||||
import NewNotebookIcon from "../../../images/Hero-new-notebook.svg";
|
||||
import NewQueryIcon from "../../../images/AddSqlQuery_16x16.svg";
|
||||
import OpenQueryIcon from "../../../images/BrowseQuery.svg";
|
||||
import NewStoredProcedureIcon from "../../../images/AddStoredProcedure.svg";
|
||||
import ScaleAndSettingsIcon from "../../../images/Scale_15x15.svg";
|
||||
import { SplashScreenComponent, SplashScreenItem } from "./SplashScreenComponent";
|
||||
import * as MostRecentActivity from "../MostRecentActivity/MostRecentActivity";
|
||||
import AddDatabaseIcon from "../../../images/AddDatabase.svg";
|
||||
import SampleIcon from "../../../images/Hero-sample.svg";
|
||||
import { DataSamplesUtil } from "../DataSamples/DataSamplesUtil";
|
||||
|
||||
/**
|
||||
* TODO Remove this when fully ported to ReactJS
|
||||
*/
|
||||
export class SplashScreenComponentAdapter implements ReactAdapter {
|
||||
private static readonly dataModelingUrl = "https://docs.microsoft.com/azure/cosmos-db/modeling-data";
|
||||
private static readonly throughputEstimatorUrl = "https://cosmos.azure.com/capacitycalculator";
|
||||
private static readonly failoverUrl = "https://docs.microsoft.com/azure/cosmos-db/high-availability";
|
||||
|
||||
public parameters: ko.Observable<number>;
|
||||
|
||||
constructor(private container: ViewModels.Explorer) {
|
||||
this.parameters = ko.observable<number>(Date.now());
|
||||
this.container.openedTabs.subscribe(tabs => {
|
||||
if (tabs.length === 0) {
|
||||
this.forceRender();
|
||||
}
|
||||
});
|
||||
this.container.selectedNode.subscribe(this.forceRender);
|
||||
this.container.isNotebookEnabled.subscribe(this.forceRender);
|
||||
}
|
||||
|
||||
public forceRender = (): void => {
|
||||
window.requestAnimationFrame(() => this.parameters(Date.now()));
|
||||
};
|
||||
|
||||
private clearMostRecent = (): void => {
|
||||
this.container.mostRecentActivity.clear(CosmosClient.databaseAccount().id);
|
||||
this.forceRender();
|
||||
};
|
||||
|
||||
public renderComponent(): JSX.Element {
|
||||
return (
|
||||
<SplashScreenComponent
|
||||
mainItems={this.createMainItems()}
|
||||
commonTaskItems={this.createCommonTaskItems()}
|
||||
recentItems={this.createRecentItems()}
|
||||
tipsItems={this.createTipsItems()}
|
||||
onClearRecent={this.clearMostRecent}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* This exists to enable unit testing
|
||||
*/
|
||||
public createDataSampleUtil(): DataSamplesUtil {
|
||||
return new DataSamplesUtil(this.container);
|
||||
}
|
||||
|
||||
/**
|
||||
* public for testing purposes
|
||||
*/
|
||||
public createMainItems(): SplashScreenItem[] {
|
||||
const dataSampleUtil = this.createDataSampleUtil();
|
||||
const heroes: SplashScreenItem[] = [
|
||||
{
|
||||
iconSrc: NewContainerIcon,
|
||||
title: this.container.addCollectionText(),
|
||||
description: "Create a new container for storage and throughput",
|
||||
onClick: () => this.container.onNewCollectionClicked()
|
||||
}
|
||||
];
|
||||
|
||||
if (dataSampleUtil.isSampleContainerCreationSupported()) {
|
||||
// Insert at the front
|
||||
heroes.unshift({
|
||||
iconSrc: SampleIcon,
|
||||
title: "Start with Sample",
|
||||
description: "Get started with a sample provided by Cosmos DB",
|
||||
onClick: () => dataSampleUtil.createSampleContainerAsync()
|
||||
});
|
||||
}
|
||||
|
||||
if (this.container.isNotebookEnabled()) {
|
||||
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;
|
||||
}
|
||||
|
||||
private createCommonTaskItems(): SplashScreenItem[] {
|
||||
const items: SplashScreenItem[] = [];
|
||||
|
||||
if (this.container.isAuthWithResourceToken()) {
|
||||
return items;
|
||||
}
|
||||
|
||||
if (!this.container.isDatabaseNodeOrNoneSelected()) {
|
||||
if (this.container.isPreferredApiDocumentDB() || this.container.isPreferredApiGraph()) {
|
||||
items.push({
|
||||
iconSrc: NewQueryIcon,
|
||||
onClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = this.container.findSelectedCollection();
|
||||
selectedCollection && selectedCollection.onNewQueryClick(selectedCollection, null);
|
||||
},
|
||||
title: "New SQL Query",
|
||||
description: null
|
||||
});
|
||||
} else if (this.container.isPreferredApiMongoDB()) {
|
||||
items.push({
|
||||
iconSrc: NewQueryIcon,
|
||||
onClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = this.container.findSelectedCollection();
|
||||
selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection, null);
|
||||
},
|
||||
title: "New Query",
|
||||
description: null
|
||||
});
|
||||
}
|
||||
|
||||
items.push({
|
||||
iconSrc: OpenQueryIcon,
|
||||
title: "Open Query",
|
||||
description: null,
|
||||
onClick: () => this.container.browseQueriesPane.open()
|
||||
});
|
||||
|
||||
if (!this.container.isPreferredApiCassandra()) {
|
||||
items.push({
|
||||
iconSrc: NewStoredProcedureIcon,
|
||||
title: "New Stored Procedure",
|
||||
description: null,
|
||||
onClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = this.container.findSelectedCollection();
|
||||
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection, null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* Scale & Settings */
|
||||
let isShared = false;
|
||||
if (this.container.isDatabaseNodeSelected()) {
|
||||
isShared = this.container.findSelectedDatabase().isDatabaseShared();
|
||||
} else if (this.container.isNodeKindSelected("Collection")) {
|
||||
const database: ViewModels.Database = this.container.findSelectedCollection().getDatabase();
|
||||
isShared = database && database.isDatabaseShared();
|
||||
}
|
||||
|
||||
const label = isShared ? "Settings" : "Scale & Settings";
|
||||
items.push({
|
||||
iconSrc: ScaleAndSettingsIcon,
|
||||
title: label,
|
||||
description: null,
|
||||
onClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = this.container.findSelectedCollection();
|
||||
selectedCollection && selectedCollection.onSettingsClick();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
items.push({
|
||||
iconSrc: AddDatabaseIcon,
|
||||
title: this.container.addDatabaseText(),
|
||||
description: null,
|
||||
onClick: () => this.container.addDatabasePane.open()
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
private static getInfo(item: MostRecentActivity.Item): string {
|
||||
if (item.type === MostRecentActivity.Type.OpenNotebook) {
|
||||
const data = item.data as MostRecentActivity.OpenNotebookItem;
|
||||
return data.path;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private createRecentItems(): SplashScreenItem[] {
|
||||
return this.container.mostRecentActivity.getItems(CosmosClient.databaseAccount().id).map(item => ({
|
||||
iconSrc: MostRecentActivity.MostRecentActivity.getItemIcon(item),
|
||||
title: item.title,
|
||||
description: item.description,
|
||||
info: SplashScreenComponentAdapter.getInfo(item),
|
||||
onClick: () => this.container.mostRecentActivity.onItemClicked(item)
|
||||
}));
|
||||
}
|
||||
|
||||
private createTipsItems(): SplashScreenItem[] {
|
||||
return [
|
||||
{
|
||||
iconSrc: null,
|
||||
title: "Data Modeling",
|
||||
description: "Learn more about modeling",
|
||||
onClick: () => window.open(SplashScreenComponentAdapter.dataModelingUrl)
|
||||
},
|
||||
{
|
||||
iconSrc: null,
|
||||
title: "Cost & Throughput Calculation",
|
||||
description: "Learn more about cost calculation",
|
||||
onClick: () => window.open(SplashScreenComponentAdapter.throughputEstimatorUrl)
|
||||
},
|
||||
{
|
||||
iconSrc: null,
|
||||
title: "Configure automatic failover",
|
||||
description: "Learn more about Cosmos DB high-availability",
|
||||
onClick: () => window.open(SplashScreenComponentAdapter.failoverUrl)
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user