Initial transfer from ADO (#13)

This commit is contained in:
Laurent Nguyen
2020-06-05 04:04:15 +02:00
committed by GitHub
parent ab3486bd05
commit e9d3160b57
34 changed files with 827 additions and 526 deletions

View File

@@ -0,0 +1,72 @@
import { FontWeights } from "@uifabric/styling";
import { IIconStyles, ITextStyles } from "office-ui-fabric-react";
export const siteTextStyles: ITextStyles = {
root: {
color: "#025F52",
fontWeight: FontWeights.semibold
}
};
export const descriptionTextStyles: ITextStyles = {
root: {
color: "#333333",
fontWeight: FontWeights.semibold
}
};
export const subtleHelpfulTextStyles: ITextStyles = {
root: {
color: "#ccc",
fontWeight: FontWeights.regular
}
};
export const iconButtonStyles: IIconStyles = {
root: {
marginLeft: "10px",
color: "#0078D4",
backgroundColor: "#FFF",
fontSize: 16,
fontWeight: FontWeights.regular,
display: "inline-block",
selectors: {
":hover .ms-Button-icon": {
color: "#ccc"
}
}
}
};
export const iconStyles: IIconStyles = {
root: {
marginLeft: "10px",
color: "#0078D4",
backgroundColor: "#FFF",
fontSize: 16,
fontWeight: FontWeights.regular,
display: "inline-block"
}
};
export const mainHelpfulTextStyles: ITextStyles = {
root: {
color: "#000",
fontWeight: FontWeights.regular
}
};
export const subtleIconStyles: IIconStyles = {
root: {
color: "#ddd",
fontSize: 12,
fontWeight: FontWeights.regular
}
};
export const helpfulTextStyles: ITextStyles = {
root: {
color: "#333333",
fontWeight: FontWeights.regular
}
};

View File

@@ -0,0 +1,18 @@
import React from "react";
import { shallow } from "enzyme";
import { GalleryCardComponent, GalleryCardComponentProps } from "./GalleryCardComponent";
describe("GalleryCardComponent", () => {
it("renders", () => {
const props: GalleryCardComponentProps = {
name: "mycard",
url: "url",
notebookMetadata: undefined,
// eslint-disable-next-line @typescript-eslint/no-empty-function
onClick: () => {}
};
const wrapper = shallow(<GalleryCardComponent {...props} />);
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,65 @@
import * as React from "react";
import * as DataModels from "../../../../Contracts/DataModels";
import { Card, ICardTokens, ICardSectionTokens } from "@uifabric/react-cards";
import { Icon, Image, Persona, Text } from "office-ui-fabric-react";
import {
siteTextStyles,
descriptionTextStyles,
helpfulTextStyles,
subtleHelpfulTextStyles,
subtleIconStyles
} from "./CardStyleConstants";
export interface GalleryCardComponentProps {
name: string;
url: string;
notebookMetadata: DataModels.NotebookMetadata;
onClick: () => void;
}
export class GalleryCardComponent extends React.Component<GalleryCardComponentProps> {
private cardTokens: ICardTokens = { childrenMargin: 12 };
private attendantsCardSectionTokens: ICardSectionTokens = { childrenGap: 6 };
public render(): JSX.Element {
return this.props.notebookMetadata != null ? (
<Card aria-label="Notebook Card" onClick={this.props.onClick} tokens={this.cardTokens}>
<Card.Item>
<Persona text={this.props.notebookMetadata.author} secondaryText={this.props.notebookMetadata.date} />
</Card.Item>
<Card.Item fill>
<Image src={this.props.notebookMetadata.imageUrl} width="100%" alt="Notebook display image" />
</Card.Item>
<Card.Section>
<Text variant="small" styles={siteTextStyles}>
{this.props.notebookMetadata.tags.join(", ")}
</Text>
<Text styles={descriptionTextStyles}>{this.props.name}</Text>
<Text variant="small" styles={helpfulTextStyles}>
{this.props.notebookMetadata.description}
</Text>
</Card.Section>
<Card.Section horizontal tokens={this.attendantsCardSectionTokens}>
<Icon iconName="RedEye" styles={subtleIconStyles} />
<Text variant="small" styles={subtleHelpfulTextStyles}>
{this.props.notebookMetadata.views}
</Text>
<Icon iconName="Download" styles={subtleIconStyles} />
<Text variant="small" styles={subtleHelpfulTextStyles}>
{this.props.notebookMetadata.downloads}
</Text>
<Icon iconName="Heart" styles={subtleIconStyles} />
<Text variant="small" styles={subtleHelpfulTextStyles}>
{this.props.notebookMetadata.likes}
</Text>
</Card.Section>
</Card>
) : (
<Card aria-label="Notebook Card" onClick={this.props.onClick} tokens={this.cardTokens}>
<Card.Section>
<Text styles={descriptionTextStyles}>{this.props.name}</Text>
</Card.Section>
</Card>
);
}
}

View File

@@ -0,0 +1,28 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`GalleryCardComponent renders 1`] = `
<Card
aria-label="Notebook Card"
onClick={[Function]}
tokens={
Object {
"childrenMargin": 12,
}
}
>
<CardSection>
<Text
styles={
Object {
"root": Object {
"color": "#333333",
"fontWeight": 600,
},
}
}
>
mycard
</Text>
</CardSection>
</Card>
`;

View File

@@ -0,0 +1,9 @@
@import "../../../../less/Common/Constants";
.galleryContainer {
padding: @LargeSpace @LargeSpace 30px @LargeSpace;
height: 100%;
overflow-y: auto;
width: 100%;
font-family: @DataExplorerFont;
}

View File

@@ -0,0 +1,77 @@
import React from "react";
import { shallow } from "enzyme";
import {
GalleryViewerContainerComponent,
GalleryViewerContainerComponentProps,
FullWidthTabs,
FullWidthTabsProps,
GalleryCardsComponent,
GalleryCardsComponentProps,
GalleryViewerComponent,
GalleryViewerComponentProps
} from "./GalleryViewerComponent";
import * as DataModels from "../../../Contracts/DataModels";
describe("GalleryCardsComponent", () => {
it("renders", () => {
// TODO Mock this
const props: GalleryCardsComponentProps = {
data: [],
userMetadata: undefined,
onNotebookMetadataChange: (officialSamplesIndex: number, notebookMetadata: DataModels.NotebookMetadata) =>
Promise.resolve(),
onClick: (
url: string,
notebookMetadata: DataModels.NotebookMetadata,
onNotebookMetadataChange: (newNotebookMetadata: DataModels.NotebookMetadata) => Promise<void>,
isLikedNotebook: boolean
) => Promise.resolve()
};
const wrapper = shallow(<GalleryCardsComponent {...props} />);
expect(wrapper).toMatchSnapshot();
});
});
describe("FullWidthTabs", () => {
it("renders", () => {
const props: FullWidthTabsProps = {
officialSamplesContent: [],
likedNotebooksContent: [],
userMetadata: undefined,
onClick: (
url: string,
notebookMetadata: DataModels.NotebookMetadata,
onNotebookMetadataChange: (newNotebookMetadata: DataModels.NotebookMetadata) => Promise<void>,
isLikedNotebook: boolean
) => Promise.resolve()
};
const wrapper = shallow(<FullWidthTabs {...props} />);
expect(wrapper).toMatchSnapshot();
});
});
describe("GalleryViewerContainerComponent", () => {
it("renders", () => {
const props: GalleryViewerContainerComponentProps = {
container: undefined
};
const wrapper = shallow(<GalleryViewerContainerComponent {...props} />);
expect(wrapper).toMatchSnapshot();
});
});
describe("GalleryCardComponent", () => {
it("renders", () => {
const props: GalleryViewerComponentProps = {
container: undefined,
officialSamplesData: [],
likedNotebookData: undefined
};
const wrapper = shallow(<GalleryViewerComponent {...props} />);
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,356 @@
/**
* Gallery Viewer
*/
import * as React from "react";
import * as DataModels from "../../../Contracts/DataModels";
import * as ViewModels from "../../../Contracts/ViewModels";
import { GalleryCardComponent } from "./Cards/GalleryCardComponent";
import { Stack, IStackTokens } from "office-ui-fabric-react";
import { JunoUtils } from "../../../Utils/JunoUtils";
import { CosmosClient } from "../../../Common/CosmosClient";
import { config } from "../../../Config";
import path from "path";
import { SessionStorageUtility, StorageKey } from "../../../Shared/StorageUtility";
import { NotificationConsoleUtils } from "../../../Utils/NotificationConsoleUtils";
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
import * as TabComponent from "../Tabs/TabComponent";
import "./GalleryViewerComponent.less";
export interface GalleryCardsComponentProps {
data: DataModels.GitHubInfoJunoResponse[];
userMetadata: DataModels.UserMetadata;
onNotebookMetadataChange: (
officialSamplesIndex: number,
notebookMetadata: DataModels.NotebookMetadata
) => Promise<void>;
onClick: (
url: string,
notebookMetadata: DataModels.NotebookMetadata,
onNotebookMetadataChange: (newNotebookMetadata: DataModels.NotebookMetadata) => Promise<void>,
isLikedNotebook: boolean
) => Promise<void>;
}
export class GalleryCardsComponent extends React.Component<GalleryCardsComponentProps> {
private sectionStackTokens: IStackTokens = { childrenGap: 30 };
public render(): JSX.Element {
return (
<Stack horizontal wrap tokens={this.sectionStackTokens}>
{this.props.data.map((githubInfo: DataModels.GitHubInfoJunoResponse, index: any) => {
const name = githubInfo.name;
const url = githubInfo.downloadUrl;
const notebookMetadata = githubInfo.metadata || {
date: "2008-12-01",
description: "Great notebook",
tags: ["favorite", "sample"],
author: "Laurent Nguyen",
views: 432,
likes: 123,
downloads: 56,
imageUrl:
"https://media.magazine.ferrari.com/images/2019/02/27/170304506-c1bcf028-b513-45f6-9f27-0cadac619c3d.jpg"
};
const officialSamplesIndex = githubInfo.officialSamplesIndex;
const isLikedNotebook = githubInfo.isLikedNotebook;
const updateTabsStatePerNotebook = this.props.onNotebookMetadataChange
? (notebookMetadata: DataModels.NotebookMetadata) =>
this.props.onNotebookMetadataChange(officialSamplesIndex, notebookMetadata)
: undefined;
return (
name !== ".gitignore" &&
url && (
<GalleryCardComponent
key={url}
name={name}
url={url}
notebookMetadata={notebookMetadata}
onClick={() => this.props.onClick(url, notebookMetadata, updateTabsStatePerNotebook, isLikedNotebook)}
/>
)
);
})}
</Stack>
);
}
}
export interface FullWidthTabsProps {
officialSamplesContent: DataModels.GitHubInfoJunoResponse[];
likedNotebooksContent: DataModels.GitHubInfoJunoResponse[];
userMetadata: DataModels.UserMetadata;
onClick: (
url: string,
notebookMetadata: DataModels.NotebookMetadata,
onNotebookMetadataChange: (newNotebookMetadata: DataModels.NotebookMetadata) => Promise<void>,
isLikedNotebook: boolean
) => Promise<void>;
}
interface FullWidthTabsState {
activeTabIndex: number;
officialSamplesContent: DataModels.GitHubInfoJunoResponse[];
likedNotebooksContent: DataModels.GitHubInfoJunoResponse[];
userMetadata: DataModels.UserMetadata;
}
export class FullWidthTabs extends React.Component<FullWidthTabsProps, FullWidthTabsState> {
private authorizationToken = CosmosClient.authorizationToken();
private appTabs: TabComponent.Tab[];
constructor(props: FullWidthTabsProps) {
super(props);
this.state = {
activeTabIndex: 0,
officialSamplesContent: this.props.officialSamplesContent,
likedNotebooksContent: this.props.likedNotebooksContent,
userMetadata: this.props.userMetadata
};
this.appTabs = [
{
title: "Official Samples",
content: {
className: "",
render: () => (
<GalleryCardsComponent
data={this.state.officialSamplesContent}
onClick={this.props.onClick}
userMetadata={this.state.userMetadata}
onNotebookMetadataChange={this.updateTabsState}
/>
)
},
isVisible: () => true
},
{
title: "Liked Notebooks",
content: {
className: "",
render: () => (
<GalleryCardsComponent
data={this.state.likedNotebooksContent}
onClick={this.props.onClick}
userMetadata={this.state.userMetadata}
onNotebookMetadataChange={this.updateTabsState}
/>
)
},
isVisible: () => true
}
];
}
public updateTabsState = async (officialSamplesIndex: number, notebookMetadata: DataModels.NotebookMetadata) => {
let currentLikedNotebooksContent = [...this.state.likedNotebooksContent];
let currentUserMetadata = { ...this.state.userMetadata };
let currentLikedNotebooks = [...currentUserMetadata.likedNotebooks];
const currentOfficialSamplesContent = [...this.state.officialSamplesContent];
const currentOfficialSamplesObject = { ...currentOfficialSamplesContent[officialSamplesIndex] };
const metadata = { ...currentOfficialSamplesObject.metadata };
const metadataLikesUpdates = metadata.likes - notebookMetadata.likes;
metadata.views = notebookMetadata.views;
metadata.downloads = notebookMetadata.downloads;
metadata.likes = notebookMetadata.likes;
currentOfficialSamplesObject.metadata = metadata;
// Notebook has been liked. Add To likedNotebooksContent, update isLikedNotebook flag
if (metadataLikesUpdates < 0) {
currentOfficialSamplesObject.isLikedNotebook = true;
currentLikedNotebooksContent = currentLikedNotebooksContent.concat(currentOfficialSamplesObject);
currentLikedNotebooks = currentLikedNotebooks.concat(currentOfficialSamplesObject.path);
currentUserMetadata = { likedNotebooks: currentLikedNotebooks };
} else if (metadataLikesUpdates > 0) {
// Notebook has been unliked. Remove from likedNotebooksContent after matching the path, update isLikedNotebook flag
currentOfficialSamplesObject.isLikedNotebook = false;
const likedNotebookIndex = currentLikedNotebooks.findIndex((path: string) => {
return path === currentOfficialSamplesObject.path;
});
currentLikedNotebooksContent.splice(likedNotebookIndex, 1);
currentLikedNotebooks.splice(likedNotebookIndex, 1);
currentUserMetadata = { likedNotebooks: currentLikedNotebooks };
}
currentOfficialSamplesContent[officialSamplesIndex] = currentOfficialSamplesObject;
this.setState({
activeTabIndex: 0,
userMetadata: currentUserMetadata,
likedNotebooksContent: currentLikedNotebooksContent,
officialSamplesContent: currentOfficialSamplesContent
});
JunoUtils.updateNotebookMetadata(this.authorizationToken, notebookMetadata).then(
async returnedNotebookMetadata => {
if (metadataLikesUpdates !== 0) {
JunoUtils.updateUserMetadata(this.authorizationToken, currentUserMetadata);
// TODO: update state here?
}
},
error => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Error updating notebook metadata: ${JSON.stringify(error)}`
);
// TODO add telemetry
}
);
};
private onTabIndexChange = (activeTabIndex: number) => this.setState({ activeTabIndex });
public render() {
return (
<TabComponent.TabComponent
tabs={this.appTabs}
onTabIndexChange={this.onTabIndexChange.bind(this)}
currentTabIndex={this.state.activeTabIndex}
hideHeader={false}
/>
);
}
}
export interface GalleryViewerContainerComponentProps {
container: ViewModels.Explorer;
}
interface GalleryViewerContainerComponentState {
officialSamplesData: DataModels.GitHubInfoJunoResponse[];
likedNotebooksData: DataModels.LikedNotebooksJunoResponse;
}
export class GalleryViewerContainerComponent extends React.Component<
GalleryViewerContainerComponentProps,
GalleryViewerContainerComponentState
> {
constructor(props: GalleryViewerContainerComponentProps) {
super(props);
this.state = {
officialSamplesData: undefined,
likedNotebooksData: undefined
};
}
componentDidMount() {
const authToken = CosmosClient.authorizationToken();
JunoUtils.getOfficialSampleNotebooks(authToken).then(
(data1: DataModels.GitHubInfoJunoResponse[]) => {
const officialSamplesData = data1;
JunoUtils.getLikedNotebooks(authToken).then(
(data2: DataModels.LikedNotebooksJunoResponse) => {
const likedNotebooksData = data2;
officialSamplesData.map((value: DataModels.GitHubInfoJunoResponse, index: number) => {
value.officialSamplesIndex = index;
value.isLikedNotebook = likedNotebooksData.userMetadata.likedNotebooks.includes(value.path);
});
likedNotebooksData.likedNotebooksContent.map((value: DataModels.GitHubInfoJunoResponse) => {
value.isLikedNotebook = true;
value.officialSamplesIndex = officialSamplesData.findIndex(
(officialSample: DataModels.GitHubInfoJunoResponse) => {
return officialSample.path === value.path;
}
);
});
this.setState({
officialSamplesData: officialSamplesData,
likedNotebooksData: likedNotebooksData
});
},
error => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Error fetching liked notebooks: ${JSON.stringify(error)}`
);
// TODO Add telemetry
}
);
},
error => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Error fetching sample notebooks: ${JSON.stringify(error)}`
);
// TODO Add telemetry
}
);
}
public render(): JSX.Element {
return this.state.officialSamplesData && this.state.likedNotebooksData ? (
<GalleryViewerComponent
container={this.props.container}
officialSamplesData={this.state.officialSamplesData}
likedNotebookData={this.state.likedNotebooksData}
/>
) : (
<></>
);
}
}
export interface GalleryViewerComponentProps {
container: ViewModels.Explorer;
officialSamplesData: DataModels.GitHubInfoJunoResponse[];
likedNotebookData: DataModels.LikedNotebooksJunoResponse;
}
export class GalleryViewerComponent extends React.Component<GalleryViewerComponentProps> {
public render(): JSX.Element {
return this.props.container ? (
<div className="galleryContainer">
<FullWidthTabs
officialSamplesContent={this.props.officialSamplesData}
likedNotebooksContent={this.props.likedNotebookData.likedNotebooksContent}
userMetadata={this.props.likedNotebookData.userMetadata}
onClick={this.openNotebookViewer}
/>
</div>
) : (
<div className="galleryContainer">
<GalleryCardsComponent
data={this.props.officialSamplesData}
onClick={this.openNotebookViewer}
userMetadata={undefined}
onNotebookMetadataChange={undefined}
/>
</div>
);
}
public getOfficialSamplesData(): DataModels.GitHubInfoJunoResponse[] {
return this.props.officialSamplesData;
}
public getLikedNotebookData(): DataModels.LikedNotebooksJunoResponse {
return this.props.likedNotebookData;
}
public openNotebookViewer = async (
url: string,
notebookMetadata: DataModels.NotebookMetadata,
onNotebookMetadataChange: (newNotebookMetadata: DataModels.NotebookMetadata) => Promise<void>,
isLikedNotebook: boolean
) => {
if (!this.props.container) {
SessionStorageUtility.setEntryString(
StorageKey.NotebookMetadata,
notebookMetadata ? JSON.stringify(notebookMetadata) : null
);
SessionStorageUtility.setEntryString(StorageKey.NotebookName, path.basename(url));
window.open(`${config.hostedExplorerURL}notebookViewer.html?notebookurl=${url}`, "_blank");
} else {
this.props.container.openNotebookViewer(url, notebookMetadata, onNotebookMetadataChange, isLikedNotebook);
}
};
}

View File

@@ -0,0 +1,54 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FullWidthTabs renders 1`] = `
<TabComponent
currentTabIndex={0}
hideHeader={false}
onTabIndexChange={[Function]}
tabs={
Array [
Object {
"content": Object {
"className": "",
"render": [Function],
},
"isVisible": [Function],
"title": "Official Samples",
},
Object {
"content": Object {
"className": "",
"render": [Function],
},
"isVisible": [Function],
"title": "Liked Notebooks",
},
]
}
/>
`;
exports[`GalleryCardComponent renders 1`] = `
<div
className="galleryContainer"
>
<GalleryCardsComponent
data={Array []}
onClick={[Function]}
/>
</div>
`;
exports[`GalleryCardsComponent renders 1`] = `
<Stack
horizontal={true}
tokens={
Object {
"childrenGap": 30,
}
}
wrap={true}
/>
`;
exports[`GalleryViewerContainerComponent renders 1`] = `<Fragment />`;

View File

@@ -0,0 +1,11 @@
.notebookViewerMetadataContainer {
margin: 0px 10px;
.title, .decoration, .persona {
display: inline-block;
}
.extras {
margin-top: 5px;
}
}

View File

@@ -0,0 +1,36 @@
import React from "react";
import { shallow } from "enzyme";
import { NotebookMetadataComponentProps, NotebookMetadataComponent } from "./NotebookMetadataComponent";
import { NotebookMetadata } from "../../../Contracts/DataModels";
describe("NotebookMetadataComponent", () => {
it("renders un-liked notebook", () => {
const props: NotebookMetadataComponentProps = {
notebookName: "My notebook",
container: undefined,
notebookMetadata: undefined,
notebookContent: {},
onNotebookMetadataChange: (newNotebookMetadata: NotebookMetadata) => Promise.resolve(),
isLikedNotebook: false
};
const wrapper = shallow(<NotebookMetadataComponent {...props} />);
expect(wrapper).toMatchSnapshot();
});
it("renders liked notebook", () => {
const props: NotebookMetadataComponentProps = {
notebookName: "My notebook",
container: undefined,
notebookMetadata: undefined,
notebookContent: {},
onNotebookMetadataChange: (newNotebookMetadata: NotebookMetadata) => Promise.resolve(),
isLikedNotebook: true
};
const wrapper = shallow(<NotebookMetadataComponent {...props} />);
expect(wrapper).toMatchSnapshot();
});
// TODO Add test for metadata display
});

View File

@@ -6,47 +6,97 @@ import * as React from "react";
import * as ViewModels from "../../../Contracts/ViewModels";
import { NotebookMetadata } from "../../../Contracts/DataModels";
import { initializeIcons } from "office-ui-fabric-react/lib/Icons";
import { Icon, Persona, Text } from "office-ui-fabric-react";
import CSS from "csstype";
import { Icon, Persona, Text, IconButton } from "office-ui-fabric-react";
import {
siteTextStyles,
subtleIconStyles,
iconStyles,
iconButtonStyles,
mainHelpfulTextStyles,
subtleHelpfulTextStyles,
helpfulTextStyles
} from "../../../GalleryViewer/Cards/CardStyleConstants";
} from "../NotebookGallery/Cards/CardStyleConstants";
import "./NotebookViewerComponent.less";
initializeIcons();
interface NotebookMetadataComponentProps {
export interface NotebookMetadataComponentProps {
notebookName: string;
container: ViewModels.Explorer;
notebookMetadata: NotebookMetadata;
notebookContent: any;
onNotebookMetadataChange: (newNotebookMetadata: NotebookMetadata) => Promise<void>;
isLikedNotebook: boolean;
}
export class NotebookMetadataComponent extends React.Component<NotebookMetadataComponentProps> {
private inlineBlockStyle: CSS.Properties = {
display: "inline-block"
interface NotebookMetadatComponentState {
liked: boolean;
notebookMetadata: NotebookMetadata;
}
export class NotebookMetadataComponent extends React.Component<
NotebookMetadataComponentProps,
NotebookMetadatComponentState
> {
constructor(props: NotebookMetadataComponentProps) {
super(props);
this.state = {
liked: this.props.isLikedNotebook,
notebookMetadata: this.props.notebookMetadata
};
}
private onDownloadClick = (newNotebookName: string) => {
this.props.container
.importAndOpenFromGallery(this.props.notebookName, newNotebookName, JSON.stringify(this.props.notebookContent))
.then(() => {
if (this.props.notebookMetadata) {
if (this.props.onNotebookMetadataChange) {
const notebookMetadata = { ...this.state.notebookMetadata };
notebookMetadata.downloads += 1;
this.props.onNotebookMetadataChange(notebookMetadata).then(() => {
this.setState({ notebookMetadata: notebookMetadata });
});
}
}
});
};
private marginTopStyle: CSS.Properties = {
marginTop: "5px"
componentDidMount() {
if (this.props.onNotebookMetadataChange) {
const notebookMetadata = { ...this.state.notebookMetadata };
if (this.props.notebookMetadata) {
notebookMetadata.views += 1;
this.props.onNotebookMetadataChange(notebookMetadata).then(() => {
this.setState({ notebookMetadata: notebookMetadata });
});
}
}
}
private onLike = (): void => {
if (this.props.onNotebookMetadataChange) {
const notebookMetadata = { ...this.state.notebookMetadata };
let liked: boolean;
if (this.state.liked) {
liked = false;
notebookMetadata.likes -= 1;
} else {
liked = true;
notebookMetadata.likes += 1;
}
this.props.onNotebookMetadataChange(notebookMetadata).then(() => {
this.setState({ liked: liked, notebookMetadata: notebookMetadata });
});
}
};
private onDownloadClick: (newNotebookName: string) => void = (newNotebookName: string) => {
this.props.container.importAndOpenFromGallery(
this.props.notebookName,
newNotebookName,
JSON.stringify(this.props.notebookContent)
);
};
public render(): JSX.Element {
private onDownload = (): void => {
const promptForNotebookName = () => {
return new Promise<string>((resolve, reject) => {
var newNotebookName = this.props.notebookName;
let newNotebookName = this.props.notebookName;
this.props.container.showOkCancelTextFieldModalDialog(
"Save notebook as",
undefined,
@@ -68,27 +118,35 @@ export class NotebookMetadataComponent extends React.Component<NotebookMetadataC
});
};
promptForNotebookName().then((newNotebookName: string) => {
this.onDownloadClick(newNotebookName);
});
};
public render(): JSX.Element {
return (
<div className="notebookViewerMetadataContainer">
<h3 style={this.inlineBlockStyle}>{this.props.notebookName}</h3>
<h3 className="title">{this.props.notebookName}</h3>
{this.props.notebookMetadata && (
<div style={this.inlineBlockStyle}>
<Icon iconName="Heart" styles={iconStyles} />
<Text variant="medium" styles={mainHelpfulTextStyles}>
{this.props.notebookMetadata.likes} likes
<div className="decoration">
{this.props.container ? (
<IconButton
iconProps={{ iconName: this.state.liked ? "HeartFill" : "Heart" }}
styles={iconButtonStyles}
onClick={this.onLike}
/>
) : (
<Icon iconName="Heart" styles={iconStyles} />
)}
<Text variant="large" styles={mainHelpfulTextStyles}>
{this.state.notebookMetadata.likes} likes
</Text>
</div>
)}
{this.props.container && (
<button
aria-label="downloadButton"
className="downloadButton"
onClick={async () => {
promptForNotebookName().then(this.onDownloadClick);
}}
>
<button aria-label="downloadButton" className="downloadButton" onClick={this.onDownload}>
Download Notebook
</button>
)}
@@ -97,20 +155,20 @@ export class NotebookMetadataComponent extends React.Component<NotebookMetadataC
<>
<div>
<Persona
style={this.inlineBlockStyle}
className="persona"
text={this.props.notebookMetadata.author}
secondaryText={this.props.notebookMetadata.date}
/>
</div>
<div>
<div style={this.marginTopStyle}>
<div className="extras">
<Icon iconName="RedEye" styles={subtleIconStyles} />
<Text variant="small" styles={subtleHelpfulTextStyles}>
{this.props.notebookMetadata.views}
{this.state.notebookMetadata.views}
</Text>
<Icon iconName="Download" styles={subtleIconStyles} />
<Text variant="small" styles={subtleHelpfulTextStyles}>
{this.props.notebookMetadata.downloads}
{this.state.notebookMetadata.downloads}
</Text>
</div>
<Text variant="small" styles={siteTextStyles}>

View File

@@ -1,11 +0,0 @@
@import "../../../../less/Common/Constants";
.notebookComponentContainer {
height: 100vh;
width: 100vw;
margin: 0px;
overflow-x: hidden;
font-family: @DataExplorerFont;
padding: 20px;
}

View File

@@ -1,41 +0,0 @@
import React from "react";
import * as ReactDOM from "react-dom";
import "bootstrap/dist/css/bootstrap.css";
import { NotebookMetadata } from "../../../Contracts/DataModels";
import { NotebookViewerComponent } from "./NotebookViewerComponent";
import { SessionStorageUtility, StorageKey } from "../../../Shared/StorageUtility";
const getNotebookUrl = (): string => {
const regex: RegExp = new RegExp("[?&]notebookurl=([^&#]*)|&|#|$");
const results: RegExpExecArray | null = regex.exec(window.location.href);
if (!results || !results[1]) {
return "";
}
return decodeURIComponent(results[1]);
};
const onInit = async () => {
var notebookMetadata: NotebookMetadata;
const notebookMetadataString = SessionStorageUtility.getEntryString(StorageKey.NotebookMetadata);
const notebookName = SessionStorageUtility.getEntryString(StorageKey.NotebookName);
if (notebookMetadataString == "null" || notebookMetadataString != null) {
notebookMetadata = (await JSON.parse(notebookMetadataString)) as NotebookMetadata;
SessionStorageUtility.removeEntry(StorageKey.NotebookMetadata);
SessionStorageUtility.removeEntry(StorageKey.NotebookName);
}
const notebookViewerComponent = (
<NotebookViewerComponent
notebookMetadata={notebookMetadata}
notebookName={notebookName}
notebookUrl={getNotebookUrl()}
container={null}
/>
);
ReactDOM.render(notebookViewerComponent, document.getElementById("notebookContent"));
};
// Entry point
window.addEventListener("load", onInit);

View File

@@ -4,7 +4,7 @@
padding: @DefaultSpace;
height: 100%;
width: 100%;
overflow-y: scroll;
overflow-y: auto;
}
.downloadButton {
@@ -20,7 +20,7 @@
display: "inline-block";
margin: 10px;
}
.active, .downloadButton:hover {
color: @BaseMedium;
}

View File

@@ -16,12 +16,14 @@ import "./NotebookViewerComponent.less";
export interface NotebookViewerComponentProps {
notebookName: string;
notebookUrl: string;
container: ViewModels.Explorer;
container?: ViewModels.Explorer;
notebookMetadata: NotebookMetadata;
onNotebookMetadataChange?: (newNotebookMetadata: NotebookMetadata) => Promise<void>;
isLikedNotebook?: boolean;
hideInputs?: boolean;
}
interface NotebookViewerComponentState {
element: JSX.Element;
content: any;
}
@@ -50,7 +52,7 @@ export class NotebookViewerComponent extends React.Component<
contentRef: createContentRef()
});
this.state = { element: undefined, content: undefined };
this.state = { content: undefined };
}
private async getJsonNotebookContent(): Promise<any> {
@@ -65,24 +67,25 @@ export class NotebookViewerComponent extends React.Component<
componentDidMount() {
this.getJsonNotebookContent().then((jsonContent: any) => {
this.notebookComponentBootstrapper.setContent("json", jsonContent);
const notebookReadonlyComponent = this.notebookComponentBootstrapper.renderComponent(NotebookReadOnlyRenderer);
this.setState({ element: notebookReadonlyComponent, content: jsonContent });
this.setState({ content: jsonContent });
});
}
public render(): JSX.Element {
return this.state != null ? (
return (
<div className="notebookViewerContainer">
<NotebookMetadataComponent
notebookMetadata={this.props.notebookMetadata}
notebookName={this.props.notebookName}
container={this.props.container}
notebookContent={this.state.content}
onNotebookMetadataChange={this.props.onNotebookMetadataChange}
isLikedNotebook={this.props.isLikedNotebook}
/>
{this.state.element}
{this.notebookComponentBootstrapper.renderComponent(NotebookReadOnlyRenderer, {
hideInputs: this.props.hideInputs
})}
</div>
) : (
<></>
);
}
}

View File

@@ -0,0 +1,25 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`NotebookMetadataComponent renders liked notebook 1`] = `
<div
className="notebookViewerMetadataContainer"
>
<h3
className="title"
>
My notebook
</h3>
</div>
`;
exports[`NotebookMetadataComponent renders un-liked notebook 1`] = `
<div
className="notebookViewerMetadataContainer"
>
<h3
className="title"
>
My notebook
</h3>
</div>
`;

View File

@@ -1,13 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="height=device-height, width=device-width, initial-scale=1.0" />
<title>Notebook Viewer</title>
</head>
<body>
<div class="notebookComponentContainer" id="notebookContent"></div>
</body>
</html>

View File

@@ -1,27 +1,28 @@
@import "../../../../less/Common/Constants";
.tabSwitch {
margin-left: @LargeSpace;
margin-bottom: 20px;
.tab {
margin-right: @MediumSpace;
}
.toggleSwitch {
.toggleSwitch();
}
.selectedToggle {
.selectedToggle();
}
.unselectedToggle {
.unselectedToggle();
}
}
.tabComponentContent {
height: calc(100% - 20px);
.tabComponentContainer {
height: 100%;
.flex-display();
.flex-direction();
.tabSwitch {
margin-left: @LargeSpace;
margin-bottom: 20px;
.tab {
margin-right: @MediumSpace;
}
.toggleSwitch {
.toggleSwitch();
}
.selectedToggle {
.selectedToggle();
}
.unselectedToggle {
.unselectedToggle();
}
}
}

View File

@@ -1,5 +1,6 @@
import * as React from "react";
import { AccessibleElement } from "../../Controls/AccessibleElement/AccessibleElement";
import "./TabComponent.less";
export interface TabContent {
render: () => JSX.Element;
@@ -75,10 +76,10 @@ export class TabComponent extends React.Component<TabComponentProps> {
}
return (
<React.Fragment>
<div className="tabComponentContainer">
{!this.props.hideHeader && <div className="tabs tabSwitch">{this.renderTabTitles()}</div>}
<div className={className}>{currentTabContent.render()}</div>
</React.Fragment>
</div>
);
}
}