mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2024-11-29 00:47:01 +00:00
Updates to standalone gallery (#91)
* Add hyperlink icon for links in DialogComponent * Use a Cosmos DB image for sample Persona * Fix tests * Update standalone gallery behavior to match DE gallery * Use /gallery endpoint for gallery * Update Gallery Card style to remove buttons in standalone mode * Address comments * Add text banner
This commit is contained in:
parent
543ae9fe4a
commit
b1e20796c2
@ -3,6 +3,7 @@ import { Dialog, DialogType, DialogFooter, IDialogProps } from "office-ui-fabric
|
|||||||
import { IButtonProps, PrimaryButton, DefaultButton } from "office-ui-fabric-react/lib/Button";
|
import { IButtonProps, PrimaryButton, DefaultButton } from "office-ui-fabric-react/lib/Button";
|
||||||
import { ITextFieldProps, TextField } from "office-ui-fabric-react/lib/TextField";
|
import { ITextFieldProps, TextField } from "office-ui-fabric-react/lib/TextField";
|
||||||
import { Link } from "office-ui-fabric-react/lib/Link";
|
import { Link } from "office-ui-fabric-react/lib/Link";
|
||||||
|
import { FontIcon } from "office-ui-fabric-react";
|
||||||
|
|
||||||
export interface TextFieldProps extends ITextFieldProps {
|
export interface TextFieldProps extends ITextFieldProps {
|
||||||
label: string;
|
label: string;
|
||||||
@ -84,7 +85,7 @@ export class DialogComponent extends React.Component<DialogProps, {}> {
|
|||||||
{textFieldProps && <TextField {...textFieldProps} />}
|
{textFieldProps && <TextField {...textFieldProps} />}
|
||||||
{linkProps && (
|
{linkProps && (
|
||||||
<Link href={linkProps.linkUrl} target="_blank">
|
<Link href={linkProps.linkUrl} target="_blank">
|
||||||
{linkProps.linkText}
|
{linkProps.linkText} <FontIcon iconName="NavigateExternalInline" />
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
|
@ -5,11 +5,9 @@ export class GalleryHeaderComponent extends React.Component {
|
|||||||
private static readonly azureText = "Microsoft Azure";
|
private static readonly azureText = "Microsoft Azure";
|
||||||
private static readonly cosmosdbText = "Cosmos DB";
|
private static readonly cosmosdbText = "Cosmos DB";
|
||||||
private static readonly galleryText = "Gallery";
|
private static readonly galleryText = "Gallery";
|
||||||
private static readonly loginText = "Log in";
|
private static readonly loginText = "Sign In";
|
||||||
private static readonly openPortal = () => window.open("https://portal.azure.com", "_blank");
|
private static readonly openPortal = () => window.open("https://portal.azure.com", "_blank");
|
||||||
private static readonly openDataExplorer = () => (window.location.href = new URL("./", window.location.href).href);
|
private static readonly openDataExplorer = () => (window.location.href = new URL("./", window.location.href).href);
|
||||||
private static readonly openGallery = () =>
|
|
||||||
(window.location.href = new URL("./galleryViewer.html", window.location.href).href);
|
|
||||||
private static readonly headerItemStyle: React.CSSProperties = {
|
private static readonly headerItemStyle: React.CSSProperties = {
|
||||||
color: "white"
|
color: "white"
|
||||||
};
|
};
|
||||||
@ -63,7 +61,7 @@ export class GalleryHeaderComponent extends React.Component {
|
|||||||
<Stack.Item>
|
<Stack.Item>
|
||||||
{this.renderHeaderItem(
|
{this.renderHeaderItem(
|
||||||
GalleryHeaderComponent.galleryText,
|
GalleryHeaderComponent.galleryText,
|
||||||
GalleryHeaderComponent.openGallery,
|
undefined,
|
||||||
GalleryHeaderComponent.headerItemTextProps
|
GalleryHeaderComponent.headerItemTextProps
|
||||||
)}
|
)}
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
|
@ -20,6 +20,7 @@ describe("GalleryCardComponent", () => {
|
|||||||
views: 0
|
views: 0
|
||||||
},
|
},
|
||||||
isFavorite: false,
|
isFavorite: false,
|
||||||
|
showDownload: true,
|
||||||
showDelete: true,
|
showDelete: true,
|
||||||
onClick: undefined,
|
onClick: undefined,
|
||||||
onTagClick: undefined,
|
onTagClick: undefined,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Card, ICardTokens } from "@uifabric/react-cards";
|
import { Card } from "@uifabric/react-cards";
|
||||||
import {
|
import {
|
||||||
FontWeights,
|
FontWeights,
|
||||||
Icon,
|
Icon,
|
||||||
@ -18,10 +18,12 @@ import * as React from "react";
|
|||||||
import { IGalleryItem } from "../../../../Juno/JunoClient";
|
import { IGalleryItem } from "../../../../Juno/JunoClient";
|
||||||
import { FileSystemUtil } from "../../../Notebook/FileSystemUtil";
|
import { FileSystemUtil } from "../../../Notebook/FileSystemUtil";
|
||||||
import CosmosDBLogo from "../../../../../images/CosmosDB-logo.svg";
|
import CosmosDBLogo from "../../../../../images/CosmosDB-logo.svg";
|
||||||
|
import { StyleConstants } from "../../../../Common/Constants";
|
||||||
|
|
||||||
export interface GalleryCardComponentProps {
|
export interface GalleryCardComponentProps {
|
||||||
data: IGalleryItem;
|
data: IGalleryItem;
|
||||||
isFavorite: boolean;
|
isFavorite: boolean;
|
||||||
|
showDownload: boolean;
|
||||||
showDelete: boolean;
|
showDelete: boolean;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
onTagClick: (tag: string) => void;
|
onTagClick: (tag: string) => void;
|
||||||
@ -32,30 +34,30 @@ export interface GalleryCardComponentProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class GalleryCardComponent extends React.Component<GalleryCardComponentProps> {
|
export class GalleryCardComponent extends React.Component<GalleryCardComponentProps> {
|
||||||
public static readonly CARD_HEIGHT = 384;
|
|
||||||
public static readonly CARD_WIDTH = 256;
|
public static readonly CARD_WIDTH = 256;
|
||||||
|
|
||||||
private static readonly cardImageHeight = 144;
|
private static readonly cardImageHeight = 144;
|
||||||
private static readonly cardDescriptionMaxChars = 88;
|
private static readonly cardDescriptionMaxChars = 88;
|
||||||
private static readonly cardTokens: ICardTokens = {
|
private static readonly cardItemGapBig = 10;
|
||||||
width: GalleryCardComponent.CARD_WIDTH,
|
private static readonly cardItemGapSmall = 8;
|
||||||
height: GalleryCardComponent.CARD_HEIGHT,
|
|
||||||
childrenGap: 8,
|
|
||||||
childrenMargin: 10
|
|
||||||
};
|
|
||||||
|
|
||||||
public render(): JSX.Element {
|
public render(): JSX.Element {
|
||||||
|
const cardButtonsVisible = this.props.isFavorite !== undefined || this.props.showDownload || this.props.showDelete;
|
||||||
const options: Intl.DateTimeFormatOptions = {
|
const options: Intl.DateTimeFormatOptions = {
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
month: "short",
|
month: "short",
|
||||||
day: "numeric"
|
day: "numeric"
|
||||||
};
|
};
|
||||||
|
|
||||||
const dateString = new Date(this.props.data.created).toLocaleString("default", options);
|
const dateString = new Date(this.props.data.created).toLocaleString("default", options);
|
||||||
|
const cardTitle = FileSystemUtil.stripExtension(this.props.data.name, "ipynb");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card aria-label="Notebook Card" tokens={GalleryCardComponent.cardTokens} onClick={this.props.onClick}>
|
<Card
|
||||||
<Card.Item>
|
aria-label={cardTitle}
|
||||||
|
data-is-focusable="true"
|
||||||
|
tokens={{ width: GalleryCardComponent.CARD_WIDTH, childrenGap: 0 }}
|
||||||
|
onClick={event => this.onClick(event, this.props.onClick)}
|
||||||
|
>
|
||||||
|
<Card.Item tokens={{ padding: GalleryCardComponent.cardItemGapBig }}>
|
||||||
<Persona
|
<Persona
|
||||||
imageUrl={this.props.data.isSample && CosmosDBLogo}
|
imageUrl={this.props.data.isSample && CosmosDBLogo}
|
||||||
text={this.props.data.author}
|
text={this.props.data.author}
|
||||||
@ -63,69 +65,89 @@ export class GalleryCardComponent extends React.Component<GalleryCardComponentPr
|
|||||||
/>
|
/>
|
||||||
</Card.Item>
|
</Card.Item>
|
||||||
|
|
||||||
<Card.Item fill>
|
<Card.Item>
|
||||||
<Image
|
<Image
|
||||||
src={
|
src={this.props.data.thumbnailUrl}
|
||||||
this.props.data.thumbnailUrl ||
|
|
||||||
`https://placehold.it/${GalleryCardComponent.CARD_WIDTH}x${GalleryCardComponent.cardImageHeight}`
|
|
||||||
}
|
|
||||||
width={GalleryCardComponent.CARD_WIDTH}
|
width={GalleryCardComponent.CARD_WIDTH}
|
||||||
height={GalleryCardComponent.cardImageHeight}
|
height={GalleryCardComponent.cardImageHeight}
|
||||||
imageFit={ImageFit.cover}
|
imageFit={ImageFit.cover}
|
||||||
alt="Notebook cover image"
|
alt={`${cardTitle} cover image`}
|
||||||
/>
|
/>
|
||||||
</Card.Item>
|
</Card.Item>
|
||||||
|
|
||||||
<Card.Section>
|
<Card.Section styles={{ root: { padding: GalleryCardComponent.cardItemGapBig } }}>
|
||||||
<Text variant="small" nowrap>
|
<Text variant="small" nowrap>
|
||||||
{this.props.data.tags?.map((tag, index, array) => (
|
{this.props.data.tags?.map((tag, index, array) => (
|
||||||
<span key={tag}>
|
<span key={tag}>
|
||||||
<Link onClick={(event): void => this.onTagClick(event, tag)}>{tag}</Link>
|
<Link onClick={event => this.onClick(event, () => this.props.onTagClick(tag))}>{tag}</Link>
|
||||||
{index === array.length - 1 ? <></> : ", "}
|
{index === array.length - 1 ? <></> : ", "}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
</Text>
|
</Text>
|
||||||
<Text styles={{ root: { fontWeight: FontWeights.semibold } }} nowrap>
|
|
||||||
{FileSystemUtil.stripExtension(this.props.data.name, "ipynb")}
|
<Text
|
||||||
|
styles={{
|
||||||
|
root: {
|
||||||
|
fontWeight: FontWeights.semibold,
|
||||||
|
paddingTop: GalleryCardComponent.cardItemGapSmall,
|
||||||
|
paddingBottom: GalleryCardComponent.cardItemGapSmall
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
nowrap
|
||||||
|
>
|
||||||
|
{cardTitle}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Text variant="small" styles={{ root: { height: 36 } }}>
|
<Text variant="small" styles={{ root: { height: 36 } }}>
|
||||||
{this.props.data.description.substr(0, GalleryCardComponent.cardDescriptionMaxChars)}
|
{this.props.data.description.substr(0, GalleryCardComponent.cardDescriptionMaxChars)}
|
||||||
</Text>
|
</Text>
|
||||||
</Card.Section>
|
|
||||||
|
|
||||||
<Card.Section horizontal styles={{ root: { alignItems: "flex-end" } }}>
|
<span>
|
||||||
{this.generateIconText("RedEye", this.props.data.views.toString())}
|
{this.generateIconText("RedEye", this.props.data.views.toString())}
|
||||||
{this.generateIconText("Download", this.props.data.downloads.toString())}
|
{this.generateIconText("Download", this.props.data.downloads.toString())}
|
||||||
{this.props.isFavorite !== undefined && this.generateIconText("Heart", this.props.data.favorites.toString())}
|
{this.props.isFavorite !== undefined &&
|
||||||
|
this.generateIconText("Heart", this.props.data.favorites.toString())}
|
||||||
|
</span>
|
||||||
</Card.Section>
|
</Card.Section>
|
||||||
|
|
||||||
<Card.Item>
|
{cardButtonsVisible && (
|
||||||
|
<Card.Section
|
||||||
|
styles={{
|
||||||
|
root: {
|
||||||
|
marginLeft: GalleryCardComponent.cardItemGapBig,
|
||||||
|
marginRight: GalleryCardComponent.cardItemGapBig
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Separator styles={{ root: { padding: 0, height: 1 } }} />
|
<Separator styles={{ root: { padding: 0, height: 1 } }} />
|
||||||
</Card.Item>
|
|
||||||
|
|
||||||
<Card.Section horizontal styles={{ root: { marginTop: 0 } }}>
|
<span>
|
||||||
{this.props.isFavorite !== undefined &&
|
{this.props.isFavorite !== undefined &&
|
||||||
this.generateIconButtonWithTooltip(
|
this.generateIconButtonWithTooltip(
|
||||||
this.props.isFavorite ? "HeartFill" : "Heart",
|
this.props.isFavorite ? "HeartFill" : "Heart",
|
||||||
this.props.isFavorite ? "Unlike" : "Like",
|
this.props.isFavorite ? "Unlike" : "Like",
|
||||||
this.props.isFavorite ? this.onUnfavoriteClick : this.onFavoriteClick
|
"left",
|
||||||
|
this.props.isFavorite ? this.props.onUnfavoriteClick : this.props.onFavoriteClick
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{this.generateIconButtonWithTooltip("Download", "Download", this.onDownloadClick)}
|
{this.props.showDownload &&
|
||||||
|
this.generateIconButtonWithTooltip("Download", "Download", "left", this.props.onDownloadClick)}
|
||||||
|
|
||||||
{this.props.showDelete && (
|
{this.props.showDelete &&
|
||||||
<div style={{ width: "100%", textAlign: "right" }}>
|
this.generateIconButtonWithTooltip("Delete", "Remove", "right", this.props.onDeleteClick)}
|
||||||
{this.generateIconButtonWithTooltip("Delete", "Remove", this.onDeleteClick)}
|
</span>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Card.Section>
|
</Card.Section>
|
||||||
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private generateIconText = (iconName: string, text: string): JSX.Element => {
|
private generateIconText = (iconName: string, text: string): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<Text variant="tiny" styles={{ root: { color: "#ccc" } }}>
|
<Text
|
||||||
|
variant="tiny"
|
||||||
|
styles={{ root: { color: StyleConstants.BaseMedium, paddingRight: GalleryCardComponent.cardItemGapSmall } }}
|
||||||
|
>
|
||||||
<Icon iconName={iconName} styles={{ root: { verticalAlign: "middle" } }} /> {text}
|
<Icon iconName={iconName} styles={{ root: { verticalAlign: "middle" } }} /> {text}
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
@ -138,70 +160,37 @@ export class GalleryCardComponent extends React.Component<GalleryCardComponentPr
|
|||||||
private generateIconButtonWithTooltip = (
|
private generateIconButtonWithTooltip = (
|
||||||
iconName: string,
|
iconName: string,
|
||||||
title: string,
|
title: string,
|
||||||
onClick: (
|
horizontalAlign: "right" | "left",
|
||||||
event: React.MouseEvent<
|
activate: () => void
|
||||||
HTMLAnchorElement | HTMLButtonElement | HTMLDivElement | BaseButton | Button | HTMLSpanElement,
|
|
||||||
MouseEvent
|
|
||||||
>
|
|
||||||
) => void
|
|
||||||
): JSX.Element => {
|
): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<TooltipHost
|
<TooltipHost
|
||||||
content={title}
|
content={title}
|
||||||
id={`TooltipHost-IconButton-${iconName}`}
|
id={`TooltipHost-IconButton-${iconName}`}
|
||||||
calloutProps={{ gapSpace: 0 }}
|
calloutProps={{ gapSpace: 0 }}
|
||||||
styles={{ root: { display: "inline-block" } }}
|
styles={{ root: { display: "inline-block", float: horizontalAlign } }}
|
||||||
>
|
>
|
||||||
<IconButton iconProps={{ iconName }} title={title} ariaLabel={title} onClick={onClick} />
|
<IconButton
|
||||||
|
iconProps={{ iconName }}
|
||||||
|
title={title}
|
||||||
|
ariaLabel={title}
|
||||||
|
onClick={event => this.onClick(event, activate)}
|
||||||
|
/>
|
||||||
</TooltipHost>
|
</TooltipHost>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
private onTagClick = (
|
private onClick = (
|
||||||
event: React.MouseEvent<HTMLElement | HTMLAnchorElement | HTMLButtonElement | LinkBase, MouseEvent>,
|
event:
|
||||||
tag: string
|
| React.MouseEvent<HTMLElement | HTMLAnchorElement | HTMLButtonElement | LinkBase, MouseEvent>
|
||||||
): void => {
|
| React.MouseEvent<
|
||||||
event.stopPropagation();
|
|
||||||
this.props.onTagClick(tag);
|
|
||||||
};
|
|
||||||
|
|
||||||
private onFavoriteClick = (
|
|
||||||
event: React.MouseEvent<
|
|
||||||
HTMLAnchorElement | HTMLButtonElement | HTMLDivElement | BaseButton | Button | HTMLSpanElement,
|
HTMLAnchorElement | HTMLButtonElement | HTMLDivElement | BaseButton | Button | HTMLSpanElement,
|
||||||
MouseEvent
|
MouseEvent
|
||||||
>
|
>,
|
||||||
|
activate: () => void
|
||||||
): void => {
|
): void => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
this.props.onFavoriteClick();
|
event.preventDefault();
|
||||||
};
|
activate();
|
||||||
|
|
||||||
private onUnfavoriteClick = (
|
|
||||||
event: React.MouseEvent<
|
|
||||||
HTMLAnchorElement | HTMLButtonElement | HTMLDivElement | BaseButton | Button | HTMLSpanElement,
|
|
||||||
MouseEvent
|
|
||||||
>
|
|
||||||
): void => {
|
|
||||||
event.stopPropagation();
|
|
||||||
this.props.onUnfavoriteClick();
|
|
||||||
};
|
|
||||||
|
|
||||||
private onDownloadClick = (
|
|
||||||
event: React.MouseEvent<
|
|
||||||
HTMLAnchorElement | HTMLButtonElement | HTMLDivElement | BaseButton | Button | HTMLSpanElement,
|
|
||||||
MouseEvent
|
|
||||||
>
|
|
||||||
): void => {
|
|
||||||
event.stopPropagation();
|
|
||||||
this.props.onDownloadClick();
|
|
||||||
};
|
|
||||||
|
|
||||||
private onDeleteClick = (
|
|
||||||
event: React.MouseEvent<
|
|
||||||
HTMLAnchorElement | HTMLButtonElement | HTMLDivElement | BaseButton | Button | HTMLSpanElement,
|
|
||||||
MouseEvent
|
|
||||||
>
|
|
||||||
): void => {
|
|
||||||
event.stopPropagation();
|
|
||||||
this.props.onDeleteClick();
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -2,35 +2,47 @@
|
|||||||
|
|
||||||
exports[`GalleryCardComponent renders 1`] = `
|
exports[`GalleryCardComponent renders 1`] = `
|
||||||
<Card
|
<Card
|
||||||
aria-label="Notebook Card"
|
aria-label="name"
|
||||||
|
data-is-focusable="true"
|
||||||
|
onClick={[Function]}
|
||||||
tokens={
|
tokens={
|
||||||
Object {
|
Object {
|
||||||
"childrenGap": 8,
|
"childrenGap": 0,
|
||||||
"childrenMargin": 10,
|
|
||||||
"height": 384,
|
|
||||||
"width": 256,
|
"width": 256,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<CardItem>
|
<CardItem
|
||||||
|
tokens={
|
||||||
|
Object {
|
||||||
|
"padding": 10,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
<StyledPersonaBase
|
<StyledPersonaBase
|
||||||
imageUrl={false}
|
imageUrl={false}
|
||||||
secondaryText="Invalid Date"
|
secondaryText="Invalid Date"
|
||||||
text="author"
|
text="author"
|
||||||
/>
|
/>
|
||||||
</CardItem>
|
</CardItem>
|
||||||
<CardItem
|
<CardItem>
|
||||||
fill={true}
|
|
||||||
>
|
|
||||||
<Memo(StyledImageBase)
|
<Memo(StyledImageBase)
|
||||||
alt="Notebook cover image"
|
alt="name cover image"
|
||||||
height={144}
|
height={144}
|
||||||
imageFit={2}
|
imageFit={2}
|
||||||
src="thumbnailUrl"
|
src="thumbnailUrl"
|
||||||
width={256}
|
width={256}
|
||||||
/>
|
/>
|
||||||
</CardItem>
|
</CardItem>
|
||||||
<CardSection>
|
<CardSection
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"root": Object {
|
||||||
|
"padding": 10,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
<Text
|
<Text
|
||||||
nowrap={true}
|
nowrap={true}
|
||||||
variant="small"
|
variant="small"
|
||||||
@ -51,6 +63,8 @@ exports[`GalleryCardComponent renders 1`] = `
|
|||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
"fontWeight": 600,
|
"fontWeight": 600,
|
||||||
|
"paddingBottom": 8,
|
||||||
|
"paddingTop": 8,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -69,22 +83,13 @@ exports[`GalleryCardComponent renders 1`] = `
|
|||||||
>
|
>
|
||||||
description
|
description
|
||||||
</Text>
|
</Text>
|
||||||
</CardSection>
|
<span>
|
||||||
<CardSection
|
|
||||||
horizontal={true}
|
|
||||||
styles={
|
|
||||||
Object {
|
|
||||||
"root": Object {
|
|
||||||
"alignItems": "flex-end",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Text
|
<Text
|
||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
"color": "#ccc",
|
"color": undefined,
|
||||||
|
"paddingRight": 8,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -107,7 +112,8 @@ exports[`GalleryCardComponent renders 1`] = `
|
|||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
"color": "#ccc",
|
"color": undefined,
|
||||||
|
"paddingRight": 8,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -130,7 +136,8 @@ exports[`GalleryCardComponent renders 1`] = `
|
|||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
"color": "#ccc",
|
"color": undefined,
|
||||||
|
"paddingRight": 8,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -149,8 +156,18 @@ exports[`GalleryCardComponent renders 1`] = `
|
|||||||
|
|
||||||
0
|
0
|
||||||
</Text>
|
</Text>
|
||||||
|
</span>
|
||||||
</CardSection>
|
</CardSection>
|
||||||
<CardItem>
|
<CardSection
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"root": Object {
|
||||||
|
"marginLeft": 10,
|
||||||
|
"marginRight": 10,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
<Styled
|
<Styled
|
||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
@ -161,17 +178,7 @@ exports[`GalleryCardComponent renders 1`] = `
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</CardItem>
|
<span>
|
||||||
<CardSection
|
|
||||||
horizontal={true}
|
|
||||||
styles={
|
|
||||||
Object {
|
|
||||||
"root": Object {
|
|
||||||
"marginTop": 0,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<StyledTooltipHostBase
|
<StyledTooltipHostBase
|
||||||
calloutProps={
|
calloutProps={
|
||||||
Object {
|
Object {
|
||||||
@ -184,6 +191,7 @@ exports[`GalleryCardComponent renders 1`] = `
|
|||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
"display": "inline-block",
|
"display": "inline-block",
|
||||||
|
"float": "left",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -211,6 +219,7 @@ exports[`GalleryCardComponent renders 1`] = `
|
|||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
"display": "inline-block",
|
"display": "inline-block",
|
||||||
|
"float": "left",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -226,14 +235,6 @@ exports[`GalleryCardComponent renders 1`] = `
|
|||||||
title="Download"
|
title="Download"
|
||||||
/>
|
/>
|
||||||
</StyledTooltipHostBase>
|
</StyledTooltipHostBase>
|
||||||
<div
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"textAlign": "right",
|
|
||||||
"width": "100%",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<StyledTooltipHostBase
|
<StyledTooltipHostBase
|
||||||
calloutProps={
|
calloutProps={
|
||||||
Object {
|
Object {
|
||||||
@ -246,6 +247,7 @@ exports[`GalleryCardComponent renders 1`] = `
|
|||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
"display": "inline-block",
|
"display": "inline-block",
|
||||||
|
"float": "right",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -261,7 +263,7 @@ exports[`GalleryCardComponent renders 1`] = `
|
|||||||
title="Remove"
|
title="Remove"
|
||||||
/>
|
/>
|
||||||
</StyledTooltipHostBase>
|
</StyledTooltipHostBase>
|
||||||
</div>
|
</span>
|
||||||
</CardSection>
|
</CardSection>
|
||||||
</Card>
|
</Card>
|
||||||
`;
|
`;
|
||||||
|
@ -0,0 +1,114 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||||
|
import { JunoClient, IGalleryItem } from "../../../Juno/JunoClient";
|
||||||
|
import { GalleryTab, SortBy, GalleryViewerComponentProps, GalleryViewerComponent } from "./GalleryViewerComponent";
|
||||||
|
import { NotebookViewerComponentProps, NotebookViewerComponent } from "../NotebookViewer/NotebookViewerComponent";
|
||||||
|
import * as GalleryUtils from "../../../Utils/GalleryUtils";
|
||||||
|
|
||||||
|
export interface GalleryAndNotebookViewerComponentProps {
|
||||||
|
container?: ViewModels.Explorer;
|
||||||
|
junoClient: JunoClient;
|
||||||
|
notebookUrl?: string;
|
||||||
|
galleryItem?: IGalleryItem;
|
||||||
|
isFavorite?: boolean;
|
||||||
|
selectedTab: GalleryTab;
|
||||||
|
sortBy: SortBy;
|
||||||
|
searchText: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GalleryAndNotebookViewerComponentState {
|
||||||
|
notebookUrl: string;
|
||||||
|
galleryItem: IGalleryItem;
|
||||||
|
isFavorite: boolean;
|
||||||
|
selectedTab: GalleryTab;
|
||||||
|
sortBy: SortBy;
|
||||||
|
searchText: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GalleryAndNotebookViewerComponent extends React.Component<
|
||||||
|
GalleryAndNotebookViewerComponentProps,
|
||||||
|
GalleryAndNotebookViewerComponentState
|
||||||
|
> {
|
||||||
|
constructor(props: GalleryAndNotebookViewerComponentProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
notebookUrl: props.notebookUrl,
|
||||||
|
galleryItem: props.galleryItem,
|
||||||
|
isFavorite: props.isFavorite,
|
||||||
|
selectedTab: props.selectedTab,
|
||||||
|
sortBy: props.sortBy,
|
||||||
|
searchText: props.searchText
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public render(): JSX.Element {
|
||||||
|
if (this.state.notebookUrl) {
|
||||||
|
const props: NotebookViewerComponentProps = {
|
||||||
|
container: this.props.container,
|
||||||
|
junoClient: this.props.junoClient,
|
||||||
|
notebookUrl: this.state.notebookUrl,
|
||||||
|
galleryItem: this.state.galleryItem,
|
||||||
|
isFavorite: this.state.isFavorite,
|
||||||
|
backNavigationText: GalleryUtils.getTabTitle(this.state.selectedTab),
|
||||||
|
onBackClick: this.onBackClick,
|
||||||
|
onTagClick: this.loadTaggedItems
|
||||||
|
};
|
||||||
|
|
||||||
|
return <NotebookViewerComponent {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props: GalleryViewerComponentProps = {
|
||||||
|
container: this.props.container,
|
||||||
|
junoClient: this.props.junoClient,
|
||||||
|
selectedTab: this.state.selectedTab,
|
||||||
|
sortBy: this.state.sortBy,
|
||||||
|
searchText: this.state.searchText,
|
||||||
|
openNotebook: this.openNotebook,
|
||||||
|
onSelectedTabChange: this.onSelectedTabChange,
|
||||||
|
onSortByChange: this.onSortByChange,
|
||||||
|
onSearchTextChange: this.onSearchTextChange
|
||||||
|
};
|
||||||
|
|
||||||
|
return <GalleryViewerComponent {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
private onBackClick = (): void => {
|
||||||
|
this.setState({
|
||||||
|
notebookUrl: undefined
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private loadTaggedItems = (tag: string): void => {
|
||||||
|
this.setState({
|
||||||
|
notebookUrl: undefined,
|
||||||
|
searchText: tag
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private openNotebook = (data: IGalleryItem, isFavorite: boolean): void => {
|
||||||
|
this.setState({
|
||||||
|
notebookUrl: this.props.junoClient.getNotebookContentUrl(data.id),
|
||||||
|
galleryItem: data,
|
||||||
|
isFavorite
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private onSelectedTabChange = (selectedTab: GalleryTab): void => {
|
||||||
|
this.setState({
|
||||||
|
selectedTab
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private onSortByChange = (sortBy: SortBy): void => {
|
||||||
|
this.setState({
|
||||||
|
sortBy
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private onSearchTextChange = (searchText: string): void => {
|
||||||
|
this.setState({
|
||||||
|
searchText
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
import ko from "knockout";
|
||||||
|
import * as React from "react";
|
||||||
|
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
|
||||||
|
import {
|
||||||
|
GalleryAndNotebookViewerComponentProps,
|
||||||
|
GalleryAndNotebookViewerComponent
|
||||||
|
} from "./GalleryAndNotebookViewerComponent";
|
||||||
|
|
||||||
|
export class GalleryAndNotebookViewerComponentAdapter implements ReactAdapter {
|
||||||
|
public parameters: ko.Observable<number>;
|
||||||
|
|
||||||
|
constructor(private props: GalleryAndNotebookViewerComponentProps) {
|
||||||
|
this.parameters = ko.observable<number>(Date.now());
|
||||||
|
}
|
||||||
|
|
||||||
|
public renderComponent(): JSX.Element {
|
||||||
|
return <GalleryAndNotebookViewerComponent {...this.props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
public triggerRender(): void {
|
||||||
|
window.requestAnimationFrame(() => this.parameters(Date.now()));
|
||||||
|
}
|
||||||
|
}
|
@ -9,6 +9,7 @@ describe("GalleryViewerComponent", () => {
|
|||||||
selectedTab: GalleryTab.OfficialSamples,
|
selectedTab: GalleryTab.OfficialSamples,
|
||||||
sortBy: SortBy.MostViewed,
|
sortBy: SortBy.MostViewed,
|
||||||
searchText: undefined,
|
searchText: undefined,
|
||||||
|
openNotebook: undefined,
|
||||||
onSelectedTabChange: undefined,
|
onSelectedTabChange: undefined,
|
||||||
onSortByChange: undefined,
|
onSortByChange: undefined,
|
||||||
onSearchTextChange: undefined
|
onSearchTextChange: undefined
|
||||||
|
@ -31,6 +31,7 @@ export interface GalleryViewerComponentProps {
|
|||||||
selectedTab: GalleryTab;
|
selectedTab: GalleryTab;
|
||||||
sortBy: SortBy;
|
sortBy: SortBy;
|
||||||
searchText: string;
|
searchText: string;
|
||||||
|
openNotebook: (data: IGalleryItem, isFavorite: boolean) => void;
|
||||||
onSelectedTabChange: (newTab: GalleryTab) => void;
|
onSelectedTabChange: (newTab: GalleryTab) => void;
|
||||||
onSortByChange: (sortBy: SortBy) => void;
|
onSortByChange: (sortBy: SortBy) => void;
|
||||||
onSearchTextChange: (searchText: string) => void;
|
onSearchTextChange: (searchText: string) => void;
|
||||||
@ -66,13 +67,14 @@ interface GalleryTabInfo {
|
|||||||
content: JSX.Element;
|
content: JSX.Element;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class GalleryViewerComponent extends React.Component<GalleryViewerComponentProps, GalleryViewerComponentState>
|
export class GalleryViewerComponent extends React.Component<GalleryViewerComponentProps, GalleryViewerComponentState> {
|
||||||
implements GalleryUtils.DialogEnabledComponent {
|
|
||||||
public static readonly OfficialSamplesTitle = "Official samples";
|
public static readonly OfficialSamplesTitle = "Official samples";
|
||||||
public static readonly PublicGalleryTitle = "Public gallery";
|
public static readonly PublicGalleryTitle = "Public gallery";
|
||||||
public static readonly FavoritesTitle = "Liked";
|
public static readonly FavoritesTitle = "Liked";
|
||||||
public static readonly PublishedTitle = "Your published work";
|
public static readonly PublishedTitle = "Your published work";
|
||||||
|
|
||||||
|
private static readonly rowsPerPage = 5;
|
||||||
|
|
||||||
private static readonly mostViewedText = "Most viewed";
|
private static readonly mostViewedText = "Most viewed";
|
||||||
private static readonly mostDownloadedText = "Most downloaded";
|
private static readonly mostDownloadedText = "Most downloaded";
|
||||||
private static readonly mostFavoritedText = "Most liked";
|
private static readonly mostFavoritedText = "Most liked";
|
||||||
@ -128,10 +130,6 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setDialogProps = (dialogProps: DialogProps): void => {
|
|
||||||
this.setState({ dialogProps });
|
|
||||||
};
|
|
||||||
|
|
||||||
public render(): JSX.Element {
|
public render(): JSX.Element {
|
||||||
const tabs: GalleryTabInfo[] = [this.createTab(GalleryTab.OfficialSamples, this.state.sampleNotebooks)];
|
const tabs: GalleryTabInfo[] = [this.createTab(GalleryTab.OfficialSamples, this.state.sampleNotebooks)];
|
||||||
|
|
||||||
@ -178,8 +176,8 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
|||||||
|
|
||||||
private createTabContent(data: IGalleryItem[]): JSX.Element {
|
private createTabContent(data: IGalleryItem[]): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<Stack tokens={{ childrenGap: 20 }}>
|
<Stack tokens={{ childrenGap: 10 }}>
|
||||||
<Stack horizontal tokens={{ childrenGap: 20 }}>
|
<Stack horizontal tokens={{ childrenGap: 20, padding: 10 }}>
|
||||||
<Stack.Item grow>
|
<Stack.Item grow>
|
||||||
<SearchBox value={this.state.searchText} placeholder="Search" onChange={this.onSearchBoxChange} />
|
<SearchBox value={this.state.searchText} placeholder="Search" onChange={this.onSearchBoxChange} />
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
@ -391,7 +389,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
|||||||
private getPageSpecification = (itemIndex?: number, visibleRect?: IRectangle): IPageSpecification => {
|
private getPageSpecification = (itemIndex?: number, visibleRect?: IRectangle): IPageSpecification => {
|
||||||
if (itemIndex === 0) {
|
if (itemIndex === 0) {
|
||||||
this.columnCount = Math.floor(visibleRect.width / GalleryCardComponent.CARD_WIDTH) || this.columnCount;
|
this.columnCount = Math.floor(visibleRect.width / GalleryCardComponent.CARD_WIDTH) || this.columnCount;
|
||||||
this.rowCount = Math.ceil(visibleRect.height / GalleryCardComponent.CARD_HEIGHT) || this.rowCount;
|
this.rowCount = GalleryViewerComponent.rowsPerPage;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -408,8 +406,9 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
|||||||
const props: GalleryCardComponentProps = {
|
const props: GalleryCardComponentProps = {
|
||||||
data,
|
data,
|
||||||
isFavorite,
|
isFavorite,
|
||||||
|
showDownload: !!this.props.container,
|
||||||
showDelete: this.state.selectedTab === GalleryTab.Published,
|
showDelete: this.state.selectedTab === GalleryTab.Published,
|
||||||
onClick: () => this.openNotebook(data, isFavorite),
|
onClick: () => this.props.openNotebook(data, isFavorite),
|
||||||
onTagClick: this.loadTaggedItems,
|
onTagClick: this.loadTaggedItems,
|
||||||
onFavoriteClick: () => this.favoriteItem(data),
|
onFavoriteClick: () => this.favoriteItem(data),
|
||||||
onUnfavoriteClick: () => this.unfavoriteItem(data),
|
onUnfavoriteClick: () => this.unfavoriteItem(data),
|
||||||
@ -424,20 +423,6 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
private openNotebook = (data: IGalleryItem, isFavorite: boolean): void => {
|
|
||||||
if (this.props.container && this.props.junoClient) {
|
|
||||||
this.props.container.openGallery(this.props.junoClient.getNotebookContentUrl(data.id), data, isFavorite);
|
|
||||||
} else {
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
[GalleryUtils.NotebookViewerParams.NotebookUrl]: this.props.junoClient.getNotebookContentUrl(data.id),
|
|
||||||
[GalleryUtils.NotebookViewerParams.GalleryItemId]: data.id
|
|
||||||
});
|
|
||||||
|
|
||||||
const location = new URL("./notebookViewer.html", window.location.href).href;
|
|
||||||
window.open(`${location}?${params.toString()}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private loadTaggedItems = (tag: string): void => {
|
private loadTaggedItems = (tag: string): void => {
|
||||||
const searchText = tag;
|
const searchText = tag;
|
||||||
this.setState({
|
this.setState({
|
||||||
@ -467,9 +452,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
|||||||
};
|
};
|
||||||
|
|
||||||
private downloadItem = async (data: IGalleryItem): Promise<void> => {
|
private downloadItem = async (data: IGalleryItem): Promise<void> => {
|
||||||
GalleryUtils.downloadItem(this, this.props.container, this.props.junoClient, data, item =>
|
GalleryUtils.downloadItem(this.props.container, this.props.junoClient, data, item => this.refreshSelectedTab(item));
|
||||||
this.refreshSelectedTab(item)
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private deleteItem = async (data: IGalleryItem): Promise<void> => {
|
private deleteItem = async (data: IGalleryItem): Promise<void> => {
|
||||||
|
@ -21,7 +21,7 @@ exports[`GalleryViewerComponent renders 1`] = `
|
|||||||
<Stack
|
<Stack
|
||||||
tokens={
|
tokens={
|
||||||
Object {
|
Object {
|
||||||
"childrenGap": 20,
|
"childrenGap": 10,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@ -30,6 +30,7 @@ exports[`GalleryViewerComponent renders 1`] = `
|
|||||||
tokens={
|
tokens={
|
||||||
Object {
|
Object {
|
||||||
"childrenGap": 20,
|
"childrenGap": 20,
|
||||||
|
"padding": 10,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
@ -16,11 +16,12 @@ import * as React from "react";
|
|||||||
import { IGalleryItem } from "../../../Juno/JunoClient";
|
import { IGalleryItem } from "../../../Juno/JunoClient";
|
||||||
import { FileSystemUtil } from "../../Notebook/FileSystemUtil";
|
import { FileSystemUtil } from "../../Notebook/FileSystemUtil";
|
||||||
import "./NotebookViewerComponent.less";
|
import "./NotebookViewerComponent.less";
|
||||||
|
import CosmosDBLogo from "../../../../images/CosmosDB-logo.svg";
|
||||||
|
|
||||||
export interface NotebookMetadataComponentProps {
|
export interface NotebookMetadataComponentProps {
|
||||||
data: IGalleryItem;
|
data: IGalleryItem;
|
||||||
isFavorite: boolean;
|
isFavorite: boolean;
|
||||||
downloadButtonText: string;
|
downloadButtonText?: string;
|
||||||
onTagClick: (tag: string) => void;
|
onTagClick: (tag: string) => void;
|
||||||
onFavoriteClick: () => void;
|
onFavoriteClick: () => void;
|
||||||
onUnfavoriteClick: () => void;
|
onUnfavoriteClick: () => void;
|
||||||
@ -54,11 +55,18 @@ export class NotebookMetadataComponent extends React.Component<NotebookMetadataC
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
|
{this.props.downloadButtonText && (
|
||||||
<PrimaryButton text={this.props.downloadButtonText} onClick={this.props.onDownloadClick} />
|
<PrimaryButton text={this.props.downloadButtonText} onClick={this.props.onDownloadClick} />
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<Stack horizontal verticalAlign="center" tokens={{ childrenGap: 10 }}>
|
<Stack horizontal verticalAlign="center" tokens={{ childrenGap: 10 }}>
|
||||||
<Persona text={this.props.data.author} size={PersonaSize.size32} />
|
<Persona
|
||||||
|
imageUrl={this.props.data.isSample && CosmosDBLogo}
|
||||||
|
text={this.props.data.author}
|
||||||
|
size={PersonaSize.size32}
|
||||||
|
/>
|
||||||
<Text>{dateString}</Text>
|
<Text>{dateString}</Text>
|
||||||
<Text>
|
<Text>
|
||||||
<Icon iconName="RedEye" /> {this.props.data.views}
|
<Icon iconName="RedEye" /> {this.props.data.views}
|
||||||
|
@ -39,8 +39,10 @@ interface NotebookViewerComponentState {
|
|||||||
showProgressBar: boolean;
|
showProgressBar: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class NotebookViewerComponent extends React.Component<NotebookViewerComponentProps, NotebookViewerComponentState>
|
export class NotebookViewerComponent extends React.Component<
|
||||||
implements GalleryUtils.DialogEnabledComponent {
|
NotebookViewerComponentProps,
|
||||||
|
NotebookViewerComponentState
|
||||||
|
> {
|
||||||
private clientManager: NotebookClientV2;
|
private clientManager: NotebookClientV2;
|
||||||
private notebookComponentBootstrapper: NotebookComponentBootstrapper;
|
private notebookComponentBootstrapper: NotebookComponentBootstrapper;
|
||||||
|
|
||||||
@ -73,10 +75,6 @@ export class NotebookViewerComponent extends React.Component<NotebookViewerCompo
|
|||||||
this.loadNotebookContent();
|
this.loadNotebookContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
setDialogProps = (dialogProps: DialogProps): void => {
|
|
||||||
this.setState({ dialogProps });
|
|
||||||
};
|
|
||||||
|
|
||||||
private async loadNotebookContent(): Promise<void> {
|
private async loadNotebookContent(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(this.props.notebookUrl);
|
const response = await fetch(this.props.notebookUrl);
|
||||||
@ -121,9 +119,7 @@ export class NotebookViewerComponent extends React.Component<NotebookViewerCompo
|
|||||||
<NotebookMetadataComponent
|
<NotebookMetadataComponent
|
||||||
data={this.state.galleryItem}
|
data={this.state.galleryItem}
|
||||||
isFavorite={this.state.isFavorite}
|
isFavorite={this.state.isFavorite}
|
||||||
downloadButtonText={
|
downloadButtonText={this.props.container && "Download to my notebooks"}
|
||||||
this.props.container ? "Download to my notebooks" : "Edit/Run in Cosmos DB data explorer"
|
|
||||||
}
|
|
||||||
onTagClick={this.props.onTagClick}
|
onTagClick={this.props.onTagClick}
|
||||||
onFavoriteClick={this.favoriteItem}
|
onFavoriteClick={this.favoriteItem}
|
||||||
onUnfavoriteClick={this.unfavoriteItem}
|
onUnfavoriteClick={this.unfavoriteItem}
|
||||||
@ -179,7 +175,7 @@ export class NotebookViewerComponent extends React.Component<NotebookViewerCompo
|
|||||||
};
|
};
|
||||||
|
|
||||||
private downloadItem = async (): Promise<void> => {
|
private downloadItem = async (): Promise<void> => {
|
||||||
GalleryUtils.downloadItem(this, this.props.container, this.props.junoClient, this.state.galleryItem, item =>
|
GalleryUtils.downloadItem(this.props.container, this.props.junoClient, this.state.galleryItem, item =>
|
||||||
this.setState({ galleryItem: item })
|
this.setState({ galleryItem: item })
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -48,6 +48,7 @@ exports[`NotebookMetadataComponent renders liked notebook 1`] = `
|
|||||||
verticalAlign="center"
|
verticalAlign="center"
|
||||||
>
|
>
|
||||||
<StyledPersonaBase
|
<StyledPersonaBase
|
||||||
|
imageUrl={false}
|
||||||
size={11}
|
size={11}
|
||||||
text="author"
|
text="author"
|
||||||
/>
|
/>
|
||||||
@ -147,6 +148,7 @@ exports[`NotebookMetadataComponent renders un-liked notebook 1`] = `
|
|||||||
verticalAlign="center"
|
verticalAlign="center"
|
||||||
>
|
>
|
||||||
<StyledPersonaBase
|
<StyledPersonaBase
|
||||||
|
imageUrl={false}
|
||||||
size={11}
|
size={11}
|
||||||
text="author"
|
text="author"
|
||||||
/>
|
/>
|
||||||
|
@ -3101,7 +3101,6 @@ export default class Explorer implements ViewModels.Explorer {
|
|||||||
|
|
||||||
if (galleryTab) {
|
if (galleryTab) {
|
||||||
this.tabsManager.activateTab(galleryTab);
|
this.tabsManager.activateTab(galleryTab);
|
||||||
(galleryTab as any).updateGalleryParams(notebookUrl, galleryItem, isFavorite);
|
|
||||||
} else {
|
} else {
|
||||||
if (!this.galleryTab) {
|
if (!this.galleryTab) {
|
||||||
this.galleryTab = await import(/* webpackChunkName: "GalleryTab" */ "./Tabs/GalleryTab");
|
this.galleryTab = await import(/* webpackChunkName: "GalleryTab" */ "./Tabs/GalleryTab");
|
||||||
|
@ -1 +1 @@
|
|||||||
<div style="height: 100%" data-bind="react:galleryComponentAdapter, setTemplateReady: true"></div>
|
<div style="height: 100%" data-bind="react:galleryAndNotebookViewerComponentAdapter, setTemplateReady: true"></div>
|
||||||
|
@ -1,129 +1,21 @@
|
|||||||
import * as ko from "knockout";
|
|
||||||
import * as React from "react";
|
|
||||||
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
|
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
import * as ViewModels from "../../Contracts/ViewModels";
|
||||||
import { IGalleryItem, JunoClient } from "../../Juno/JunoClient";
|
import { GalleryAndNotebookViewerComponentProps } from "../Controls/NotebookGallery/GalleryAndNotebookViewerComponent";
|
||||||
import * as GalleryUtils from "../../Utils/GalleryUtils";
|
import { GalleryAndNotebookViewerComponentAdapter } from "../Controls/NotebookGallery/GalleryAndNotebookViewerComponentAdapter";
|
||||||
import {
|
import { GalleryTab as GalleryViewerTab, SortBy } from "../Controls/NotebookGallery/GalleryViewerComponent";
|
||||||
GalleryTab as GalleryViewerTab,
|
|
||||||
GalleryViewerComponent,
|
|
||||||
GalleryViewerComponentProps,
|
|
||||||
SortBy
|
|
||||||
} from "../Controls/NotebookGallery/GalleryViewerComponent";
|
|
||||||
import {
|
|
||||||
NotebookViewerComponent,
|
|
||||||
NotebookViewerComponentProps
|
|
||||||
} from "../Controls/NotebookViewer/NotebookViewerComponent";
|
|
||||||
import TabsBase from "./TabsBase";
|
import TabsBase from "./TabsBase";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notebook gallery tab
|
* Notebook gallery tab
|
||||||
*/
|
*/
|
||||||
interface GalleryComponentAdapterProps {
|
|
||||||
container: ViewModels.Explorer;
|
|
||||||
junoClient: JunoClient;
|
|
||||||
notebookUrl: string;
|
|
||||||
galleryItem: IGalleryItem;
|
|
||||||
isFavorite: boolean;
|
|
||||||
selectedTab: GalleryViewerTab;
|
|
||||||
sortBy: SortBy;
|
|
||||||
searchText: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GalleryComponentAdapterState {
|
|
||||||
notebookUrl: string;
|
|
||||||
galleryItem: IGalleryItem;
|
|
||||||
isFavorite: boolean;
|
|
||||||
selectedTab: GalleryViewerTab;
|
|
||||||
sortBy: SortBy;
|
|
||||||
searchText: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
class GalleryComponentAdapter implements ReactAdapter {
|
|
||||||
public parameters: ko.Observable<number>;
|
|
||||||
private state: GalleryComponentAdapterState;
|
|
||||||
|
|
||||||
constructor(private props: GalleryComponentAdapterProps) {
|
|
||||||
this.parameters = ko.observable<number>(Date.now());
|
|
||||||
this.state = {
|
|
||||||
notebookUrl: props.notebookUrl,
|
|
||||||
galleryItem: props.galleryItem,
|
|
||||||
isFavorite: props.isFavorite,
|
|
||||||
selectedTab: props.selectedTab,
|
|
||||||
sortBy: props.sortBy,
|
|
||||||
searchText: props.searchText
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public renderComponent(): JSX.Element {
|
|
||||||
if (this.state.notebookUrl) {
|
|
||||||
const props: NotebookViewerComponentProps = {
|
|
||||||
container: this.props.container,
|
|
||||||
junoClient: this.props.junoClient,
|
|
||||||
notebookUrl: this.state.notebookUrl,
|
|
||||||
galleryItem: this.state.galleryItem,
|
|
||||||
isFavorite: this.state.isFavorite,
|
|
||||||
backNavigationText: GalleryUtils.getTabTitle(this.state.selectedTab),
|
|
||||||
onBackClick: this.onBackClick,
|
|
||||||
onTagClick: this.loadTaggedItems
|
|
||||||
};
|
|
||||||
|
|
||||||
return <NotebookViewerComponent {...props} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
const props: GalleryViewerComponentProps = {
|
|
||||||
container: this.props.container,
|
|
||||||
junoClient: this.props.junoClient,
|
|
||||||
selectedTab: this.state.selectedTab,
|
|
||||||
sortBy: this.state.sortBy,
|
|
||||||
searchText: this.state.searchText,
|
|
||||||
onSelectedTabChange: this.onSelectedTabChange,
|
|
||||||
onSortByChange: this.onSortByChange,
|
|
||||||
onSearchTextChange: this.onSearchTextChange
|
|
||||||
};
|
|
||||||
|
|
||||||
return <GalleryViewerComponent {...props} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
public setState(state: Partial<GalleryComponentAdapterState>): void {
|
|
||||||
this.state = Object.assign(this.state, state);
|
|
||||||
window.requestAnimationFrame(() => this.parameters(Date.now()));
|
|
||||||
}
|
|
||||||
|
|
||||||
private onBackClick = (): void => {
|
|
||||||
this.props.container.openGallery();
|
|
||||||
};
|
|
||||||
|
|
||||||
private loadTaggedItems = (tag: string): void => {
|
|
||||||
this.setState({
|
|
||||||
notebookUrl: undefined,
|
|
||||||
searchText: tag
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
private onSelectedTabChange = (selectedTab: GalleryViewerTab): void => {
|
|
||||||
this.state.selectedTab = selectedTab;
|
|
||||||
};
|
|
||||||
|
|
||||||
private onSortByChange = (sortBy: SortBy): void => {
|
|
||||||
this.state.sortBy = sortBy;
|
|
||||||
};
|
|
||||||
|
|
||||||
private onSearchTextChange = (searchText: string): void => {
|
|
||||||
this.state.searchText = searchText;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class GalleryTab extends TabsBase implements ViewModels.Tab {
|
export default class GalleryTab extends TabsBase implements ViewModels.Tab {
|
||||||
private container: ViewModels.Explorer;
|
private container: ViewModels.Explorer;
|
||||||
private galleryComponentAdapterProps: GalleryComponentAdapterProps;
|
public galleryAndNotebookViewerComponentAdapter: GalleryAndNotebookViewerComponentAdapter;
|
||||||
private galleryComponentAdapter: GalleryComponentAdapter;
|
|
||||||
|
|
||||||
constructor(options: ViewModels.GalleryTabOptions) {
|
constructor(options: ViewModels.GalleryTabOptions) {
|
||||||
super(options);
|
super(options);
|
||||||
|
|
||||||
this.container = options.container;
|
this.container = options.container;
|
||||||
this.galleryComponentAdapterProps = {
|
const props: GalleryAndNotebookViewerComponentProps = {
|
||||||
container: options.container,
|
container: options.container,
|
||||||
junoClient: options.junoClient,
|
junoClient: options.junoClient,
|
||||||
notebookUrl: options.notebookUrl,
|
notebookUrl: options.notebookUrl,
|
||||||
@ -134,18 +26,10 @@ export default class GalleryTab extends TabsBase implements ViewModels.Tab {
|
|||||||
searchText: undefined
|
searchText: undefined
|
||||||
};
|
};
|
||||||
|
|
||||||
this.galleryComponentAdapter = new GalleryComponentAdapter(this.galleryComponentAdapterProps);
|
this.galleryAndNotebookViewerComponentAdapter = new GalleryAndNotebookViewerComponentAdapter(props);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getContainer(): ViewModels.Explorer {
|
protected getContainer(): ViewModels.Explorer {
|
||||||
return this.container;
|
return this.container;
|
||||||
}
|
}
|
||||||
|
|
||||||
public updateGalleryParams(notebookUrl?: string, galleryItem?: IGalleryItem, isFavorite?: boolean): void {
|
|
||||||
this.galleryComponentAdapter.setState({
|
|
||||||
notebookUrl,
|
|
||||||
galleryItem,
|
|
||||||
isFavorite
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,31 +1,33 @@
|
|||||||
import "bootstrap/dist/css/bootstrap.css";
|
import "bootstrap/dist/css/bootstrap.css";
|
||||||
import { initializeIcons } from "office-ui-fabric-react/lib/Icons";
|
import { initializeIcons } from "office-ui-fabric-react/lib/Icons";
|
||||||
|
import { Text, Link } from "office-ui-fabric-react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as ReactDOM from "react-dom";
|
import * as ReactDOM from "react-dom";
|
||||||
import { initializeConfiguration } from "../Config";
|
import { initializeConfiguration } from "../Config";
|
||||||
|
import { GalleryHeaderComponent } from "../Explorer/Controls/Header/GalleryHeaderComponent";
|
||||||
import {
|
import {
|
||||||
GalleryTab,
|
GalleryAndNotebookViewerComponent,
|
||||||
GalleryViewerComponent,
|
GalleryAndNotebookViewerComponentProps
|
||||||
GalleryViewerComponentProps,
|
} from "../Explorer/Controls/NotebookGallery/GalleryAndNotebookViewerComponent";
|
||||||
SortBy
|
import { GalleryTab, SortBy } from "../Explorer/Controls/NotebookGallery/GalleryViewerComponent";
|
||||||
} from "../Explorer/Controls/NotebookGallery/GalleryViewerComponent";
|
|
||||||
import { JunoClient } from "../Juno/JunoClient";
|
import { JunoClient } from "../Juno/JunoClient";
|
||||||
import * as GalleryUtils from "../Utils/GalleryUtils";
|
import * as GalleryUtils from "../Utils/GalleryUtils";
|
||||||
import { GalleryHeaderComponent } from "../Explorer/Controls/Header/GalleryHeaderComponent";
|
|
||||||
|
const enableNotebooksUrl = "https://aka.ms/cosmos-enable-notebooks";
|
||||||
|
const createAccountUrl = "https://aka.ms/cosmos-create-account-portal";
|
||||||
|
|
||||||
const onInit = async () => {
|
const onInit = async () => {
|
||||||
|
const dataExplorerUrl = new URL("./", window.location.href).href;
|
||||||
|
|
||||||
initializeIcons();
|
initializeIcons();
|
||||||
await initializeConfiguration();
|
await initializeConfiguration();
|
||||||
const galleryViewerProps = GalleryUtils.getGalleryViewerProps(window.location.search);
|
const galleryViewerProps = GalleryUtils.getGalleryViewerProps(window.location.search);
|
||||||
|
|
||||||
const props: GalleryViewerComponentProps = {
|
const props: GalleryAndNotebookViewerComponentProps = {
|
||||||
junoClient: new JunoClient(),
|
junoClient: new JunoClient(),
|
||||||
selectedTab: galleryViewerProps.selectedTab || GalleryTab.OfficialSamples,
|
selectedTab: galleryViewerProps.selectedTab || GalleryTab.OfficialSamples,
|
||||||
sortBy: galleryViewerProps.sortBy || SortBy.MostViewed,
|
sortBy: galleryViewerProps.sortBy || SortBy.MostViewed,
|
||||||
searchText: galleryViewerProps.searchText,
|
searchText: galleryViewerProps.searchText
|
||||||
onSelectedTabChange: undefined,
|
|
||||||
onSortByChange: undefined,
|
|
||||||
onSearchTextChange: undefined
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const element = (
|
const element = (
|
||||||
@ -34,7 +36,21 @@ const onInit = async () => {
|
|||||||
<GalleryHeaderComponent />
|
<GalleryHeaderComponent />
|
||||||
</header>
|
</header>
|
||||||
<div style={{ marginLeft: 138, marginRight: 138 }}>
|
<div style={{ marginLeft: 138, marginRight: 138 }}>
|
||||||
<GalleryViewerComponent {...props} />
|
<div style={{ paddingLeft: 26, paddingRight: 26, paddingTop: 20 }}>
|
||||||
|
<Text block>
|
||||||
|
Welcome to the Azure Cosmos DB notebooks gallery! View the sample notebooks to learn about use cases, best
|
||||||
|
practices, and how to get started with Azure Cosmos DB.
|
||||||
|
</Text>
|
||||||
|
<Text styles={{ root: { marginTop: 10 } }} block>
|
||||||
|
If you'd like to run or edit the notebook in your own Azure Cosmos DB account,{" "}
|
||||||
|
<Link href={dataExplorerUrl}>sign in</Link> and select an account with{" "}
|
||||||
|
<Link href={enableNotebooksUrl}>notebooks enabled</Link>. From there, you can download the sample to your
|
||||||
|
account. If you don't have an account yet, you can{" "}
|
||||||
|
<Link href={createAccountUrl}>create one from the Azure portal</Link>.
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<GalleryAndNotebookViewerComponent {...props} />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
<link rel="shortcut icon" href="images/CosmosDB_rgb_ui_lighttheme.ico" type="image/x-icon" />
|
<link rel="shortcut icon" href="images/CosmosDB_rgb_ui_lighttheme.ico" type="image/x-icon" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body style="overflow-y:scroll">
|
||||||
<div class="galleryContent" id="galleryContent"></div>
|
<div class="galleryContent" id="galleryContent"></div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -24,22 +24,12 @@ describe("GalleryUtils", () => {
|
|||||||
jest.resetAllMocks();
|
jest.resetAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("downloadItem shows dialog in standalone gallery", () => {
|
|
||||||
const setDialogProps = jest.fn().mockImplementation();
|
|
||||||
|
|
||||||
GalleryUtils.downloadItem({ setDialogProps }, undefined, undefined, galleryItem, undefined);
|
|
||||||
|
|
||||||
expect(setDialogProps).toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("downloadItem shows dialog in data explorer", () => {
|
it("downloadItem shows dialog in data explorer", () => {
|
||||||
const setDialogProps = jest.fn().mockImplementation();
|
|
||||||
const container = new ExplorerStub();
|
const container = new ExplorerStub();
|
||||||
container.showOkCancelModalDialog = jest.fn().mockImplementation();
|
container.showOkCancelModalDialog = jest.fn().mockImplementation();
|
||||||
|
|
||||||
GalleryUtils.downloadItem({ setDialogProps }, container, undefined, galleryItem, undefined);
|
GalleryUtils.downloadItem(container, undefined, galleryItem, undefined);
|
||||||
|
|
||||||
expect(setDialogProps).not.toBeCalled();
|
|
||||||
expect(container.showOkCancelModalDialog).toBeCalled();
|
expect(container.showOkCancelModalDialog).toBeCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { LinkProps, DialogProps } from "../Explorer/Controls/DialogReactComponent/DialogComponent";
|
|
||||||
import { IGalleryItem, JunoClient } from "../Juno/JunoClient";
|
import { IGalleryItem, JunoClient } from "../Juno/JunoClient";
|
||||||
import * as ViewModels from "../Contracts/ViewModels";
|
import * as ViewModels from "../Contracts/ViewModels";
|
||||||
import { NotificationConsoleUtils } from "./NotificationConsoleUtils";
|
import { NotificationConsoleUtils } from "./NotificationConsoleUtils";
|
||||||
@ -10,10 +9,6 @@ import {
|
|||||||
GalleryViewerComponent
|
GalleryViewerComponent
|
||||||
} from "../Explorer/Controls/NotebookGallery/GalleryViewerComponent";
|
} from "../Explorer/Controls/NotebookGallery/GalleryViewerComponent";
|
||||||
|
|
||||||
export interface DialogEnabledComponent {
|
|
||||||
setDialogProps: (dialogProps: DialogProps) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum NotebookViewerParams {
|
export enum NotebookViewerParams {
|
||||||
NotebookUrl = "notebookUrl",
|
NotebookUrl = "notebookUrl",
|
||||||
GalleryItemId = "galleryItemId",
|
GalleryItemId = "galleryItemId",
|
||||||
@ -38,48 +33,13 @@ export interface GalleryViewerProps {
|
|||||||
searchText: string;
|
searchText: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function showOkCancelModalDialog(
|
|
||||||
component: DialogEnabledComponent,
|
|
||||||
title: string,
|
|
||||||
msg: string,
|
|
||||||
linkProps: LinkProps,
|
|
||||||
showCloseButton: boolean,
|
|
||||||
okLabel: string,
|
|
||||||
onOk: () => void,
|
|
||||||
cancelLabel: string,
|
|
||||||
onCancel: () => void
|
|
||||||
): void {
|
|
||||||
component.setDialogProps({
|
|
||||||
linkProps,
|
|
||||||
isModal: true,
|
|
||||||
visible: true,
|
|
||||||
title,
|
|
||||||
subText: msg,
|
|
||||||
primaryButtonText: okLabel,
|
|
||||||
secondaryButtonText: cancelLabel,
|
|
||||||
onPrimaryButtonClick: () => {
|
|
||||||
component.setDialogProps(undefined);
|
|
||||||
onOk && onOk();
|
|
||||||
},
|
|
||||||
onSecondaryButtonClick: () => {
|
|
||||||
component.setDialogProps(undefined);
|
|
||||||
onCancel && onCancel();
|
|
||||||
},
|
|
||||||
showCloseButton,
|
|
||||||
onDismiss: () => component.setDialogProps(undefined)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function downloadItem(
|
export function downloadItem(
|
||||||
component: DialogEnabledComponent,
|
|
||||||
container: ViewModels.Explorer,
|
container: ViewModels.Explorer,
|
||||||
junoClient: JunoClient,
|
junoClient: JunoClient,
|
||||||
data: IGalleryItem,
|
data: IGalleryItem,
|
||||||
onComplete: (item: IGalleryItem) => void
|
onComplete: (item: IGalleryItem) => void
|
||||||
): void {
|
): void {
|
||||||
const name = data.name;
|
const name = data.name;
|
||||||
|
|
||||||
if (container) {
|
|
||||||
container.showOkCancelModalDialog(
|
container.showOkCancelModalDialog(
|
||||||
"Download to My Notebooks",
|
"Download to My Notebooks",
|
||||||
`Download ${name} from gallery as a copy to your notebooks to run and/or edit the notebook.`,
|
`Download ${name} from gallery as a copy to your notebooks to run and/or edit the notebook.`,
|
||||||
@ -117,26 +77,6 @@ export function downloadItem(
|
|||||||
"Cancel",
|
"Cancel",
|
||||||
undefined
|
undefined
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
showOkCancelModalDialog(
|
|
||||||
component,
|
|
||||||
"Edit/Run notebook in Cosmos DB data explorer",
|
|
||||||
`In order to edit/run ${name} in Cosmos DB data explorer, a Cosmos DB account will be needed. If you do not have a Cosmos DB account yet, please create one.`,
|
|
||||||
{
|
|
||||||
linkText: "Learn more about Cosmos DB",
|
|
||||||
linkUrl: "https://azure.microsoft.com/en-us/services/cosmos-db"
|
|
||||||
},
|
|
||||||
true,
|
|
||||||
"Open data explorer",
|
|
||||||
() => {
|
|
||||||
window.open("https://cosmos.azure.com");
|
|
||||||
},
|
|
||||||
"Create Cosmos DB account",
|
|
||||||
() => {
|
|
||||||
window.open("https://ms.portal.azure.com/#create/Microsoft.DocumentDB");
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function favoriteItem(
|
export async function favoriteItem(
|
||||||
|
@ -148,7 +148,7 @@ module.exports = function(env = {}, argv = {}) {
|
|||||||
chunks: ["notebookViewer"]
|
chunks: ["notebookViewer"]
|
||||||
}),
|
}),
|
||||||
new HtmlWebpackPlugin({
|
new HtmlWebpackPlugin({
|
||||||
filename: "galleryViewer.html",
|
filename: "gallery/index.html",
|
||||||
template: "src/GalleryViewer/galleryViewer.html",
|
template: "src/GalleryViewer/galleryViewer.html",
|
||||||
chunks: ["galleryViewer"]
|
chunks: ["galleryViewer"]
|
||||||
}),
|
}),
|
||||||
|
Loading…
Reference in New Issue
Block a user