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 />`;