mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2024-11-24 22:46:40 +00:00
Notebooks Gallery (#59)
* Initial commit * Address PR comments * Move notebook related stuff to NotebookManager and dynamically load it * Add New gallery callout and other UI tweaks * Update test snapshot
This commit is contained in:
parent
dd199e6565
commit
7512b3c1d5
@ -112,6 +112,7 @@ export class Features {
|
||||
public static readonly enableTtl = "enablettl";
|
||||
public static readonly enableNotebooks = "enablenotebooks";
|
||||
public static readonly enableGallery = "enablegallery";
|
||||
public static readonly enableGalleryPublish = "enablegallerypublish";
|
||||
public static readonly enableSpark = "enablespark";
|
||||
public static readonly livyEndpoint = "livyendpoint";
|
||||
public static readonly notebookServerUrl = "notebookserverurl";
|
||||
|
@ -704,49 +704,6 @@ export interface MemoryUsageInfo {
|
||||
totalKB: number;
|
||||
}
|
||||
|
||||
export interface NotebookMetadata {
|
||||
date: string;
|
||||
description: string;
|
||||
tags: string[];
|
||||
author: string;
|
||||
views: number;
|
||||
likes: number;
|
||||
downloads: number;
|
||||
imageUrl: string;
|
||||
}
|
||||
|
||||
export interface UserMetadata {
|
||||
likedNotebooks: string[];
|
||||
}
|
||||
|
||||
export interface GitHubInfoJunoResponse {
|
||||
encoding: string;
|
||||
encodedContent: string;
|
||||
content: string;
|
||||
target: string;
|
||||
submoduleGitUrl: string;
|
||||
name: string;
|
||||
path: string;
|
||||
sha: string;
|
||||
size: number;
|
||||
type: {
|
||||
stringValue: string;
|
||||
value: number;
|
||||
};
|
||||
downloadUrl: string;
|
||||
url: string;
|
||||
gitUrl: string;
|
||||
htmlUrl: string;
|
||||
metadata?: NotebookMetadata;
|
||||
officialSamplesIndex?: number;
|
||||
isLikedNotebook?: boolean;
|
||||
}
|
||||
|
||||
export interface LikedNotebooksJunoResponse {
|
||||
likedNotebooksContent: GitHubInfoJunoResponse[];
|
||||
userMetadata: UserMetadata;
|
||||
}
|
||||
|
||||
export interface resourceTokenConnectionStringProperties {
|
||||
accountEndpoint: string;
|
||||
collectionId: string;
|
||||
|
@ -12,10 +12,8 @@ import { CommandButtonComponentProps } from "../Explorer/Controls/CommandButton/
|
||||
import { ConsoleData } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import { ExecuteSprocParam } from "../Explorer/Panes/ExecuteSprocParamsPane";
|
||||
import { GitHubClient } from "../GitHub/GitHubClient";
|
||||
import { GitHubOAuthService } from "../GitHub/GitHubOAuthService";
|
||||
import { IColumnSetting } from "../Explorer/Panes/Tables/TableColumnOptionsPane";
|
||||
import { IContentProvider } from "@nteract/core";
|
||||
import { JunoClient } from "../Juno/JunoClient";
|
||||
import { JunoClient, IGalleryItem } from "../Juno/JunoClient";
|
||||
import { Library } from "./DataModels";
|
||||
import { MostRecentActivity } from "../Explorer/MostRecentActivity/MostRecentActivity";
|
||||
import { NotebookContentItem } from "../Explorer/Notebook/NotebookContentItem";
|
||||
@ -27,6 +25,7 @@ import { StringInputPane } from "../Explorer/Panes/StringInputPane";
|
||||
import { TextFieldProps } from "../Explorer/Controls/DialogReactComponent/DialogComponent";
|
||||
import { UploadDetails } from "../workers/upload/definitions";
|
||||
import { UploadItemsPaneAdapter } from "../Explorer/Panes/UploadItemsPaneAdapter";
|
||||
import { ReactAdapter } from "../Bindings/ReactBindingHandler";
|
||||
|
||||
export interface ExplorerOptions {
|
||||
documentClientUtility: DocumentClientUtilityBase;
|
||||
@ -85,7 +84,9 @@ export interface Explorer {
|
||||
armEndpoint: ko.Observable<string>;
|
||||
isFeatureEnabled: (feature: string) => boolean;
|
||||
isGalleryEnabled: ko.Computed<boolean>;
|
||||
isGalleryPublishEnabled: ko.Computed<boolean>;
|
||||
isGitHubPaneEnabled: ko.Observable<boolean>;
|
||||
isPublishNotebookPaneEnabled: ko.Observable<boolean>;
|
||||
isRightPanelV2Enabled: ko.Computed<boolean>;
|
||||
canExceedMaximumValue: ko.Computed<boolean>;
|
||||
hasAutoPilotV2FeatureFlag: ko.Computed<boolean>;
|
||||
@ -153,6 +154,7 @@ export interface Explorer {
|
||||
libraryManagePane: ContextualPane;
|
||||
clusterLibraryPane: ContextualPane;
|
||||
gitHubReposPane: ContextualPane;
|
||||
publishNotebookPaneAdapter: ReactAdapter;
|
||||
|
||||
// Facade
|
||||
logConsoleData(data: ConsoleData): void;
|
||||
@ -224,22 +226,17 @@ export interface Explorer {
|
||||
arcadiaWorkspaces: ko.ObservableArray<ArcadiaWorkspaceItem>;
|
||||
isNotebookTabActive: ko.Computed<boolean>;
|
||||
memoryUsageInfo: ko.Observable<DataModels.MemoryUsageInfo>;
|
||||
notebookManager?: any; // This is dynamically loaded
|
||||
openNotebook(notebookContentItem: NotebookContentItem): Promise<boolean>; // True if it was opened, false otherwise
|
||||
resetNotebookWorkspace(): void;
|
||||
importAndOpen: (path: string) => Promise<boolean>;
|
||||
importAndOpenFromGallery: (path: string, newName: string, content: any) => Promise<boolean>;
|
||||
importAndOpenFromGallery: (name: string, content: string) => Promise<boolean>;
|
||||
publishNotebook: (name: string, content: string) => void;
|
||||
openNotebookTerminal: (kind: TerminalKind) => void;
|
||||
openGallery: () => void;
|
||||
openNotebookViewer: (
|
||||
notebookUrl: string,
|
||||
notebookMetadata: DataModels.NotebookMetadata,
|
||||
onNotebookMetadataChange: (newNotebookMetadata: DataModels.NotebookMetadata) => Promise<void>,
|
||||
isLikedNotebook: boolean
|
||||
) => void;
|
||||
openGallery: (notebookUrl?: string, galleryItem?: IGalleryItem, isFavorite?: boolean) => void;
|
||||
openNotebookViewer: (notebookUrl: string) => void;
|
||||
notebookWorkspaceManager: NotebookWorkspaceManager;
|
||||
sparkClusterManager: SparkClusterManager;
|
||||
notebookContentProvider: IContentProvider;
|
||||
gitHubOAuthService: GitHubOAuthService;
|
||||
mostRecentActivity: MostRecentActivity;
|
||||
initNotebooks: (databaseAccount: DataModels.DatabaseAccount) => Promise<void>;
|
||||
deleteCluster(): void;
|
||||
@ -594,6 +591,16 @@ export interface GitHubReposPaneOptions extends PaneOptions {
|
||||
junoClient: JunoClient;
|
||||
}
|
||||
|
||||
export interface PublishNotebookPaneOptions extends PaneOptions {
|
||||
junoClient: JunoClient;
|
||||
}
|
||||
|
||||
export interface PublishNotebookPaneOpenOptions {
|
||||
name: string;
|
||||
author: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface AddCollectionPaneOptions extends PaneOptions {
|
||||
isPreferredApiTable: ko.Computed<boolean>;
|
||||
databaseId?: string;
|
||||
@ -873,16 +880,16 @@ export interface TerminalTabOptions extends TabOptions {
|
||||
export interface GalleryTabOptions extends TabOptions {
|
||||
account: DatabaseAccount;
|
||||
container: Explorer;
|
||||
junoClient: JunoClient;
|
||||
notebookUrl?: string;
|
||||
galleryItem?: IGalleryItem;
|
||||
isFavorite?: boolean;
|
||||
}
|
||||
|
||||
export interface NotebookViewerTabOptions extends TabOptions {
|
||||
account: DatabaseAccount;
|
||||
container: Explorer;
|
||||
notebookUrl: string;
|
||||
notebookName: string;
|
||||
notebookMetadata: DataModels.NotebookMetadata;
|
||||
onNotebookMetadataChange: (newNotebookMetadata: DataModels.NotebookMetadata) => Promise<void>;
|
||||
isLikedNotebook: boolean;
|
||||
}
|
||||
|
||||
export interface DocumentsTabOptions extends TabOptions {
|
||||
|
@ -15,15 +15,20 @@ import { ArcadiaMenuPickerProps } from "../Arcadia/ArcadiaMenuPicker";
|
||||
* Options for this component
|
||||
*/
|
||||
export interface CommandButtonComponentProps {
|
||||
/**
|
||||
* font icon name for the button
|
||||
*/
|
||||
iconName?: string;
|
||||
|
||||
/**
|
||||
* image source for the button icon
|
||||
*/
|
||||
iconSrc: string;
|
||||
iconSrc?: string;
|
||||
|
||||
/**
|
||||
* image alt for accessibility
|
||||
*/
|
||||
iconAlt: string;
|
||||
iconAlt?: string;
|
||||
|
||||
/**
|
||||
* Click handler for command button click
|
||||
|
@ -49,6 +49,7 @@ export const FeaturePanelComponent: React.FunctionComponent = () => {
|
||||
{ key: "feature.hosteddataexplorerenabled", label: "Hosted Data Explorer (deprecated?)", value: "true" },
|
||||
{ key: "feature.enablettl", label: "Enable TTL", value: "true" },
|
||||
{ key: "feature.enablegallery", label: "Enable Notebook Gallery", value: "true" },
|
||||
{ key: "feature.enablegallerypublish", label: "Enable Notebook Gallery Publishing", value: "true" },
|
||||
{ key: "feature.canexceedmaximumvalue", label: "Can exceed max value", value: "true" },
|
||||
{
|
||||
key: "feature.enablefixedcollectionwithsharedthroughput",
|
||||
|
@ -163,8 +163,8 @@ exports[`Feature panel renders all flags 1`] = `
|
||||
/>
|
||||
<StyledCheckboxBase
|
||||
checked={false}
|
||||
key="feature.canexceedmaximumvalue"
|
||||
label="Can exceed max value"
|
||||
key="feature.enablegallerypublish"
|
||||
label="Enable Notebook Gallery Publishing"
|
||||
onChange={[Function]}
|
||||
/>
|
||||
</Stack>
|
||||
@ -172,6 +172,12 @@ exports[`Feature panel renders all flags 1`] = `
|
||||
className="checkboxRow"
|
||||
horizontalAlign="space-between"
|
||||
>
|
||||
<StyledCheckboxBase
|
||||
checked={false}
|
||||
key="feature.canexceedmaximumvalue"
|
||||
label="Can exceed max value"
|
||||
onChange={[Function]}
|
||||
/>
|
||||
<StyledCheckboxBase
|
||||
checked={false}
|
||||
key="feature.enablefixedcollectionwithsharedthroughput"
|
||||
|
@ -1,15 +1,32 @@
|
||||
import React from "react";
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
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: () => {}
|
||||
data: {
|
||||
id: "id",
|
||||
name: "name",
|
||||
description: "description",
|
||||
author: "author",
|
||||
thumbnailUrl: "thumbnailUrl",
|
||||
created: "created",
|
||||
gitSha: "gitSha",
|
||||
tags: ["tag"],
|
||||
isSample: false,
|
||||
downloads: 0,
|
||||
favorites: 0,
|
||||
views: 0
|
||||
},
|
||||
isFavorite: false,
|
||||
showDelete: true,
|
||||
onClick: undefined,
|
||||
onTagClick: undefined,
|
||||
onFavoriteClick: undefined,
|
||||
onUnfavoriteClick: undefined,
|
||||
onDownloadClick: undefined,
|
||||
onDeleteClick: undefined
|
||||
};
|
||||
|
||||
const wrapper = shallow(<GalleryCardComponent {...props} />);
|
||||
|
@ -1,65 +1,199 @@
|
||||
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 { Card, ICardTokens } from "@uifabric/react-cards";
|
||||
import {
|
||||
siteTextStyles,
|
||||
descriptionTextStyles,
|
||||
helpfulTextStyles,
|
||||
subtleHelpfulTextStyles,
|
||||
subtleIconStyles
|
||||
} from "./CardStyleConstants";
|
||||
FontWeights,
|
||||
Icon,
|
||||
IconButton,
|
||||
Image,
|
||||
ImageFit,
|
||||
Persona,
|
||||
Text,
|
||||
Link,
|
||||
BaseButton,
|
||||
Button,
|
||||
LinkBase,
|
||||
Separator,
|
||||
TooltipHost
|
||||
} from "office-ui-fabric-react";
|
||||
import * as React from "react";
|
||||
import { IGalleryItem } from "../../../../Juno/JunoClient";
|
||||
import { FileSystemUtil } from "../../../Notebook/FileSystemUtil";
|
||||
|
||||
export interface GalleryCardComponentProps {
|
||||
name: string;
|
||||
url: string;
|
||||
notebookMetadata: DataModels.NotebookMetadata;
|
||||
data: IGalleryItem;
|
||||
isFavorite: boolean;
|
||||
showDelete: boolean;
|
||||
onClick: () => void;
|
||||
onTagClick: (tag: string) => void;
|
||||
onFavoriteClick: () => void;
|
||||
onUnfavoriteClick: () => void;
|
||||
onDownloadClick: () => void;
|
||||
onDeleteClick: () => void;
|
||||
}
|
||||
|
||||
export class GalleryCardComponent extends React.Component<GalleryCardComponentProps> {
|
||||
private cardTokens: ICardTokens = { childrenMargin: 12 };
|
||||
private attendantsCardSectionTokens: ICardSectionTokens = { childrenGap: 6 };
|
||||
public static readonly CARD_HEIGHT = 384;
|
||||
public static readonly CARD_WIDTH = 256;
|
||||
|
||||
private static readonly cardImageHeight = 144;
|
||||
private static readonly cardDescriptionMaxChars = 88;
|
||||
private static readonly cardTokens: ICardTokens = {
|
||||
width: GalleryCardComponent.CARD_WIDTH,
|
||||
height: GalleryCardComponent.CARD_HEIGHT,
|
||||
childrenGap: 8,
|
||||
childrenMargin: 10
|
||||
};
|
||||
|
||||
public render(): JSX.Element {
|
||||
return this.props.notebookMetadata !== undefined ? (
|
||||
<Card aria-label="Notebook Card" onClick={this.props.onClick} tokens={this.cardTokens}>
|
||||
const options: Intl.DateTimeFormatOptions = {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric"
|
||||
};
|
||||
|
||||
const dateString = new Date(this.props.data.created).toLocaleString("default", options);
|
||||
|
||||
return (
|
||||
<Card aria-label="Notebook Card" tokens={GalleryCardComponent.cardTokens} onClick={this.props.onClick}>
|
||||
<Card.Item>
|
||||
<Persona text={this.props.notebookMetadata.author} secondaryText={this.props.notebookMetadata.date} />
|
||||
<Persona text={this.props.data.author} secondaryText={dateString} />
|
||||
</Card.Item>
|
||||
|
||||
<Card.Item fill>
|
||||
<Image src={this.props.notebookMetadata.imageUrl} width="100%" alt="Notebook display image" />
|
||||
<Image
|
||||
src={
|
||||
this.props.data.thumbnailUrl ||
|
||||
`https://placehold.it/${GalleryCardComponent.CARD_WIDTH}x${GalleryCardComponent.cardImageHeight}`
|
||||
}
|
||||
width={GalleryCardComponent.CARD_WIDTH}
|
||||
height={GalleryCardComponent.cardImageHeight}
|
||||
imageFit={ImageFit.cover}
|
||||
alt="Notebook cover image"
|
||||
/>
|
||||
</Card.Item>
|
||||
|
||||
<Card.Section>
|
||||
<Text variant="small" styles={siteTextStyles}>
|
||||
{this.props.notebookMetadata.tags.join(", ")}
|
||||
<Text variant="small" nowrap>
|
||||
{this.props.data.tags?.map((tag, index, array) => (
|
||||
<span key={tag}>
|
||||
<Link onClick={(event): void => this.onTagClick(event, tag)}>{tag}</Link>
|
||||
{index === array.length - 1 ? <></> : ", "}
|
||||
</span>
|
||||
))}
|
||||
</Text>
|
||||
<Text styles={descriptionTextStyles}>{this.props.name}</Text>
|
||||
<Text variant="small" styles={helpfulTextStyles}>
|
||||
{this.props.notebookMetadata.description}
|
||||
<Text styles={{ root: { fontWeight: FontWeights.semibold } }} nowrap>
|
||||
{FileSystemUtil.stripExtension(this.props.data.name, "ipynb")}
|
||||
</Text>
|
||||
<Text variant="small" styles={{ root: { height: 36 } }}>
|
||||
{this.props.data.description.substr(0, GalleryCardComponent.cardDescriptionMaxChars)}
|
||||
</Text>
|
||||
</Card.Section>
|
||||
<Card.Section horizontal tokens={this.attendantsCardSectionTokens}>
|
||||
<Icon iconName="RedEye" styles={subtleIconStyles} />
|
||||
<Text variant="small" styles={subtleHelpfulTextStyles}>
|
||||
{this.props.notebookMetadata.views}
|
||||
|
||||
<Card.Section horizontal styles={{ root: { alignItems: "flex-end" } }}>
|
||||
<Text variant="tiny" styles={{ root: { color: "#ccc" } }}>
|
||||
<Icon iconName="RedEye" styles={{ root: { verticalAlign: "middle" } }} /> {this.props.data.views}
|
||||
</Text>
|
||||
<Icon iconName="Download" styles={subtleIconStyles} />
|
||||
<Text variant="small" styles={subtleHelpfulTextStyles}>
|
||||
{this.props.notebookMetadata.downloads}
|
||||
<Text variant="tiny" styles={{ root: { color: "#ccc" } }}>
|
||||
<Icon iconName="Download" styles={{ root: { verticalAlign: "middle" } }} /> {this.props.data.downloads}
|
||||
</Text>
|
||||
<Icon iconName="Heart" styles={subtleIconStyles} />
|
||||
<Text variant="small" styles={subtleHelpfulTextStyles}>
|
||||
{this.props.notebookMetadata.likes}
|
||||
<Text variant="tiny" styles={{ root: { color: "#ccc" } }}>
|
||||
<Icon iconName="Heart" styles={{ root: { verticalAlign: "middle" } }} /> {this.props.data.favorites}
|
||||
</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.Item>
|
||||
<Separator styles={{ root: { padding: 0, height: 1 } }} />
|
||||
</Card.Item>
|
||||
|
||||
<Card.Section horizontal styles={{ root: { marginTop: 0 } }}>
|
||||
{this.generateIconButtonWithTooltip(
|
||||
this.props.isFavorite ? "HeartFill" : "Heart",
|
||||
this.props.isFavorite ? "Unlike" : "Like",
|
||||
this.props.isFavorite ? this.onUnfavoriteClick : this.onFavoriteClick
|
||||
)}
|
||||
|
||||
{this.generateIconButtonWithTooltip("Download", "Download", this.onDownloadClick)}
|
||||
|
||||
{this.props.showDelete && (
|
||||
<div style={{ width: "100%", textAlign: "right" }}>
|
||||
{this.generateIconButtonWithTooltip("Delete", "Remove", this.props.onDeleteClick)}
|
||||
</div>
|
||||
)}
|
||||
</Card.Section>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
* Fluent UI doesn't support tooltips on IconButtons out of the box. In the meantime the recommendation is
|
||||
* to do the following (from https://developer.microsoft.com/en-us/fluentui#/controls/web/button)
|
||||
*/
|
||||
private generateIconButtonWithTooltip = (
|
||||
iconName: string,
|
||||
title: string,
|
||||
onClick: (
|
||||
event: React.MouseEvent<
|
||||
HTMLAnchorElement | HTMLButtonElement | HTMLDivElement | BaseButton | Button | HTMLSpanElement,
|
||||
MouseEvent
|
||||
>
|
||||
) => void
|
||||
): JSX.Element => {
|
||||
return (
|
||||
<TooltipHost
|
||||
content={title}
|
||||
id={`TooltipHost-IconButton-${iconName}`}
|
||||
calloutProps={{ gapSpace: 0 }}
|
||||
styles={{ root: { display: "inline-block" } }}
|
||||
>
|
||||
<IconButton iconProps={{ iconName }} title={title} ariaLabel={title} onClick={onClick} />
|
||||
</TooltipHost>
|
||||
);
|
||||
};
|
||||
|
||||
private onTagClick = (
|
||||
event: React.MouseEvent<HTMLElement | HTMLAnchorElement | HTMLButtonElement | LinkBase, MouseEvent>,
|
||||
tag: string
|
||||
): void => {
|
||||
event.stopPropagation();
|
||||
this.props.onTagClick(tag);
|
||||
};
|
||||
|
||||
private onFavoriteClick = (
|
||||
event: React.MouseEvent<
|
||||
HTMLAnchorElement | HTMLButtonElement | HTMLDivElement | BaseButton | Button | HTMLSpanElement,
|
||||
MouseEvent
|
||||
>
|
||||
): void => {
|
||||
event.stopPropagation();
|
||||
this.props.onFavoriteClick();
|
||||
};
|
||||
|
||||
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();
|
||||
};
|
||||
}
|
||||
|
@ -3,26 +3,263 @@
|
||||
exports[`GalleryCardComponent renders 1`] = `
|
||||
<Card
|
||||
aria-label="Notebook Card"
|
||||
onClick={[Function]}
|
||||
tokens={
|
||||
Object {
|
||||
"childrenMargin": 12,
|
||||
"childrenGap": 8,
|
||||
"childrenMargin": 10,
|
||||
"height": 384,
|
||||
"width": 256,
|
||||
}
|
||||
}
|
||||
>
|
||||
<CardItem>
|
||||
<StyledPersonaBase
|
||||
secondaryText="Invalid Date"
|
||||
text="author"
|
||||
/>
|
||||
</CardItem>
|
||||
<CardItem
|
||||
fill={true}
|
||||
>
|
||||
<StyledImageBase
|
||||
alt="Notebook cover image"
|
||||
height={144}
|
||||
imageFit={2}
|
||||
src="thumbnailUrl"
|
||||
width={256}
|
||||
/>
|
||||
</CardItem>
|
||||
<CardSection>
|
||||
<Text
|
||||
nowrap={true}
|
||||
variant="small"
|
||||
>
|
||||
<span
|
||||
key="tag"
|
||||
>
|
||||
<StyledLinkBase
|
||||
onClick={[Function]}
|
||||
>
|
||||
tag
|
||||
</StyledLinkBase>
|
||||
</span>
|
||||
</Text>
|
||||
<Text
|
||||
nowrap={true}
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "#333333",
|
||||
"fontWeight": 600,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
mycard
|
||||
name
|
||||
</Text>
|
||||
<Text
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"height": 36,
|
||||
},
|
||||
}
|
||||
}
|
||||
variant="small"
|
||||
>
|
||||
description
|
||||
</Text>
|
||||
</CardSection>
|
||||
<CardSection
|
||||
horizontal={true}
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"alignItems": "flex-end",
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "#ccc",
|
||||
},
|
||||
}
|
||||
}
|
||||
variant="tiny"
|
||||
>
|
||||
<StyledIconBase
|
||||
iconName="RedEye"
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"verticalAlign": "middle",
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
|
||||
0
|
||||
</Text>
|
||||
<Text
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "#ccc",
|
||||
},
|
||||
}
|
||||
}
|
||||
variant="tiny"
|
||||
>
|
||||
<StyledIconBase
|
||||
iconName="Download"
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"verticalAlign": "middle",
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
|
||||
0
|
||||
</Text>
|
||||
<Text
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "#ccc",
|
||||
},
|
||||
}
|
||||
}
|
||||
variant="tiny"
|
||||
>
|
||||
<StyledIconBase
|
||||
iconName="Heart"
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"verticalAlign": "middle",
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
|
||||
0
|
||||
</Text>
|
||||
</CardSection>
|
||||
<CardItem>
|
||||
<Styled
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"height": 1,
|
||||
"padding": 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
</CardItem>
|
||||
<CardSection
|
||||
horizontal={true}
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"marginTop": 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<StyledTooltipHostBase
|
||||
calloutProps={
|
||||
Object {
|
||||
"gapSpace": 0,
|
||||
}
|
||||
}
|
||||
content="Like"
|
||||
id="TooltipHost-IconButton-Heart"
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"display": "inline-block",
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<CustomizedIconButton
|
||||
ariaLabel="Like"
|
||||
iconProps={
|
||||
Object {
|
||||
"iconName": "Heart",
|
||||
}
|
||||
}
|
||||
onClick={[Function]}
|
||||
title="Like"
|
||||
/>
|
||||
</StyledTooltipHostBase>
|
||||
<StyledTooltipHostBase
|
||||
calloutProps={
|
||||
Object {
|
||||
"gapSpace": 0,
|
||||
}
|
||||
}
|
||||
content="Download"
|
||||
id="TooltipHost-IconButton-Download"
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"display": "inline-block",
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<CustomizedIconButton
|
||||
ariaLabel="Download"
|
||||
iconProps={
|
||||
Object {
|
||||
"iconName": "Download",
|
||||
}
|
||||
}
|
||||
onClick={[Function]}
|
||||
title="Download"
|
||||
/>
|
||||
</StyledTooltipHostBase>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"textAlign": "right",
|
||||
"width": "100%",
|
||||
}
|
||||
}
|
||||
>
|
||||
<StyledTooltipHostBase
|
||||
calloutProps={
|
||||
Object {
|
||||
"gapSpace": 0,
|
||||
}
|
||||
}
|
||||
content="Remove"
|
||||
id="TooltipHost-IconButton-Delete"
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"display": "inline-block",
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<CustomizedIconButton
|
||||
ariaLabel="Remove"
|
||||
iconProps={
|
||||
Object {
|
||||
"iconName": "Delete",
|
||||
}
|
||||
}
|
||||
title="Remove"
|
||||
/>
|
||||
</StyledTooltipHostBase>
|
||||
</div>
|
||||
</CardSection>
|
||||
</Card>
|
||||
`;
|
||||
|
@ -1,62 +1,17 @@
|
||||
import React from "react";
|
||||
import { shallow } from "enzyme";
|
||||
import {
|
||||
GalleryViewerContainerComponent,
|
||||
GalleryViewerContainerComponentProps,
|
||||
FullWidthTabs,
|
||||
FullWidthTabsProps,
|
||||
GalleryCardsComponent,
|
||||
GalleryCardsComponentProps,
|
||||
GalleryViewerComponent,
|
||||
GalleryViewerComponentProps
|
||||
} from "./GalleryViewerComponent";
|
||||
import React from "react";
|
||||
import { GalleryViewerComponent, GalleryViewerComponentProps, GalleryTab, SortBy } from "./GalleryViewerComponent";
|
||||
|
||||
describe("GalleryCardsComponent", () => {
|
||||
it("renders", () => {
|
||||
// TODO Mock this
|
||||
const props: GalleryCardsComponentProps = {
|
||||
data: [],
|
||||
userMetadata: undefined,
|
||||
onNotebookMetadataChange: () => Promise.resolve(),
|
||||
onClick: () => Promise.resolve()
|
||||
};
|
||||
|
||||
const wrapper = shallow(<GalleryCardsComponent {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("FullWidthTabs", () => {
|
||||
it("renders", () => {
|
||||
const props: FullWidthTabsProps = {
|
||||
officialSamplesContent: [],
|
||||
likedNotebooksContent: [],
|
||||
userMetadata: undefined,
|
||||
onClick: () => 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", () => {
|
||||
describe("GalleryViewerComponent", () => {
|
||||
it("renders", () => {
|
||||
const props: GalleryViewerComponentProps = {
|
||||
container: undefined,
|
||||
officialSamplesData: [],
|
||||
likedNotebookData: undefined
|
||||
junoClient: undefined,
|
||||
selectedTab: GalleryTab.OfficialSamples,
|
||||
sortBy: SortBy.MostViewed,
|
||||
searchText: undefined,
|
||||
onSelectedTabChange: undefined,
|
||||
onSortByChange: undefined,
|
||||
onSearchTextChange: undefined
|
||||
};
|
||||
|
||||
const wrapper = shallow(<GalleryViewerComponent {...props} />);
|
||||
|
@ -1,361 +1,513 @@
|
||||
/**
|
||||
* Gallery Viewer
|
||||
*/
|
||||
|
||||
import {
|
||||
Dropdown,
|
||||
FocusZone,
|
||||
IDropdownOption,
|
||||
IPageSpecification,
|
||||
IPivotItemProps,
|
||||
IPivotProps,
|
||||
IRectangle,
|
||||
Label,
|
||||
List,
|
||||
Pivot,
|
||||
PivotItem,
|
||||
SearchBox,
|
||||
Stack
|
||||
} from "office-ui-fabric-react";
|
||||
import * as React from "react";
|
||||
import * as DataModels from "../../../Contracts/DataModels";
|
||||
import * as Logger from "../../../Common/Logger";
|
||||
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 { IGalleryItem, JunoClient } from "../../../Juno/JunoClient";
|
||||
import * as GalleryUtils from "../../../Utils/GalleryUtils";
|
||||
import { NotificationConsoleUtils } from "../../../Utils/NotificationConsoleUtils";
|
||||
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import * as TabComponent from "../Tabs/TabComponent";
|
||||
|
||||
import { DialogComponent, DialogProps } from "../DialogReactComponent/DialogComponent";
|
||||
import { GalleryCardComponent, GalleryCardComponentProps } from "./Cards/GalleryCardComponent";
|
||||
import "./GalleryViewerComponent.less";
|
||||
import { HttpStatusCodes } from "../../../Common/Constants";
|
||||
|
||||
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 interface GalleryViewerComponentProps {
|
||||
container?: ViewModels.Explorer;
|
||||
junoClient: JunoClient;
|
||||
selectedTab: GalleryTab;
|
||||
sortBy: SortBy;
|
||||
searchText: string;
|
||||
onSelectedTabChange: (newTab: GalleryTab) => void;
|
||||
onSortByChange: (sortBy: SortBy) => void;
|
||||
onSearchTextChange: (searchText: string) => void;
|
||||
}
|
||||
|
||||
export class GalleryCardsComponent extends React.Component<GalleryCardsComponentProps> {
|
||||
private sectionStackTokens: IStackTokens = { childrenGap: 30 };
|
||||
export enum GalleryTab {
|
||||
OfficialSamples,
|
||||
PublicGallery,
|
||||
Favorites,
|
||||
Published
|
||||
}
|
||||
|
||||
export enum SortBy {
|
||||
MostViewed,
|
||||
MostDownloaded,
|
||||
MostFavorited,
|
||||
MostRecent
|
||||
}
|
||||
|
||||
interface GalleryViewerComponentState {
|
||||
sampleNotebooks: IGalleryItem[];
|
||||
publicNotebooks: IGalleryItem[];
|
||||
favoriteNotebooks: IGalleryItem[];
|
||||
publishedNotebooks: IGalleryItem[];
|
||||
selectedTab: GalleryTab;
|
||||
sortBy: SortBy;
|
||||
searchText: string;
|
||||
dialogProps: DialogProps;
|
||||
}
|
||||
|
||||
interface GalleryTabInfo {
|
||||
tab: GalleryTab;
|
||||
content: JSX.Element;
|
||||
}
|
||||
|
||||
export class GalleryViewerComponent extends React.Component<GalleryViewerComponentProps, GalleryViewerComponentState>
|
||||
implements GalleryUtils.DialogEnabledComponent {
|
||||
public static readonly OfficialSamplesTitle = "Official samples";
|
||||
public static readonly PublicGalleryTitle = "Public gallery";
|
||||
public static readonly FavoritesTitle = "Liked";
|
||||
public static readonly PublishedTitle = "Your published work";
|
||||
|
||||
private static readonly mostViewedText = "Most viewed";
|
||||
private static readonly mostDownloadedText = "Most downloaded";
|
||||
private static readonly mostFavoritedText = "Most favorited";
|
||||
private static readonly mostRecentText = "Most recent";
|
||||
|
||||
private static readonly sortingOptions: IDropdownOption[] = [
|
||||
{
|
||||
key: SortBy.MostViewed,
|
||||
text: GalleryViewerComponent.mostViewedText
|
||||
},
|
||||
{
|
||||
key: SortBy.MostDownloaded,
|
||||
text: GalleryViewerComponent.mostDownloadedText
|
||||
},
|
||||
{
|
||||
key: SortBy.MostFavorited,
|
||||
text: GalleryViewerComponent.mostFavoritedText
|
||||
},
|
||||
{
|
||||
key: SortBy.MostRecent,
|
||||
text: GalleryViewerComponent.mostRecentText
|
||||
}
|
||||
];
|
||||
|
||||
private sampleNotebooks: IGalleryItem[];
|
||||
private publicNotebooks: IGalleryItem[];
|
||||
private favoriteNotebooks: IGalleryItem[];
|
||||
private publishedNotebooks: IGalleryItem[];
|
||||
private columnCount: number;
|
||||
private rowCount: number;
|
||||
|
||||
constructor(props: GalleryViewerComponentProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
sampleNotebooks: undefined,
|
||||
publicNotebooks: undefined,
|
||||
favoriteNotebooks: undefined,
|
||||
publishedNotebooks: undefined,
|
||||
selectedTab: props.selectedTab,
|
||||
sortBy: props.sortBy,
|
||||
searchText: props.searchText,
|
||||
dialogProps: undefined
|
||||
};
|
||||
|
||||
this.loadTabContent(this.state.selectedTab, this.state.searchText, this.state.sortBy, false);
|
||||
if (this.props.container) {
|
||||
this.loadFavoriteNotebooks(this.state.searchText, this.state.sortBy, false); // Need this to show correct favorite button state
|
||||
}
|
||||
}
|
||||
|
||||
setDialogProps = (dialogProps: DialogProps): void => {
|
||||
this.setState({ dialogProps });
|
||||
};
|
||||
|
||||
public render(): JSX.Element {
|
||||
return (
|
||||
<Stack horizontal wrap tokens={this.sectionStackTokens}>
|
||||
{this.props.data.map((githubInfo: DataModels.GitHubInfoJunoResponse) => {
|
||||
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 tabs: GalleryTabInfo[] = [this.createTab(GalleryTab.OfficialSamples, this.state.sampleNotebooks)];
|
||||
|
||||
if (this.props.container) {
|
||||
if (this.props.container.isGalleryPublishEnabled()) {
|
||||
tabs.push(this.createTab(GalleryTab.PublicGallery, this.state.publicNotebooks));
|
||||
}
|
||||
|
||||
tabs.push(this.createTab(GalleryTab.Favorites, this.state.favoriteNotebooks));
|
||||
|
||||
if (this.props.container.isGalleryPublishEnabled()) {
|
||||
tabs.push(this.createTab(GalleryTab.Published, this.state.publishedNotebooks));
|
||||
}
|
||||
}
|
||||
|
||||
const pivotProps: IPivotProps = {
|
||||
onLinkClick: this.onPivotChange,
|
||||
selectedKey: GalleryTab[this.state.selectedTab]
|
||||
};
|
||||
|
||||
const pivotItems = tabs.map(tab => {
|
||||
const pivotItemProps: IPivotItemProps = {
|
||||
itemKey: GalleryTab[tab.tab],
|
||||
style: { marginTop: 20 },
|
||||
headerText: GalleryUtils.getTabTitle(tab.tab)
|
||||
};
|
||||
const officialSamplesIndex = githubInfo.officialSamplesIndex;
|
||||
const isLikedNotebook = githubInfo.isLikedNotebook;
|
||||
const updateTabsStatePerNotebook = this.props.onNotebookMetadataChange
|
||||
? (notebookMetadata: DataModels.NotebookMetadata): Promise<void> =>
|
||||
this.props.onNotebookMetadataChange(officialSamplesIndex, notebookMetadata)
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
name !== ".gitignore" &&
|
||||
url && (
|
||||
<GalleryCardComponent
|
||||
key={url}
|
||||
name={name}
|
||||
url={url}
|
||||
notebookMetadata={notebookMetadata}
|
||||
onClick={(): Promise<void> =>
|
||||
this.props.onClick(url, notebookMetadata, updateTabsStatePerNotebook, isLikedNotebook)
|
||||
}
|
||||
/>
|
||||
)
|
||||
<PivotItem key={pivotItemProps.itemKey} {...pivotItemProps}>
|
||||
{tab.content}
|
||||
</PivotItem>
|
||||
);
|
||||
})}
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="galleryContainer">
|
||||
<Pivot {...pivotProps}>{pivotItems}</Pivot>
|
||||
|
||||
{this.state.dialogProps && <DialogComponent {...this.state.dialogProps} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private createTab(tab: GalleryTab, data: IGalleryItem[]): GalleryTabInfo {
|
||||
return {
|
||||
tab,
|
||||
content: this.createTabContent(data)
|
||||
};
|
||||
}
|
||||
|
||||
private createTabContent(data: IGalleryItem[]): JSX.Element {
|
||||
return (
|
||||
<Stack tokens={{ childrenGap: 20 }}>
|
||||
<Stack horizontal tokens={{ childrenGap: 20 }}>
|
||||
<Stack.Item grow>
|
||||
<SearchBox value={this.state.searchText} placeholder="Search" onChange={this.onSearchBoxChange} />
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Label>Sort by</Label>
|
||||
</Stack.Item>
|
||||
<Stack.Item styles={{ root: { minWidth: 200 } }}>
|
||||
<Dropdown
|
||||
options={GalleryViewerComponent.sortingOptions}
|
||||
selectedKey={this.state.sortBy}
|
||||
onChange={this.onDropdownChange}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
|
||||
{data && this.createCardsTabContent(data)}
|
||||
</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: (): JSX.Element => (
|
||||
<GalleryCardsComponent
|
||||
data={this.state.officialSamplesContent}
|
||||
onClick={this.props.onClick}
|
||||
userMetadata={this.state.userMetadata}
|
||||
onNotebookMetadataChange={this.updateTabsState}
|
||||
/>
|
||||
)
|
||||
},
|
||||
isVisible: (): boolean => true
|
||||
},
|
||||
{
|
||||
title: "Liked Notebooks",
|
||||
content: {
|
||||
className: "",
|
||||
render: (): JSX.Element => (
|
||||
<GalleryCardsComponent
|
||||
data={this.state.likedNotebooksContent}
|
||||
onClick={this.props.onClick}
|
||||
userMetadata={this.state.userMetadata}
|
||||
onNotebookMetadataChange={this.updateTabsState}
|
||||
/>
|
||||
)
|
||||
},
|
||||
isVisible: (): boolean => true
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
public updateTabsState = async (
|
||||
officialSamplesIndex: number,
|
||||
notebookMetadata: DataModels.NotebookMetadata
|
||||
): Promise<void> => {
|
||||
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 () => {
|
||||
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): void => this.setState({ activeTabIndex });
|
||||
|
||||
public render(): JSX.Element {
|
||||
private createCardsTabContent(data: IGalleryItem[]): JSX.Element {
|
||||
return (
|
||||
<TabComponent.TabComponent
|
||||
tabs={this.appTabs}
|
||||
onTabIndexChange={this.onTabIndexChange.bind(this)}
|
||||
currentTabIndex={this.state.activeTabIndex}
|
||||
hideHeader={false}
|
||||
<FocusZone>
|
||||
<List
|
||||
items={data}
|
||||
getPageSpecification={this.getPageSpecification}
|
||||
renderedWindowsAhead={3}
|
||||
onRenderCell={this.onRenderCell}
|
||||
/>
|
||||
</FocusZone>
|
||||
);
|
||||
}
|
||||
|
||||
private loadTabContent(tab: GalleryTab, searchText: string, sortBy: SortBy, offline: boolean): void {
|
||||
switch (tab) {
|
||||
case GalleryTab.OfficialSamples:
|
||||
this.loadSampleNotebooks(searchText, sortBy, offline);
|
||||
break;
|
||||
|
||||
case GalleryTab.PublicGallery:
|
||||
this.loadPublicNotebooks(searchText, sortBy, offline);
|
||||
break;
|
||||
|
||||
case GalleryTab.Favorites:
|
||||
this.loadFavoriteNotebooks(searchText, sortBy, offline);
|
||||
break;
|
||||
|
||||
case GalleryTab.Published:
|
||||
this.loadPublishedNotebooks(searchText, sortBy, offline);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown tab ${tab}`);
|
||||
}
|
||||
}
|
||||
|
||||
export interface GalleryViewerContainerComponentProps {
|
||||
container: ViewModels.Explorer;
|
||||
private async loadSampleNotebooks(searchText: string, sortBy: SortBy, offline: boolean): Promise<void> {
|
||||
if (!offline) {
|
||||
try {
|
||||
const response = await this.props.junoClient.getSampleNotebooks();
|
||||
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
|
||||
throw new Error(`Received HTTP ${response.status} when loading sample notebooks`);
|
||||
}
|
||||
|
||||
interface GalleryViewerContainerComponentState {
|
||||
officialSamplesData: DataModels.GitHubInfoJunoResponse[];
|
||||
likedNotebooksData: DataModels.LikedNotebooksJunoResponse;
|
||||
this.sampleNotebooks = response.data;
|
||||
} catch (error) {
|
||||
const message = `Failed to load sample notebooks: ${error}`;
|
||||
Logger.logError(message, "GalleryViewerComponent/loadSampleNotebooks");
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message);
|
||||
}
|
||||
|
||||
export class GalleryViewerContainerComponent extends React.Component<
|
||||
GalleryViewerContainerComponentProps,
|
||||
GalleryViewerContainerComponentState
|
||||
> {
|
||||
constructor(props: GalleryViewerContainerComponentProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
officialSamplesData: undefined,
|
||||
likedNotebooksData: undefined
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
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
|
||||
sampleNotebooks: this.sampleNotebooks && [...this.sort(sortBy, this.search(searchText, this.sampleNotebooks))]
|
||||
});
|
||||
},
|
||||
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}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
private async loadPublicNotebooks(searchText: string, sortBy: SortBy, offline: boolean): Promise<void> {
|
||||
if (!offline) {
|
||||
try {
|
||||
const response = await this.props.junoClient.getPublicNotebooks();
|
||||
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
|
||||
throw new Error(`Received HTTP ${response.status} when loading public notebooks`);
|
||||
}
|
||||
|
||||
this.publicNotebooks = response.data;
|
||||
} catch (error) {
|
||||
const message = `Failed to load public notebooks: ${error}`;
|
||||
Logger.logError(message, "GalleryViewerComponent/loadPublicNotebooks");
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message);
|
||||
}
|
||||
}
|
||||
|
||||
export interface GalleryViewerComponentProps {
|
||||
container: ViewModels.Explorer;
|
||||
officialSamplesData: DataModels.GitHubInfoJunoResponse[];
|
||||
likedNotebookData: DataModels.LikedNotebooksJunoResponse;
|
||||
this.setState({
|
||||
publicNotebooks: this.publicNotebooks && [...this.sort(sortBy, this.search(searchText, this.publicNotebooks))]
|
||||
});
|
||||
}
|
||||
|
||||
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}
|
||||
/>
|
||||
private async loadFavoriteNotebooks(searchText: string, sortBy: SortBy, offline: boolean): Promise<void> {
|
||||
if (!offline) {
|
||||
try {
|
||||
const response = await this.props.junoClient.getFavoriteNotebooks();
|
||||
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
|
||||
throw new Error(`Received HTTP ${response.status} when loading favorite notebooks`);
|
||||
}
|
||||
|
||||
this.favoriteNotebooks = response.data;
|
||||
} catch (error) {
|
||||
const message = `Failed to load favorite notebooks: ${error}`;
|
||||
Logger.logError(message, "GalleryViewerComponent/loadFavoriteNotebooks");
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message);
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
favoriteNotebooks: this.favoriteNotebooks && [
|
||||
...this.sort(sortBy, this.search(searchText, this.favoriteNotebooks))
|
||||
]
|
||||
});
|
||||
|
||||
// Refresh favorite button state
|
||||
if (this.state.selectedTab !== GalleryTab.Favorites) {
|
||||
this.refreshSelectedTab();
|
||||
}
|
||||
}
|
||||
|
||||
private async loadPublishedNotebooks(searchText: string, sortBy: SortBy, offline: boolean): Promise<void> {
|
||||
if (!offline) {
|
||||
try {
|
||||
const response = await this.props.junoClient.getPublishedNotebooks();
|
||||
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
|
||||
throw new Error(`Received HTTP ${response.status} when loading published notebooks`);
|
||||
}
|
||||
|
||||
this.publishedNotebooks = response.data;
|
||||
} catch (error) {
|
||||
const message = `Failed to load published notebooks: ${error}`;
|
||||
Logger.logError(message, "GalleryViewerComponent/loadPublishedNotebooks");
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message);
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
publishedNotebooks: this.publishedNotebooks && [
|
||||
...this.sort(sortBy, this.search(searchText, this.publishedNotebooks))
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
private search(searchText: string, data: IGalleryItem[]): IGalleryItem[] {
|
||||
if (searchText) {
|
||||
return data?.filter(item => this.isGalleryItemPresent(searchText, item));
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
private isGalleryItemPresent(searchText: string, item: IGalleryItem): boolean {
|
||||
const toSearch = searchText.trim().toUpperCase();
|
||||
const searchData: string[] = [
|
||||
item.author.toUpperCase(),
|
||||
item.description.toUpperCase(),
|
||||
item.name.toUpperCase(),
|
||||
...item.tags?.map(tag => tag.toUpperCase())
|
||||
];
|
||||
|
||||
for (const data of searchData) {
|
||||
if (data?.indexOf(toSearch) !== -1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private sort(sortBy: SortBy, data: IGalleryItem[]): IGalleryItem[] {
|
||||
return data?.sort((a, b) => {
|
||||
switch (sortBy) {
|
||||
case SortBy.MostViewed:
|
||||
return b.views - a.views;
|
||||
case SortBy.MostDownloaded:
|
||||
return b.downloads - a.downloads;
|
||||
case SortBy.MostFavorited:
|
||||
return b.favorites - a.favorites;
|
||||
case SortBy.MostRecent:
|
||||
return Date.parse(b.created) - Date.parse(a.created);
|
||||
default:
|
||||
throw new Error(`Unknown sorting condition ${sortBy}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private refreshSelectedTab(item?: IGalleryItem): void {
|
||||
if (item) {
|
||||
this.updateGalleryItem(item);
|
||||
}
|
||||
this.loadTabContent(this.state.selectedTab, this.state.searchText, this.state.sortBy, true);
|
||||
}
|
||||
|
||||
private updateGalleryItem(updatedItem: IGalleryItem): void {
|
||||
this.replaceGalleryItem(updatedItem, this.sampleNotebooks);
|
||||
this.replaceGalleryItem(updatedItem, this.publicNotebooks);
|
||||
this.replaceGalleryItem(updatedItem, this.favoriteNotebooks);
|
||||
this.replaceGalleryItem(updatedItem, this.publishedNotebooks);
|
||||
}
|
||||
|
||||
private replaceGalleryItem(item: IGalleryItem, items?: IGalleryItem[]): void {
|
||||
const index = items?.findIndex(value => value.id === item.id);
|
||||
if (index !== -1) {
|
||||
items?.splice(index, 1, item);
|
||||
}
|
||||
}
|
||||
|
||||
private getPageSpecification = (itemIndex?: number, visibleRect?: IRectangle): IPageSpecification => {
|
||||
this.columnCount = Math.floor(visibleRect.width / GalleryCardComponent.CARD_WIDTH);
|
||||
this.rowCount = Math.floor(visibleRect.height / GalleryCardComponent.CARD_HEIGHT);
|
||||
|
||||
return {
|
||||
height: visibleRect.height,
|
||||
itemCount: this.columnCount * this.rowCount
|
||||
};
|
||||
};
|
||||
|
||||
private onRenderCell = (data?: IGalleryItem): JSX.Element => {
|
||||
const isFavorite = this.favoriteNotebooks?.find(item => item.id === data.id) !== undefined;
|
||||
const props: GalleryCardComponentProps = {
|
||||
data,
|
||||
isFavorite,
|
||||
showDelete: this.state.selectedTab === GalleryTab.Published,
|
||||
onClick: () => this.openNotebook(data, isFavorite),
|
||||
onTagClick: this.loadTaggedItems,
|
||||
onFavoriteClick: () => this.favoriteItem(data),
|
||||
onUnfavoriteClick: () => this.unfavoriteItem(data),
|
||||
onDownloadClick: () => this.downloadItem(data),
|
||||
onDeleteClick: () => this.deleteItem(data)
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ float: "left", padding: 10 }}>
|
||||
<GalleryCardComponent {...props} />
|
||||
</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
|
||||
): Promise<void> => {
|
||||
if (!this.props.container) {
|
||||
SessionStorageUtility.setEntryString(
|
||||
StorageKey.NotebookMetadata,
|
||||
notebookMetadata ? JSON.stringify(notebookMetadata) : undefined
|
||||
);
|
||||
SessionStorageUtility.setEntryString(StorageKey.NotebookName, path.basename(url));
|
||||
window.open(`${config.hostedExplorerURL}notebookViewer.html?notebookurl=${url}`, "_blank");
|
||||
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 {
|
||||
this.props.container.openNotebookViewer(url, notebookMetadata, onNotebookMetadataChange, isLikedNotebook);
|
||||
const params = new URLSearchParams({
|
||||
[GalleryUtils.NotebookViewerParams.NotebookUrl]: this.props.junoClient.getNotebookContentUrl(data.id),
|
||||
[GalleryUtils.NotebookViewerParams.GalleryItemId]: data.id
|
||||
});
|
||||
|
||||
window.open(`/notebookViewer.html?${params.toString()}`);
|
||||
}
|
||||
};
|
||||
|
||||
private loadTaggedItems = (tag: string): void => {
|
||||
const searchText = tag;
|
||||
this.setState({
|
||||
searchText
|
||||
});
|
||||
|
||||
this.loadTabContent(this.state.selectedTab, searchText, this.state.sortBy, true);
|
||||
this.props.onSearchTextChange && this.props.onSearchTextChange(searchText);
|
||||
};
|
||||
|
||||
private favoriteItem = async (data: IGalleryItem): Promise<void> => {
|
||||
GalleryUtils.favoriteItem(this.props.container, this.props.junoClient, data, (item: IGalleryItem) => {
|
||||
if (this.favoriteNotebooks) {
|
||||
this.favoriteNotebooks.push(item);
|
||||
} else {
|
||||
this.favoriteNotebooks = [item];
|
||||
}
|
||||
this.refreshSelectedTab(item);
|
||||
});
|
||||
};
|
||||
|
||||
private unfavoriteItem = async (data: IGalleryItem): Promise<void> => {
|
||||
GalleryUtils.unfavoriteItem(this.props.container, this.props.junoClient, data, (item: IGalleryItem) => {
|
||||
this.favoriteNotebooks = this.favoriteNotebooks?.filter(value => value.id !== item.id);
|
||||
this.refreshSelectedTab(item);
|
||||
});
|
||||
};
|
||||
|
||||
private downloadItem = async (data: IGalleryItem): Promise<void> => {
|
||||
GalleryUtils.downloadItem(this, this.props.container, this.props.junoClient, data, item =>
|
||||
this.refreshSelectedTab(item)
|
||||
);
|
||||
};
|
||||
|
||||
private deleteItem = async (data: IGalleryItem): Promise<void> => {
|
||||
GalleryUtils.deleteItem(this.props.container, this.props.junoClient, data, item => {
|
||||
this.publishedNotebooks = this.publishedNotebooks.filter(notebook => item.id !== notebook.id);
|
||||
this.refreshSelectedTab(item);
|
||||
});
|
||||
};
|
||||
|
||||
private onPivotChange = (item: PivotItem): void => {
|
||||
const selectedTab = GalleryTab[item.props.itemKey as keyof typeof GalleryTab];
|
||||
const searchText: string = undefined;
|
||||
this.setState({
|
||||
selectedTab,
|
||||
searchText
|
||||
});
|
||||
|
||||
this.loadTabContent(selectedTab, searchText, this.state.sortBy, false);
|
||||
this.props.onSelectedTabChange && this.props.onSelectedTabChange(selectedTab);
|
||||
};
|
||||
|
||||
private onSearchBoxChange = (event?: React.ChangeEvent<HTMLInputElement>, newValue?: string): void => {
|
||||
const searchText = newValue;
|
||||
this.setState({
|
||||
searchText
|
||||
});
|
||||
|
||||
this.loadTabContent(this.state.selectedTab, searchText, this.state.sortBy, true);
|
||||
this.props.onSearchTextChange && this.props.onSearchTextChange(searchText);
|
||||
};
|
||||
|
||||
private onDropdownChange = (event: React.FormEvent<HTMLDivElement>, option?: IDropdownOption): void => {
|
||||
const sortBy = option.key as SortBy;
|
||||
this.setState({
|
||||
sortBy
|
||||
});
|
||||
|
||||
this.loadTabContent(this.state.selectedTab, this.state.searchText, sortBy, true);
|
||||
this.props.onSortByChange && this.props.onSortByChange(sortBy);
|
||||
};
|
||||
}
|
||||
|
@ -1,54 +1,88 @@
|
||||
// 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`] = `
|
||||
exports[`GalleryViewerComponent renders 1`] = `
|
||||
<div
|
||||
className="galleryContainer"
|
||||
>
|
||||
<GalleryCardsComponent
|
||||
data={Array []}
|
||||
onClick={[Function]}
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`GalleryCardsComponent renders 1`] = `
|
||||
<StyledPivotBase
|
||||
onLinkClick={[Function]}
|
||||
selectedKey="OfficialSamples"
|
||||
>
|
||||
<PivotItem
|
||||
headerText="Official samples"
|
||||
itemKey="OfficialSamples"
|
||||
key="OfficialSamples"
|
||||
style={
|
||||
Object {
|
||||
"marginTop": 20,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Stack
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 20,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 30,
|
||||
"childrenGap": 20,
|
||||
}
|
||||
}
|
||||
wrap={true}
|
||||
>
|
||||
<StackItem
|
||||
grow={true}
|
||||
>
|
||||
<StyledSearchBoxBase
|
||||
onChange={[Function]}
|
||||
placeholder="Search"
|
||||
/>
|
||||
</StackItem>
|
||||
<StackItem>
|
||||
<StyledLabelBase>
|
||||
Sort by
|
||||
</StyledLabelBase>
|
||||
</StackItem>
|
||||
<StackItem
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"minWidth": 200,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<StyledWithResponsiveMode
|
||||
onChange={[Function]}
|
||||
options={
|
||||
Array [
|
||||
Object {
|
||||
"key": 0,
|
||||
"text": "Most viewed",
|
||||
},
|
||||
Object {
|
||||
"key": 1,
|
||||
"text": "Most downloaded",
|
||||
},
|
||||
Object {
|
||||
"key": 2,
|
||||
"text": "Most favorited",
|
||||
},
|
||||
Object {
|
||||
"key": 3,
|
||||
"text": "Most recent",
|
||||
},
|
||||
]
|
||||
}
|
||||
selectedKey={0}
|
||||
/>
|
||||
</StackItem>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</PivotItem>
|
||||
</StyledPivotBase>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`GalleryViewerContainerComponent renders 1`] = `<Fragment />`;
|
||||
|
@ -1,16 +1,30 @@
|
||||
import React from "react";
|
||||
import { shallow } from "enzyme";
|
||||
import { NotebookMetadataComponentProps, NotebookMetadataComponent } from "./NotebookMetadataComponent";
|
||||
import React from "react";
|
||||
import { NotebookMetadataComponent, NotebookMetadataComponentProps } from "./NotebookMetadataComponent";
|
||||
|
||||
describe("NotebookMetadataComponent", () => {
|
||||
it("renders un-liked notebook", () => {
|
||||
const props: NotebookMetadataComponentProps = {
|
||||
notebookName: "My notebook",
|
||||
container: undefined,
|
||||
notebookMetadata: undefined,
|
||||
notebookContent: {},
|
||||
onNotebookMetadataChange: () => Promise.resolve(),
|
||||
isLikedNotebook: false
|
||||
data: {
|
||||
id: "id",
|
||||
name: "name",
|
||||
description: "description",
|
||||
author: "author",
|
||||
thumbnailUrl: "thumbnailUrl",
|
||||
created: "created",
|
||||
gitSha: "gitSha",
|
||||
tags: ["tag"],
|
||||
isSample: false,
|
||||
downloads: 0,
|
||||
favorites: 0,
|
||||
views: 0
|
||||
},
|
||||
isFavorite: false,
|
||||
downloadButtonText: "Download",
|
||||
onTagClick: undefined,
|
||||
onDownloadClick: undefined,
|
||||
onFavoriteClick: undefined,
|
||||
onUnfavoriteClick: undefined
|
||||
};
|
||||
|
||||
const wrapper = shallow(<NotebookMetadataComponent {...props} />);
|
||||
@ -19,12 +33,26 @@ describe("NotebookMetadataComponent", () => {
|
||||
|
||||
it("renders liked notebook", () => {
|
||||
const props: NotebookMetadataComponentProps = {
|
||||
notebookName: "My notebook",
|
||||
container: undefined,
|
||||
notebookMetadata: undefined,
|
||||
notebookContent: {},
|
||||
onNotebookMetadataChange: () => Promise.resolve(),
|
||||
isLikedNotebook: true
|
||||
data: {
|
||||
id: "id",
|
||||
name: "name",
|
||||
description: "description",
|
||||
author: "author",
|
||||
thumbnailUrl: "thumbnailUrl",
|
||||
created: "created",
|
||||
gitSha: "gitSha",
|
||||
tags: ["tag"],
|
||||
isSample: false,
|
||||
downloads: 0,
|
||||
favorites: 0,
|
||||
views: 0
|
||||
},
|
||||
isFavorite: true,
|
||||
downloadButtonText: "Download",
|
||||
onTagClick: undefined,
|
||||
onDownloadClick: undefined,
|
||||
onFavoriteClick: undefined,
|
||||
onUnfavoriteClick: undefined
|
||||
};
|
||||
|
||||
const wrapper = shallow(<NotebookMetadataComponent {...props} />);
|
||||
|
@ -1,189 +1,85 @@
|
||||
/**
|
||||
* Wrapper around Notebook metadata
|
||||
*/
|
||||
|
||||
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, IconButton } from "office-ui-fabric-react";
|
||||
import {
|
||||
siteTextStyles,
|
||||
subtleIconStyles,
|
||||
iconStyles,
|
||||
iconButtonStyles,
|
||||
mainHelpfulTextStyles,
|
||||
subtleHelpfulTextStyles,
|
||||
helpfulTextStyles
|
||||
} from "../NotebookGallery/Cards/CardStyleConstants";
|
||||
|
||||
FontWeights,
|
||||
Icon,
|
||||
IconButton,
|
||||
Link,
|
||||
Persona,
|
||||
PersonaSize,
|
||||
PrimaryButton,
|
||||
Stack,
|
||||
Text
|
||||
} from "office-ui-fabric-react";
|
||||
import * as React from "react";
|
||||
import { IGalleryItem } from "../../../Juno/JunoClient";
|
||||
import { FileSystemUtil } from "../../Notebook/FileSystemUtil";
|
||||
import "./NotebookViewerComponent.less";
|
||||
|
||||
initializeIcons();
|
||||
|
||||
export interface NotebookMetadataComponentProps {
|
||||
notebookName: string;
|
||||
container: ViewModels.Explorer;
|
||||
notebookMetadata: NotebookMetadata;
|
||||
notebookContent: any;
|
||||
onNotebookMetadataChange: (newNotebookMetadata: NotebookMetadata) => Promise<void>;
|
||||
isLikedNotebook: boolean;
|
||||
data: IGalleryItem;
|
||||
isFavorite: boolean;
|
||||
downloadButtonText: string;
|
||||
onTagClick: (tag: string) => void;
|
||||
onFavoriteClick: () => void;
|
||||
onUnfavoriteClick: () => void;
|
||||
onDownloadClick: () => void;
|
||||
}
|
||||
|
||||
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 });
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
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 onDownload = (): void => {
|
||||
const promptForNotebookName = () => {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
let newNotebookName = this.props.notebookName;
|
||||
this.props.container.showOkCancelTextFieldModalDialog(
|
||||
"Save notebook as",
|
||||
undefined,
|
||||
"Ok",
|
||||
() => resolve(newNotebookName),
|
||||
"Cancel",
|
||||
() => reject(new Error("New notebook name dialog canceled")),
|
||||
{
|
||||
label: "New notebook name:",
|
||||
autoAdjustHeight: true,
|
||||
multiline: true,
|
||||
rows: 3,
|
||||
defaultValue: this.props.notebookName,
|
||||
onChange: (_, newValue: string) => {
|
||||
newNotebookName = newValue;
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
promptForNotebookName().then((newNotebookName: string) => {
|
||||
this.onDownloadClick(newNotebookName);
|
||||
});
|
||||
};
|
||||
|
||||
export class NotebookMetadataComponent extends React.Component<NotebookMetadataComponentProps> {
|
||||
public render(): JSX.Element {
|
||||
const options: Intl.DateTimeFormatOptions = {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric"
|
||||
};
|
||||
|
||||
const dateString = new Date(this.props.data.created).toLocaleString("default", options);
|
||||
|
||||
return (
|
||||
<div className="notebookViewerMetadataContainer">
|
||||
<h3 className="title">{this.props.notebookName}</h3>
|
||||
|
||||
{this.props.notebookMetadata && (
|
||||
<div className="decoration">
|
||||
{this.props.container ? (
|
||||
<Stack tokens={{ childrenGap: 10 }}>
|
||||
<Stack horizontal verticalAlign="center" tokens={{ childrenGap: 30 }}>
|
||||
<Text variant="xxLarge" nowrap>
|
||||
{FileSystemUtil.stripExtension(this.props.data.name, "ipynb")}
|
||||
</Text>
|
||||
<Text>
|
||||
<IconButton
|
||||
iconProps={{ iconName: this.state.liked ? "HeartFill" : "Heart" }}
|
||||
styles={iconButtonStyles}
|
||||
onClick={this.onLike}
|
||||
iconProps={{ iconName: this.props.isFavorite ? "HeartFill" : "Heart" }}
|
||||
onClick={this.props.isFavorite ? this.props.onUnfavoriteClick : this.props.onFavoriteClick}
|
||||
/>
|
||||
) : (
|
||||
<Icon iconName="Heart" styles={iconStyles} />
|
||||
)}
|
||||
<Text variant="large" styles={mainHelpfulTextStyles}>
|
||||
{this.state.notebookMetadata.likes} likes
|
||||
{this.props.data.favorites} likes
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
<PrimaryButton text={this.props.downloadButtonText} onClick={this.props.onDownloadClick} />
|
||||
</Stack>
|
||||
|
||||
{this.props.container && (
|
||||
<button aria-label="downloadButton" className="downloadButton" onClick={this.onDownload}>
|
||||
Download Notebook
|
||||
</button>
|
||||
)}
|
||||
<Stack horizontal verticalAlign="center" tokens={{ childrenGap: 10 }}>
|
||||
<Persona text={this.props.data.author} size={PersonaSize.size32} />
|
||||
<Text>{dateString}</Text>
|
||||
<Text>
|
||||
<Icon iconName="RedEye" /> {this.props.data.views}
|
||||
</Text>
|
||||
<Text>
|
||||
<Icon iconName="Download" />
|
||||
{this.props.data.downloads}
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
{this.props.notebookMetadata && (
|
||||
<>
|
||||
<div>
|
||||
<Persona
|
||||
className="persona"
|
||||
text={this.props.notebookMetadata.author}
|
||||
secondaryText={this.props.notebookMetadata.date}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="extras">
|
||||
<Icon iconName="RedEye" styles={subtleIconStyles} />
|
||||
<Text variant="small" styles={subtleHelpfulTextStyles}>
|
||||
{this.state.notebookMetadata.views}
|
||||
<Text nowrap>
|
||||
{this.props.data.tags?.map((tag, index, array) => (
|
||||
<span key={tag}>
|
||||
<Link onClick={(): void => this.props.onTagClick(tag)}>{tag}</Link>
|
||||
{index === array.length - 1 ? <></> : ", "}
|
||||
</span>
|
||||
))}
|
||||
</Text>
|
||||
<Icon iconName="Download" styles={subtleIconStyles} />
|
||||
<Text variant="small" styles={subtleHelpfulTextStyles}>
|
||||
{this.state.notebookMetadata.downloads}
|
||||
|
||||
<Text variant="large" styles={{ root: { fontWeight: FontWeights.semibold } }}>
|
||||
Description
|
||||
</Text>
|
||||
</div>
|
||||
<Text variant="small" styles={siteTextStyles}>
|
||||
{this.props.notebookMetadata.tags.join(", ")}
|
||||
</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text variant="small" styles={helpfulTextStyles}>
|
||||
<b>Description:</b>
|
||||
<p>{this.props.notebookMetadata.description}</p>
|
||||
</Text>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Text>{this.props.data.description}</Text>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
@import "../../../../less/Common/Constants";
|
||||
|
||||
.notebookViewerContainer {
|
||||
padding: @DefaultSpace;
|
||||
padding: 30px;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow-y: auto;
|
||||
|
@ -1,36 +1,44 @@
|
||||
/**
|
||||
* Wrapper around Notebook Viewer Read only content
|
||||
*/
|
||||
|
||||
import { Notebook } from "@nteract/commutable";
|
||||
import { createContentRef } from "@nteract/core";
|
||||
import { Icon, Link } from "office-ui-fabric-react";
|
||||
import * as React from "react";
|
||||
import { contents } from "rx-jupyter";
|
||||
import * as Logger from "../../../Common/Logger";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { IGalleryItem, JunoClient } from "../../../Juno/JunoClient";
|
||||
import * as GalleryUtils from "../../../Utils/GalleryUtils";
|
||||
import { NotificationConsoleUtils } from "../../../Utils/NotificationConsoleUtils";
|
||||
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import { NotebookClientV2 } from "../../Notebook/NotebookClientV2";
|
||||
import { NotebookComponentBootstrapper } from "../../Notebook/NotebookComponent/NotebookComponentBootstrapper";
|
||||
import { createContentRef } from "@nteract/core";
|
||||
import NotebookReadOnlyRenderer from "../../Notebook/NotebookRenderer/NotebookReadOnlyRenderer";
|
||||
import { contents } from "rx-jupyter";
|
||||
import { NotebookMetadata } from "../../../Contracts/DataModels";
|
||||
import { DialogComponent, DialogProps } from "../DialogReactComponent/DialogComponent";
|
||||
import { NotebookMetadataComponent } from "./NotebookMetadataComponent";
|
||||
import "./NotebookViewerComponent.less";
|
||||
|
||||
export interface NotebookViewerComponentProps {
|
||||
notebookName: string;
|
||||
notebookUrl: string;
|
||||
container?: ViewModels.Explorer;
|
||||
notebookMetadata: NotebookMetadata;
|
||||
onNotebookMetadataChange?: (newNotebookMetadata: NotebookMetadata) => Promise<void>;
|
||||
isLikedNotebook?: boolean;
|
||||
hideInputs?: boolean;
|
||||
junoClient?: JunoClient;
|
||||
notebookUrl: string;
|
||||
galleryItem?: IGalleryItem;
|
||||
isFavorite?: boolean;
|
||||
backNavigationText: string;
|
||||
onBackClick: () => void;
|
||||
onTagClick: (tag: string) => void;
|
||||
}
|
||||
|
||||
interface NotebookViewerComponentState {
|
||||
content: any;
|
||||
content: Notebook;
|
||||
galleryItem?: IGalleryItem;
|
||||
isFavorite?: boolean;
|
||||
dialogProps: DialogProps;
|
||||
}
|
||||
|
||||
export class NotebookViewerComponent extends React.Component<
|
||||
NotebookViewerComponentProps,
|
||||
NotebookViewerComponentState
|
||||
> {
|
||||
export class NotebookViewerComponent extends React.Component<NotebookViewerComponentProps, NotebookViewerComponentState>
|
||||
implements GalleryUtils.DialogEnabledComponent {
|
||||
private clientManager: NotebookClientV2;
|
||||
private notebookComponentBootstrapper: NotebookComponentBootstrapper;
|
||||
|
||||
@ -52,40 +60,118 @@ export class NotebookViewerComponent extends React.Component<
|
||||
contentRef: createContentRef()
|
||||
});
|
||||
|
||||
this.state = { content: undefined };
|
||||
this.state = {
|
||||
content: undefined,
|
||||
galleryItem: props.galleryItem,
|
||||
isFavorite: props.isFavorite,
|
||||
dialogProps: undefined
|
||||
};
|
||||
|
||||
this.loadNotebookContent();
|
||||
}
|
||||
|
||||
private async getJsonNotebookContent(): Promise<any> {
|
||||
const response: Response = await fetch(this.props.notebookUrl);
|
||||
if (response.ok) {
|
||||
return await response.json();
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
setDialogProps = (dialogProps: DialogProps): void => {
|
||||
this.setState({ dialogProps });
|
||||
};
|
||||
|
||||
private async loadNotebookContent(): Promise<void> {
|
||||
try {
|
||||
const response = await fetch(this.props.notebookUrl);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Received HTTP ${response.status} while fetching ${this.props.notebookUrl}`);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.getJsonNotebookContent().then((jsonContent: any) => {
|
||||
this.notebookComponentBootstrapper.setContent("json", jsonContent);
|
||||
this.setState({ content: jsonContent });
|
||||
});
|
||||
const notebook: Notebook = await response.json();
|
||||
this.notebookComponentBootstrapper.setContent("json", notebook);
|
||||
this.setState({ content: notebook });
|
||||
|
||||
if (this.props.galleryItem) {
|
||||
const response = await this.props.junoClient.increaseNotebookViews(this.props.galleryItem.id);
|
||||
if (!response.data) {
|
||||
throw new Error(`Received HTTP ${response.status} while increasing notebook views`);
|
||||
}
|
||||
|
||||
this.setState({ galleryItem: response.data });
|
||||
}
|
||||
} catch (error) {
|
||||
const message = `Failed to load notebook content: ${error}`;
|
||||
Logger.logError(message, "NotebookViewerComponent/loadNotebookContent");
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message);
|
||||
}
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
return (
|
||||
<div className="notebookViewerContainer">
|
||||
{this.props.backNavigationText ? (
|
||||
<Link onClick={this.props.onBackClick}>
|
||||
<Icon iconName="Back" /> {this.props.backNavigationText}
|
||||
</Link>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
|
||||
{this.state.galleryItem ? (
|
||||
<div style={{ margin: 10 }}>
|
||||
<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}
|
||||
data={this.state.galleryItem}
|
||||
isFavorite={this.state.isFavorite}
|
||||
downloadButtonText={
|
||||
this.props.container ? "Download to my notebooks" : "Edit/Run in Cosmos DB data explorer"
|
||||
}
|
||||
onTagClick={this.props.onTagClick}
|
||||
onFavoriteClick={this.favoriteItem}
|
||||
onUnfavoriteClick={this.unfavoriteItem}
|
||||
onDownloadClick={this.downloadItem}
|
||||
/>
|
||||
{this.notebookComponentBootstrapper.renderComponent(NotebookReadOnlyRenderer, {
|
||||
hideInputs: this.props.hideInputs
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
|
||||
{this.notebookComponentBootstrapper.renderComponent(NotebookReadOnlyRenderer, { hideInputs: true })}
|
||||
|
||||
{this.state.dialogProps && <DialogComponent {...this.state.dialogProps} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
public static getDerivedStateFromProps(
|
||||
props: NotebookViewerComponentProps,
|
||||
state: NotebookViewerComponentState
|
||||
): Partial<NotebookViewerComponentState> {
|
||||
let galleryItem = props.galleryItem;
|
||||
let isFavorite = props.isFavorite;
|
||||
|
||||
if (state.galleryItem !== undefined) {
|
||||
galleryItem = state.galleryItem;
|
||||
}
|
||||
|
||||
if (state.isFavorite !== undefined) {
|
||||
isFavorite = state.isFavorite;
|
||||
}
|
||||
|
||||
return {
|
||||
galleryItem,
|
||||
isFavorite
|
||||
};
|
||||
}
|
||||
|
||||
private favoriteItem = async (): Promise<void> => {
|
||||
GalleryUtils.favoriteItem(this.props.container, this.props.junoClient, this.state.galleryItem, item =>
|
||||
this.setState({ galleryItem: item, isFavorite: true })
|
||||
);
|
||||
};
|
||||
|
||||
private unfavoriteItem = async (): Promise<void> => {
|
||||
GalleryUtils.unfavoriteItem(this.props.container, this.props.junoClient, this.state.galleryItem, item =>
|
||||
this.setState({ galleryItem: item, isFavorite: false })
|
||||
);
|
||||
};
|
||||
|
||||
private downloadItem = async (): Promise<void> => {
|
||||
GalleryUtils.downloadItem(this, this.props.container, this.props.junoClient, this.state.galleryItem, item =>
|
||||
this.setState({ galleryItem: item })
|
||||
);
|
||||
};
|
||||
}
|
||||
|
@ -1,25 +1,199 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`NotebookMetadataComponent renders liked notebook 1`] = `
|
||||
<div
|
||||
className="notebookViewerMetadataContainer"
|
||||
<Stack
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<h3
|
||||
className="title"
|
||||
<Stack
|
||||
horizontal={true}
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 30,
|
||||
}
|
||||
}
|
||||
verticalAlign="center"
|
||||
>
|
||||
My notebook
|
||||
</h3>
|
||||
</div>
|
||||
<Text
|
||||
nowrap={true}
|
||||
variant="xxLarge"
|
||||
>
|
||||
name
|
||||
</Text>
|
||||
<Text>
|
||||
<CustomizedIconButton
|
||||
iconProps={
|
||||
Object {
|
||||
"iconName": "HeartFill",
|
||||
}
|
||||
}
|
||||
/>
|
||||
0
|
||||
likes
|
||||
</Text>
|
||||
<CustomizedPrimaryButton
|
||||
text="Download"
|
||||
/>
|
||||
</Stack>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 10,
|
||||
}
|
||||
}
|
||||
verticalAlign="center"
|
||||
>
|
||||
<StyledPersonaBase
|
||||
size={11}
|
||||
text="author"
|
||||
/>
|
||||
<Text>
|
||||
Invalid Date
|
||||
</Text>
|
||||
<Text>
|
||||
<StyledIconBase
|
||||
iconName="RedEye"
|
||||
/>
|
||||
|
||||
0
|
||||
</Text>
|
||||
<Text>
|
||||
<StyledIconBase
|
||||
iconName="Download"
|
||||
/>
|
||||
0
|
||||
</Text>
|
||||
</Stack>
|
||||
<Text
|
||||
nowrap={true}
|
||||
>
|
||||
<span
|
||||
key="tag"
|
||||
>
|
||||
<StyledLinkBase
|
||||
onClick={[Function]}
|
||||
>
|
||||
tag
|
||||
</StyledLinkBase>
|
||||
</span>
|
||||
</Text>
|
||||
<Text
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"fontWeight": 600,
|
||||
},
|
||||
}
|
||||
}
|
||||
variant="large"
|
||||
>
|
||||
Description
|
||||
</Text>
|
||||
<Text>
|
||||
description
|
||||
</Text>
|
||||
</Stack>
|
||||
`;
|
||||
|
||||
exports[`NotebookMetadataComponent renders un-liked notebook 1`] = `
|
||||
<div
|
||||
className="notebookViewerMetadataContainer"
|
||||
<Stack
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<h3
|
||||
className="title"
|
||||
<Stack
|
||||
horizontal={true}
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 30,
|
||||
}
|
||||
}
|
||||
verticalAlign="center"
|
||||
>
|
||||
My notebook
|
||||
</h3>
|
||||
</div>
|
||||
<Text
|
||||
nowrap={true}
|
||||
variant="xxLarge"
|
||||
>
|
||||
name
|
||||
</Text>
|
||||
<Text>
|
||||
<CustomizedIconButton
|
||||
iconProps={
|
||||
Object {
|
||||
"iconName": "Heart",
|
||||
}
|
||||
}
|
||||
/>
|
||||
0
|
||||
likes
|
||||
</Text>
|
||||
<CustomizedPrimaryButton
|
||||
text="Download"
|
||||
/>
|
||||
</Stack>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 10,
|
||||
}
|
||||
}
|
||||
verticalAlign="center"
|
||||
>
|
||||
<StyledPersonaBase
|
||||
size={11}
|
||||
text="author"
|
||||
/>
|
||||
<Text>
|
||||
Invalid Date
|
||||
</Text>
|
||||
<Text>
|
||||
<StyledIconBase
|
||||
iconName="RedEye"
|
||||
/>
|
||||
|
||||
0
|
||||
</Text>
|
||||
<Text>
|
||||
<StyledIconBase
|
||||
iconName="Download"
|
||||
/>
|
||||
0
|
||||
</Text>
|
||||
</Stack>
|
||||
<Text
|
||||
nowrap={true}
|
||||
>
|
||||
<span
|
||||
key="tag"
|
||||
>
|
||||
<StyledLinkBase
|
||||
onClick={[Function]}
|
||||
>
|
||||
tag
|
||||
</StyledLinkBase>
|
||||
</span>
|
||||
</Text>
|
||||
<Text
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"fontWeight": 600,
|
||||
},
|
||||
}
|
||||
}
|
||||
variant="large"
|
||||
>
|
||||
Description
|
||||
</Text>
|
||||
<Text>
|
||||
description
|
||||
</Text>
|
||||
</Stack>
|
||||
`;
|
||||
|
@ -49,19 +49,15 @@ import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane";
|
||||
import { ExplorerMetrics } from "../Common/Constants";
|
||||
import { ExplorerSettings } from "../Shared/ExplorerSettings";
|
||||
import { FileSystemUtil } from "./Notebook/FileSystemUtil";
|
||||
import { GitHubOAuthService } from "../GitHub/GitHubOAuthService";
|
||||
import { GitHubReposPane } from "./Panes/GitHubReposPane";
|
||||
import { handleOpenAction } from "./OpenActions";
|
||||
import { IContentProvider } from "@nteract/core";
|
||||
import { isInvalidParentFrameOrigin } from "../Utils/MessageValidation";
|
||||
import { JunoClient } from "../Juno/JunoClient";
|
||||
import { IGalleryItem } from "../Juno/JunoClient";
|
||||
import { LibraryManagePane } from "./Panes/LibraryManagePane";
|
||||
import { LoadQueryPane } from "./Panes/LoadQueryPane";
|
||||
import * as Logger from "../Common/Logger";
|
||||
import { ManageSparkClusterPane } from "./Panes/ManageSparkClusterPane";
|
||||
import { MessageHandler } from "../Common/MessageHandler";
|
||||
import { NotebookContentItem, NotebookContentItemType } from "./Notebook/NotebookContentItem";
|
||||
import { NotebookContentProvider } from "./Notebook/NotebookComponent/NotebookContentProvider";
|
||||
import { NotebookUtil } from "./Notebook/NotebookUtil";
|
||||
import { NotebookWorkspaceManager } from "../NotebookWorkspaceManager/NotebookWorkspaceManager";
|
||||
import { NotificationConsoleComponentAdapter } from "./Menus/NotificationConsole/NotificationConsoleComponentAdapter";
|
||||
@ -86,6 +82,7 @@ import { TableColumnOptionsPane } from "./Panes/Tables/TableColumnOptionsPane";
|
||||
import { UploadFilePane } from "./Panes/UploadFilePane";
|
||||
import { UploadItemsPane } from "./Panes/UploadItemsPane";
|
||||
import { UploadItemsPaneAdapter } from "./Panes/UploadItemsPaneAdapter";
|
||||
import { ReactAdapter } from "../Bindings/ReactBindingHandler";
|
||||
|
||||
BindingHandlersRegisterer.registerBindingHandlers();
|
||||
// Hold a reference to ComponentRegisterer to prevent transpiler to ignore import
|
||||
@ -199,10 +196,13 @@ export default class Explorer implements ViewModels.Explorer {
|
||||
public libraryManagePane: ViewModels.ContextualPane;
|
||||
public clusterLibraryPane: ViewModels.ContextualPane;
|
||||
public gitHubReposPane: ViewModels.ContextualPane;
|
||||
public publishNotebookPaneAdapter: ReactAdapter;
|
||||
|
||||
// features
|
||||
public isGalleryEnabled: ko.Computed<boolean>;
|
||||
public isGalleryPublishEnabled: ko.Computed<boolean>;
|
||||
public isGitHubPaneEnabled: ko.Observable<boolean>;
|
||||
public isPublishNotebookPaneEnabled: ko.Observable<boolean>;
|
||||
public isHostedDataExplorerEnabled: ko.Computed<boolean>;
|
||||
public isRightPanelV2Enabled: ko.Computed<boolean>;
|
||||
public canExceedMaximumValue: ko.Computed<boolean>;
|
||||
@ -223,11 +223,7 @@ export default class Explorer implements ViewModels.Explorer {
|
||||
// Notebooks
|
||||
public isNotebookEnabled: ko.Observable<boolean>;
|
||||
public isNotebooksEnabledForAccount: ko.Observable<boolean>;
|
||||
private notebookClient: ViewModels.INotebookContainerClient;
|
||||
private notebookContentClient: ViewModels.INotebookContentClient;
|
||||
public notebookServerInfo: ko.Observable<DataModels.NotebookWorkspaceConnectionInfo>;
|
||||
public notebookContentProvider: IContentProvider;
|
||||
public gitHubOAuthService: GitHubOAuthService;
|
||||
public notebookWorkspaceManager: ViewModels.NotebookWorkspaceManager;
|
||||
public sparkClusterManager: ViewModels.SparkClusterManager;
|
||||
public sparkClusterConnectionInfo: ko.Observable<DataModels.SparkClusterConnectionInfo>;
|
||||
@ -239,6 +235,7 @@ export default class Explorer implements ViewModels.Explorer {
|
||||
public isSynapseLinkUpdating: ko.Observable<boolean>;
|
||||
public isNotebookTabActive: ko.Computed<boolean>;
|
||||
public memoryUsageInfo: ko.Observable<DataModels.MemoryUsageInfo>;
|
||||
public notebookManager?: any; // This is dynamically loaded
|
||||
|
||||
private _panes: ViewModels.ContextualPane[] = [];
|
||||
private _importExplorerConfigComplete: boolean = false;
|
||||
@ -409,7 +406,11 @@ export default class Explorer implements ViewModels.Explorer {
|
||||
this.shouldShowDataAccessExpiryDialog = ko.observable<boolean>(false);
|
||||
this.shouldShowContextSwitchPrompt = ko.observable<boolean>(false);
|
||||
this.isGalleryEnabled = ko.computed<boolean>(() => this.isFeatureEnabled(Constants.Features.enableGallery));
|
||||
this.isGalleryPublishEnabled = ko.computed<boolean>(() =>
|
||||
this.isFeatureEnabled(Constants.Features.enableGalleryPublish)
|
||||
);
|
||||
this.isGitHubPaneEnabled = ko.observable<boolean>(false);
|
||||
this.isPublishNotebookPaneEnabled = ko.observable<boolean>(false);
|
||||
|
||||
this.canExceedMaximumValue = ko.computed<boolean>(() =>
|
||||
this.isFeatureEnabled(Constants.Features.canExceedMaximumValue)
|
||||
@ -937,127 +938,33 @@ export default class Explorer implements ViewModels.Explorer {
|
||||
startKey
|
||||
);
|
||||
|
||||
const junoClient = new JunoClient(this.databaseAccount);
|
||||
|
||||
this.isNotebookEnabled = ko.observable(false);
|
||||
this.isNotebookEnabled.subscribe(async (isEnabled: boolean) => {
|
||||
this.refreshCommandBarButtons();
|
||||
|
||||
this.gitHubOAuthService = new GitHubOAuthService(junoClient);
|
||||
|
||||
const GitHubClientModule = await import(/* webpackChunkName: "GitHubClient" */ "../GitHub/GitHubClient");
|
||||
const gitHubClient = new GitHubClientModule.GitHubClient(config.AZURESAMPLESCOSMOSDBPAT, error => {
|
||||
Logger.logError(error, "Explorer/GitHubClient errorCallback");
|
||||
|
||||
if (error.status === Constants.HttpStatusCodes.Unauthorized) {
|
||||
this.gitHubOAuthService?.resetToken();
|
||||
|
||||
this.showOkCancelModalDialog(
|
||||
undefined,
|
||||
"Cosmos DB cannot access your Github account anymore. Please connect to GitHub again.",
|
||||
"Connect to GitHub",
|
||||
() => this.gitHubReposPane?.open(),
|
||||
"Cancel",
|
||||
undefined
|
||||
this.isNotebookEnabled.subscribe(async () => {
|
||||
if (!this.notebookManager) {
|
||||
const notebookManagerModule = await import(
|
||||
/* webpackChunkName: "NotebookManager" */ "./Notebook/NotebookManager"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
this.gitHubReposPane = new GitHubReposPane({
|
||||
documentClientUtility: this.documentClientUtility,
|
||||
id: "gitHubReposPane",
|
||||
visible: ko.observable<boolean>(false),
|
||||
this.notebookManager = new notebookManagerModule.default();
|
||||
this.notebookManager.initialize({
|
||||
container: this,
|
||||
junoClient,
|
||||
gitHubClient
|
||||
dialogProps: this._dialogProps,
|
||||
notebookBasePath: this.notebookBasePath,
|
||||
resourceTree: this.resourceTree,
|
||||
refreshCommandBarButtons: () => this.refreshCommandBarButtons(),
|
||||
refreshNotebookList: () => this.refreshNotebookList()
|
||||
});
|
||||
|
||||
this.gitHubReposPane = this.notebookManager.gitHubReposPane;
|
||||
this.isGitHubPaneEnabled(true);
|
||||
|
||||
this.gitHubOAuthService.getTokenObservable().subscribe(token => {
|
||||
gitHubClient.setToken(token?.access_token ? token.access_token : config.AZURESAMPLESCOSMOSDBPAT);
|
||||
|
||||
if (this.gitHubReposPane?.visible()) {
|
||||
this.gitHubReposPane.open();
|
||||
}
|
||||
|
||||
this.refreshCommandBarButtons();
|
||||
this.refreshNotebookList();
|
||||
});
|
||||
|
||||
if (this.isGalleryEnabled()) {
|
||||
this.galleryTab = await import(/* webpackChunkName: "GalleryTab" */ "./Tabs/GalleryTab");
|
||||
this.notebookViewerTab = await import(/* webpackChunkName: "NotebookViewerTab" */ "./Tabs/NotebookViewerTab");
|
||||
}
|
||||
|
||||
const promptForCommitMsg = (title: string, primaryButtonLabel: string) => {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
let commitMsg: string = "Committed from Azure Cosmos DB Notebooks";
|
||||
this.showOkCancelTextFieldModalDialog(
|
||||
title || "Commit",
|
||||
undefined,
|
||||
primaryButtonLabel || "Commit",
|
||||
() => {
|
||||
TelemetryProcessor.trace(Action.NotebooksGitHubCommit, ActionModifiers.Mark, {
|
||||
databaseAccountName: this.databaseAccount() && this.databaseAccount().name,
|
||||
defaultExperience: this.defaultExperience && this.defaultExperience(),
|
||||
dataExplorerArea: Constants.Areas.Notebook
|
||||
});
|
||||
resolve(commitMsg);
|
||||
},
|
||||
"Cancel",
|
||||
() => reject(new Error("Commit dialog canceled")),
|
||||
{
|
||||
label: "Commit message",
|
||||
autoAdjustHeight: true,
|
||||
multiline: true,
|
||||
defaultValue: commitMsg,
|
||||
rows: 3,
|
||||
onChange: (_, newValue: string) => {
|
||||
commitMsg = newValue;
|
||||
this._dialogProps().primaryButtonDisabled = !commitMsg;
|
||||
this._dialogProps.valueHasMutated();
|
||||
}
|
||||
},
|
||||
!commitMsg
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const GitHubContentProviderModule = await import(
|
||||
/* webpackChunkName: "rx-jupyter" */ "../GitHub/GitHubContentProvider"
|
||||
);
|
||||
const RXJupyterModule = await import(/* webpackChunkName: "rx-jupyter" */ "rx-jupyter");
|
||||
this.notebookContentProvider = new NotebookContentProvider(
|
||||
new GitHubContentProviderModule.GitHubContentProvider({ gitHubClient, promptForCommitMsg }),
|
||||
RXJupyterModule.contents.JupyterContentProvider
|
||||
);
|
||||
|
||||
const NotebookContainerClientModule = await import(
|
||||
/* webpackChunkName: "NotebookContainerClient" */ "./Notebook/NotebookContainerClient"
|
||||
);
|
||||
|
||||
this.notebookClient = new NotebookContainerClientModule.NotebookContainerClient(
|
||||
this.notebookServerInfo,
|
||||
() => this.initNotebooks(this.databaseAccount()),
|
||||
(update: DataModels.MemoryUsageInfo) => this.memoryUsageInfo(update)
|
||||
);
|
||||
|
||||
const NotebookContentClientModule = await import(
|
||||
/* webpackChunkName: "NotebookContentClient" */ "./Notebook/NotebookContentClient"
|
||||
);
|
||||
this.notebookContentClient = new NotebookContentClientModule.NotebookContentClient(
|
||||
this.notebookServerInfo,
|
||||
this.notebookBasePath,
|
||||
this.notebookContentProvider
|
||||
);
|
||||
|
||||
this.refreshNotebookList();
|
||||
});
|
||||
|
||||
this.isSparkEnabled = ko.observable(false);
|
||||
this.isSparkEnabled.subscribe((isEnabled: boolean) => this.refreshCommandBarButtons());
|
||||
this.resourceTree = new ResourceTreeAdapter(this, junoClient);
|
||||
this.resourceTree = new ResourceTreeAdapter(this);
|
||||
this.resourceTreeForResourceToken = new ResourceTreeAdapterForResourceToken(this);
|
||||
this.notebookServerInfo = ko.observable<DataModels.NotebookWorkspaceConnectionInfo>({
|
||||
notebookServerEndpoint: undefined,
|
||||
@ -1887,7 +1794,7 @@ export default class Explorer implements ViewModels.Explorer {
|
||||
}
|
||||
|
||||
public resetNotebookWorkspace() {
|
||||
if (!this.isNotebookEnabled() || !this.notebookClient) {
|
||||
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookClient) {
|
||||
const error = "Attempt to reset notebook workspace, but notebook is not enabled";
|
||||
Logger.logError(error, "Explorer/resetNotebookWorkspace");
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error);
|
||||
@ -1994,7 +1901,7 @@ export default class Explorer implements ViewModels.Explorer {
|
||||
this._closeModalDialog();
|
||||
const id = NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.InProgress, "Resetting notebook workspace");
|
||||
try {
|
||||
await this.notebookClient.resetWorkspace();
|
||||
await this.notebookManager?.notebookClient.resetWorkspace();
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, "Successfully reset notebook workspace");
|
||||
TelemetryProcessor.traceSuccess(Action.ResetNotebookWorkspace);
|
||||
} catch (error) {
|
||||
@ -2563,17 +2470,17 @@ export default class Explorer implements ViewModels.Explorer {
|
||||
}
|
||||
|
||||
private uploadFile(name: string, content: string, parent: NotebookContentItem): Promise<NotebookContentItem> {
|
||||
if (!this.isNotebookEnabled() || !this.notebookContentClient) {
|
||||
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
|
||||
const error = "Attempt to upload notebook, but notebook is not enabled";
|
||||
Logger.logError(error, "Explorer/uploadFile");
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error);
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
const promise = this.notebookContentClient.uploadFileAsync(name, content, parent);
|
||||
const promise = this.notebookManager?.notebookContentClient.uploadFileAsync(name, content, parent);
|
||||
promise
|
||||
.then(() => this.resourceTree.triggerRender())
|
||||
.catch(reason => this.showOkModalDialog("Unable to upload file", reason));
|
||||
.catch((reason: any) => this.showOkModalDialog("Unable to upload file", reason));
|
||||
return promise;
|
||||
}
|
||||
|
||||
@ -2582,7 +2489,7 @@ export default class Explorer implements ViewModels.Explorer {
|
||||
const item = NotebookUtil.createNotebookContentItem(name, path, "file");
|
||||
const parent = this.resourceTree.myNotebooksContentRoot;
|
||||
|
||||
if (parent && parent.children && this.isNotebookEnabled() && this.notebookClient) {
|
||||
if (parent && parent.children && this.isNotebookEnabled() && this.notebookManager?.notebookClient) {
|
||||
if (this._filePathToImportAndOpen === path) {
|
||||
this._filePathToImportAndOpen = null; // we don't want to try opening this path again
|
||||
}
|
||||
@ -2601,15 +2508,10 @@ export default class Explorer implements ViewModels.Explorer {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
public async importAndOpenFromGallery(path: string, newName: string, content: any): Promise<boolean> {
|
||||
const name = newName;
|
||||
public async importAndOpenFromGallery(name: string, content: string): Promise<boolean> {
|
||||
const parent = this.resourceTree.myNotebooksContentRoot;
|
||||
|
||||
if (parent && this.isNotebookEnabled() && this.notebookClient) {
|
||||
if (this._filePathToImportAndOpen === path) {
|
||||
this._filePathToImportAndOpen = undefined; // we don't want to try opening this path again
|
||||
}
|
||||
|
||||
if (parent && parent.children && this.isNotebookEnabled() && this.notebookManager?.notebookClient) {
|
||||
const existingItem = _.find(parent.children, node => node.name === name);
|
||||
if (existingItem) {
|
||||
this.showOkModalDialog("Download failed", "Notebook with the same name already exists.");
|
||||
@ -2620,10 +2522,17 @@ export default class Explorer implements ViewModels.Explorer {
|
||||
return this.openNotebook(uploadedItem);
|
||||
}
|
||||
|
||||
this._filePathToImportAndOpen = path; // we'll try opening this path later on
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
public publishNotebook(name: string, content: string): void {
|
||||
if (this.notebookManager) {
|
||||
this.notebookManager.openPublishNotebookPane(name, content);
|
||||
this.publishNotebookPaneAdapter = this.notebookManager.publishNotebookPaneAdapter;
|
||||
this.isPublishNotebookPaneEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
public showOkModalDialog(title: string, msg: string): void {
|
||||
this._dialogProps({
|
||||
isModal: true,
|
||||
@ -2756,7 +2665,7 @@ export default class Explorer implements ViewModels.Explorer {
|
||||
}
|
||||
|
||||
public renameNotebook(notebookFile: NotebookContentItem): Q.Promise<NotebookContentItem> {
|
||||
if (!this.isNotebookEnabled() || !this.notebookContentClient) {
|
||||
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
|
||||
const error = "Attempt to rename notebook, but notebook is not enabled";
|
||||
Logger.logError(error, "Explorer/renameNotebook");
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error);
|
||||
@ -2784,7 +2693,7 @@ export default class Explorer implements ViewModels.Explorer {
|
||||
paneTitle: "Rename Notebook",
|
||||
submitButtonLabel: "Rename",
|
||||
defaultInput: FileSystemUtil.stripExtension(notebookFile.name, "ipynb"),
|
||||
onSubmit: (input: string) => this.notebookContentClient.renameNotebook(notebookFile, input)
|
||||
onSubmit: (input: string) => this.notebookManager?.notebookContentClient.renameNotebook(notebookFile, input)
|
||||
})
|
||||
.then(newNotebookFile => {
|
||||
this.openedTabs()
|
||||
@ -2806,7 +2715,7 @@ export default class Explorer implements ViewModels.Explorer {
|
||||
}
|
||||
|
||||
public onCreateDirectory(parent: NotebookContentItem): Q.Promise<NotebookContentItem> {
|
||||
if (!this.isNotebookEnabled() || !this.notebookContentClient) {
|
||||
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
|
||||
const error = "Attempt to create notebook directory, but notebook is not enabled";
|
||||
Logger.logError(error, "Explorer/onCreateDirectory");
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error);
|
||||
@ -2821,32 +2730,32 @@ export default class Explorer implements ViewModels.Explorer {
|
||||
paneTitle: "Create new directory",
|
||||
submitButtonLabel: "Create",
|
||||
defaultInput: "",
|
||||
onSubmit: (input: string) => this.notebookContentClient.createDirectory(parent, input)
|
||||
onSubmit: (input: string) => this.notebookManager?.notebookContentClient.createDirectory(parent, input)
|
||||
});
|
||||
result.then(() => this.resourceTree.triggerRender());
|
||||
return result;
|
||||
}
|
||||
|
||||
public readFile(notebookFile: NotebookContentItem): Promise<string> {
|
||||
if (!this.isNotebookEnabled() || !this.notebookContentClient) {
|
||||
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
|
||||
const error = "Attempt to read file, but notebook is not enabled";
|
||||
Logger.logError(error, "Explorer/downloadFile");
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error);
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
return this.notebookContentClient.readFileContent(notebookFile.path);
|
||||
return this.notebookManager?.notebookContentClient.readFileContent(notebookFile.path);
|
||||
}
|
||||
|
||||
public downloadFile(notebookFile: NotebookContentItem): Promise<void> {
|
||||
if (!this.isNotebookEnabled() || !this.notebookContentClient) {
|
||||
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
|
||||
const error = "Attempt to download file, but notebook is not enabled";
|
||||
Logger.logError(error, "Explorer/downloadFile");
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error);
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
return this.notebookContentClient.readFileContent(notebookFile.path).then(
|
||||
return this.notebookManager?.notebookContentClient.readFileContent(notebookFile.path).then(
|
||||
(content: string) => {
|
||||
const blob = new Blob([content], { type: "octet/stream" });
|
||||
if (navigator.msSaveBlob) {
|
||||
@ -2866,7 +2775,7 @@ export default class Explorer implements ViewModels.Explorer {
|
||||
downloadLink.remove();
|
||||
}
|
||||
},
|
||||
error => {
|
||||
(error: any) => {
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Could not download notebook ${JSON.stringify(error)}`
|
||||
@ -3013,7 +2922,7 @@ export default class Explorer implements ViewModels.Explorer {
|
||||
}
|
||||
|
||||
private refreshNotebookList = async (): Promise<void> => {
|
||||
if (!this.isNotebookEnabled() || !this.notebookContentClient) {
|
||||
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -3024,7 +2933,7 @@ export default class Explorer implements ViewModels.Explorer {
|
||||
};
|
||||
|
||||
public deleteNotebookFile(item: NotebookContentItem): Promise<void> {
|
||||
if (!this.isNotebookEnabled() || !this.notebookContentClient) {
|
||||
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
|
||||
const error = "Attempt to delete notebook file, but notebook is not enabled";
|
||||
Logger.logError(error, "Explorer/deleteNotebookFile");
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error);
|
||||
@ -3055,11 +2964,11 @@ export default class Explorer implements ViewModels.Explorer {
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
return this.notebookContentClient.deleteContentItem(item).then(
|
||||
return this.notebookManager?.notebookContentClient.deleteContentItem(item).then(
|
||||
() => {
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, `Successfully deleted: ${item.path}`);
|
||||
},
|
||||
reason => {
|
||||
(reason: any) => {
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Failed to delete "${item.path}": ${JSON.stringify(reason)}`
|
||||
@ -3072,7 +2981,7 @@ export default class Explorer implements ViewModels.Explorer {
|
||||
* This creates a new notebook file, then opens the notebook
|
||||
*/
|
||||
public onNewNotebookClicked(parent?: NotebookContentItem): void {
|
||||
if (!this.isNotebookEnabled() || !this.notebookContentClient) {
|
||||
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
|
||||
const error = "Attempt to create new notebook, but notebook is not enabled";
|
||||
Logger.logError(error, "Explorer/onNewNotebookClicked");
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error);
|
||||
@ -3092,7 +3001,7 @@ export default class Explorer implements ViewModels.Explorer {
|
||||
dataExplorerArea: Constants.Areas.Notebook
|
||||
});
|
||||
|
||||
this.notebookContentClient
|
||||
this.notebookManager?.notebookContentClient
|
||||
.createNewNotebookFile(parent)
|
||||
.then((newFile: NotebookContentItem) => {
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, `Successfully created: ${newFile.name}`);
|
||||
@ -3108,7 +3017,7 @@ export default class Explorer implements ViewModels.Explorer {
|
||||
return this.openNotebook(newFile);
|
||||
})
|
||||
.then(() => this.resourceTree.triggerRender())
|
||||
.catch(reason => {
|
||||
.catch((reason: any) => {
|
||||
const error = `Failed to create a new notebook: ${reason}`;
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error);
|
||||
TelemetryProcessor.traceFailure(
|
||||
@ -3158,14 +3067,14 @@ export default class Explorer implements ViewModels.Explorer {
|
||||
}
|
||||
|
||||
public refreshContentItem(item: NotebookContentItem): Promise<void> {
|
||||
if (!this.isNotebookEnabled() || !this.notebookContentClient) {
|
||||
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
|
||||
const error = "Attempt to refresh notebook list, but notebook is not enabled";
|
||||
Logger.logError(error, "Explorer/refreshContentItem");
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error);
|
||||
return Promise.reject(new Error(error));
|
||||
}
|
||||
|
||||
return this.notebookContentClient.updateItemChildren(item);
|
||||
return this.notebookManager?.notebookContentClient.updateItemChildren(item);
|
||||
}
|
||||
|
||||
public getNotebookBasePath(): string {
|
||||
@ -3234,7 +3143,7 @@ export default class Explorer implements ViewModels.Explorer {
|
||||
newTab.onTabClick();
|
||||
}
|
||||
|
||||
public openGallery() {
|
||||
public async openGallery(notebookUrl?: string, galleryItem?: IGalleryItem, isFavorite?: boolean) {
|
||||
let title: string;
|
||||
let hashLocation: string;
|
||||
|
||||
@ -3249,25 +3158,34 @@ export default class Explorer implements ViewModels.Explorer {
|
||||
if (openedTabs[i].hashLocation() == hashLocation) {
|
||||
openedTabs[i].onTabClick();
|
||||
openedTabs[i].onActivate();
|
||||
(openedTabs[i] as any).updateGalleryParams(notebookUrl, galleryItem, isFavorite);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.galleryTab) {
|
||||
this.galleryTab = await import(/* webpackChunkName: "GalleryTab" */ "./Tabs/GalleryTab");
|
||||
}
|
||||
|
||||
const newTab = new this.galleryTab.default({
|
||||
// GalleryTabOptions
|
||||
account: CosmosClient.databaseAccount(),
|
||||
container: this,
|
||||
junoClient: this.notebookManager?.junoClient,
|
||||
notebookUrl,
|
||||
galleryItem,
|
||||
isFavorite,
|
||||
// TabOptions
|
||||
tabKind: ViewModels.CollectionTabKind.Gallery,
|
||||
node: null,
|
||||
title: title,
|
||||
tabPath: title,
|
||||
documentClientUtility: null,
|
||||
collection: null,
|
||||
selfLink: null,
|
||||
hashLocation: hashLocation,
|
||||
isActive: ko.observable(false),
|
||||
hashLocation: hashLocation,
|
||||
onUpdateTabsButtons: this.onUpdateTabsButtons,
|
||||
isTabsContentExpanded: ko.observable(true),
|
||||
onLoadStartKey: null,
|
||||
onUpdateTabsButtons: this.onUpdateTabsButtons,
|
||||
container: this,
|
||||
openedTabs: this.openedTabs()
|
||||
});
|
||||
|
||||
@ -3277,16 +3195,14 @@ export default class Explorer implements ViewModels.Explorer {
|
||||
newTab.onTabClick();
|
||||
}
|
||||
|
||||
public openNotebookViewer(
|
||||
notebookUrl: string,
|
||||
notebookMetadata: DataModels.NotebookMetadata,
|
||||
onNotebookMetadataChange: (newNotebookMetadata: DataModels.NotebookMetadata) => Promise<void>,
|
||||
isLikedNotebook: boolean
|
||||
) {
|
||||
const notebookName = path.basename(notebookUrl);
|
||||
const title = notebookName;
|
||||
public async openNotebookViewer(notebookUrl: string) {
|
||||
const title = path.basename(notebookUrl);
|
||||
const hashLocation = notebookUrl;
|
||||
|
||||
if (!this.notebookViewerTab) {
|
||||
this.notebookViewerTab = await import(/* webpackChunkName: "NotebookViewerTab" */ "./Tabs/NotebookViewerTab");
|
||||
}
|
||||
|
||||
const notebookViewerTabModule = this.notebookViewerTab;
|
||||
|
||||
let isNotebookViewerOpen = (tab: ViewModels.Tab) => {
|
||||
@ -3322,11 +3238,7 @@ export default class Explorer implements ViewModels.Explorer {
|
||||
onUpdateTabsButtons: this.onUpdateTabsButtons,
|
||||
container: this,
|
||||
openedTabs: this.openedTabs(),
|
||||
notebookUrl: notebookUrl,
|
||||
notebookName: notebookName,
|
||||
notebookMetadata: notebookMetadata,
|
||||
onNotebookMetadataChange: onNotebookMetadataChange,
|
||||
isLikedNotebook: isLikedNotebook
|
||||
notebookUrl
|
||||
});
|
||||
|
||||
this.openedTabs.push(newTab);
|
||||
|
@ -3,6 +3,7 @@ import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { CommandBarComponentButtonFactory } from "./CommandBarComponentButtonFactory";
|
||||
import { ExplorerStub } from "../../OpenActionsStubs";
|
||||
import { GitHubOAuthService } from "../../../GitHub/GitHubOAuthService";
|
||||
import NotebookManager from "../../Notebook/NotebookManager";
|
||||
|
||||
describe("CommandBarComponentButtonFactory tests", () => {
|
||||
let mockExplorer: ViewModels.Explorer;
|
||||
@ -19,6 +20,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
mockExplorer.isPreferredApiCassandra = ko.computed<boolean>(() => false);
|
||||
mockExplorer.isSparkEnabled = ko.observable(true);
|
||||
mockExplorer.isGalleryEnabled = ko.computed<boolean>(() => false);
|
||||
mockExplorer.isGalleryPublishEnabled = ko.computed<boolean>(() => false);
|
||||
mockExplorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
|
||||
mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
|
||||
});
|
||||
@ -81,6 +83,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
mockExplorer.isPreferredApiCassandra = ko.computed<boolean>(() => false);
|
||||
mockExplorer.isSparkEnabled = ko.observable(true);
|
||||
mockExplorer.isGalleryEnabled = ko.computed<boolean>(() => false);
|
||||
mockExplorer.isGalleryPublishEnabled = ko.computed<boolean>(() => false);
|
||||
mockExplorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
|
||||
mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
|
||||
});
|
||||
@ -161,6 +164,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false);
|
||||
mockExplorer.isSparkEnabled = ko.observable(true);
|
||||
mockExplorer.isGalleryEnabled = ko.computed<boolean>(() => false);
|
||||
mockExplorer.isGalleryPublishEnabled = ko.computed<boolean>(() => false);
|
||||
mockExplorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
|
||||
mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
|
||||
});
|
||||
@ -247,7 +251,9 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
mockExplorer.isNotebooksEnabledForAccount = ko.observable(false);
|
||||
mockExplorer.isRunningOnNationalCloud = ko.observable(false);
|
||||
mockExplorer.isGalleryEnabled = ko.computed<boolean>(() => false);
|
||||
mockExplorer.gitHubOAuthService = new GitHubOAuthService(undefined);
|
||||
mockExplorer.isGalleryPublishEnabled = ko.computed<boolean>(() => false);
|
||||
mockExplorer.notebookManager = new NotebookManager();
|
||||
mockExplorer.notebookManager.gitHubOAuthService = new GitHubOAuthService(undefined);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
@ -268,7 +274,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
|
||||
it("Notebooks is enabled and GitHubOAuthService is logged in - manage github settings button should be visible", () => {
|
||||
mockExplorer.isNotebookEnabled = ko.observable(true);
|
||||
mockExplorer.gitHubOAuthService.isLoggedIn = jest.fn().mockReturnValue(true);
|
||||
mockExplorer.notebookManager.gitHubOAuthService.isLoggedIn = jest.fn().mockReturnValue(true);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
|
||||
const manageGitHubSettingsBtn = buttons.find(
|
||||
|
@ -26,7 +26,6 @@ import EnableNotebooksIcon from "../../../../images/notebook/Notebook-enable.svg
|
||||
import NewNotebookIcon from "../../../../images/notebook/Notebook-new.svg";
|
||||
import ResetWorkspaceIcon from "../../../../images/notebook/Notebook-reset-workspace.svg";
|
||||
import LibraryManageIcon from "../../../../images/notebook/Spark-library-manage.svg";
|
||||
import GalleryIcon from "../../../../images/GalleryIcon.svg";
|
||||
import GitHubIcon from "../../../../images/github.svg";
|
||||
import SynapseIcon from "../../../../images/synapse-link.svg";
|
||||
import { config, Platform } from "../../../Config";
|
||||
@ -64,7 +63,7 @@ export class CommandBarComponentButtonFactory {
|
||||
];
|
||||
buttons.push(newNotebookButton);
|
||||
|
||||
if (container.gitHubOAuthService) {
|
||||
if (container.notebookManager?.gitHubOAuthService) {
|
||||
buttons.push(CommandBarComponentButtonFactory.createManageGitHubAccountButton(container));
|
||||
}
|
||||
}
|
||||
@ -87,10 +86,6 @@ export class CommandBarComponentButtonFactory {
|
||||
buttons.push(CommandBarComponentButtonFactory.createOpenTerminalButton(container));
|
||||
|
||||
buttons.push(CommandBarComponentButtonFactory.createNotebookWorkspaceResetButton(container));
|
||||
|
||||
if (container.isGalleryEnabled()) {
|
||||
buttons.push(CommandBarComponentButtonFactory.createGalleryButton(container));
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Should be replaced with the create arcadia spark pool button
|
||||
@ -575,19 +570,6 @@ export class CommandBarComponentButtonFactory {
|
||||
};
|
||||
}
|
||||
|
||||
private static createGalleryButton(container: ViewModels.Explorer): ViewModels.NavbarButtonConfig {
|
||||
const label = "View Gallery";
|
||||
return {
|
||||
iconSrc: GalleryIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => container.openGallery(),
|
||||
commandButtonLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: false,
|
||||
ariaLabel: label
|
||||
};
|
||||
}
|
||||
|
||||
private static createOpenMongoTerminalButton(container: ViewModels.Explorer): ViewModels.NavbarButtonConfig {
|
||||
const label = "Open Mongo Shell";
|
||||
const tooltip =
|
||||
@ -654,7 +636,7 @@ export class CommandBarComponentButtonFactory {
|
||||
}
|
||||
|
||||
private static createManageGitHubAccountButton(container: ViewModels.Explorer): ViewModels.NavbarButtonConfig {
|
||||
let connectedToGitHub: boolean = container.gitHubOAuthService.isLoggedIn();
|
||||
let connectedToGitHub: boolean = container.notebookManager?.gitHubOAuthService.isLoggedIn();
|
||||
const label = connectedToGitHub ? "Manage GitHub settings" : "Connect to GitHub";
|
||||
return {
|
||||
iconSrc: GitHubIcon,
|
||||
|
@ -36,11 +36,12 @@ export class CommandBarUtil {
|
||||
|
||||
const result: ICommandBarItemProps = {
|
||||
iconProps: {
|
||||
iconType: IconType.image,
|
||||
style: {
|
||||
width: StyleConstants.CommandBarIconWidth // 16
|
||||
width: StyleConstants.CommandBarIconWidth, // 16
|
||||
alignSelf: btn.iconName ? "baseline" : undefined
|
||||
},
|
||||
imageProps: { src: btn.iconSrc, alt: btn.iconAlt }
|
||||
imageProps: btn.iconSrc ? { src: btn.iconSrc, alt: btn.iconAlt } : undefined,
|
||||
iconName: btn.iconName
|
||||
},
|
||||
onClick: btn.onCommandClick,
|
||||
key: `${btn.commandButtonLabel}${index}`,
|
||||
|
@ -17,7 +17,7 @@ import {
|
||||
} from "@nteract/core";
|
||||
import * as Immutable from "immutable";
|
||||
import { Provider } from "react-redux";
|
||||
import { CellType, CellId } from "@nteract/commutable";
|
||||
import { CellType, CellId, toJS } from "@nteract/commutable";
|
||||
import { Store, AnyAction } from "redux";
|
||||
|
||||
import "./NotebookComponent.less";
|
||||
@ -71,6 +71,28 @@ export class NotebookComponentBootstrapper {
|
||||
);
|
||||
}
|
||||
|
||||
public getContent(): { name: string; content: string } {
|
||||
const record = this.getStore()
|
||||
.getState()
|
||||
.core.entities.contents.byRef.get(this.contentRef);
|
||||
let content: string;
|
||||
switch (record.model.type) {
|
||||
case "notebook":
|
||||
content = JSON.stringify(toJS(record.model.notebook));
|
||||
break;
|
||||
case "file":
|
||||
content = record.model.text;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported model type ${record.model.type}`);
|
||||
}
|
||||
|
||||
return {
|
||||
name: NotebookUtil.getName(record.filepath),
|
||||
content
|
||||
};
|
||||
}
|
||||
|
||||
public setContent(name: string, content: any): void {
|
||||
this.getStore().dispatch(
|
||||
actions.fetchContentFulfilled({
|
||||
|
168
src/Explorer/Notebook/NotebookManager.ts
Normal file
168
src/Explorer/Notebook/NotebookManager.ts
Normal file
@ -0,0 +1,168 @@
|
||||
/*
|
||||
* Contains all notebook related stuff meant to be dynamically loaded by explorer
|
||||
*/
|
||||
|
||||
import { JunoClient } from "../../Juno/JunoClient";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { GitHubOAuthService } from "../../GitHub/GitHubOAuthService";
|
||||
import { GitHubClient } from "../../GitHub/GitHubClient";
|
||||
import { config } from "../../Config";
|
||||
import * as Logger from "../../Common/Logger";
|
||||
import { HttpStatusCodes, Areas } from "../../Common/Constants";
|
||||
import { GitHubReposPane } from "../Panes/GitHubReposPane";
|
||||
import ko from "knockout";
|
||||
import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
import { IContentProvider } from "@nteract/core";
|
||||
import { NotebookContentProvider } from "./NotebookComponent/NotebookContentProvider";
|
||||
import { GitHubContentProvider } from "../../GitHub/GitHubContentProvider";
|
||||
import { contents } from "rx-jupyter";
|
||||
import { NotebookContainerClient } from "./NotebookContainerClient";
|
||||
import { MemoryUsageInfo } from "../../Contracts/DataModels";
|
||||
import { NotebookContentClient } from "./NotebookContentClient";
|
||||
import { DialogProps } from "../Controls/DialogReactComponent/DialogComponent";
|
||||
import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter";
|
||||
import { PublishNotebookPaneAdapter } from "../Panes/PublishNotebookPaneAdapter";
|
||||
import { getFullName } from "../../Utils/UserUtils";
|
||||
|
||||
export interface NotebookManagerOptions {
|
||||
container: ViewModels.Explorer;
|
||||
notebookBasePath: ko.Observable<string>;
|
||||
dialogProps: ko.Observable<DialogProps>;
|
||||
resourceTree: ResourceTreeAdapter;
|
||||
refreshCommandBarButtons: () => void;
|
||||
refreshNotebookList: () => void;
|
||||
}
|
||||
|
||||
export default class NotebookManager {
|
||||
private params: NotebookManagerOptions;
|
||||
public junoClient: JunoClient;
|
||||
|
||||
public notebookContentProvider: IContentProvider;
|
||||
public notebookClient: ViewModels.INotebookContainerClient;
|
||||
public notebookContentClient: ViewModels.INotebookContentClient;
|
||||
|
||||
private gitHubContentProvider: GitHubContentProvider;
|
||||
public gitHubOAuthService: GitHubOAuthService;
|
||||
private gitHubClient: GitHubClient;
|
||||
|
||||
public gitHubReposPane: ViewModels.ContextualPane;
|
||||
public publishNotebookPaneAdapter: PublishNotebookPaneAdapter;
|
||||
|
||||
public initialize(params: NotebookManagerOptions): void {
|
||||
this.params = params;
|
||||
this.junoClient = new JunoClient(this.params.container.databaseAccount);
|
||||
|
||||
this.gitHubOAuthService = new GitHubOAuthService(this.junoClient);
|
||||
this.gitHubClient = new GitHubClient(config.AZURESAMPLESCOSMOSDBPAT, this.onGitHubClientError);
|
||||
this.gitHubReposPane = new GitHubReposPane({
|
||||
documentClientUtility: this.params.container.documentClientUtility,
|
||||
id: "gitHubReposPane",
|
||||
visible: ko.observable<boolean>(false),
|
||||
container: this.params.container,
|
||||
junoClient: this.junoClient,
|
||||
gitHubClient: this.gitHubClient
|
||||
});
|
||||
|
||||
this.gitHubContentProvider = new GitHubContentProvider({
|
||||
gitHubClient: this.gitHubClient,
|
||||
promptForCommitMsg: this.promptForCommitMsg
|
||||
});
|
||||
|
||||
this.notebookContentProvider = new NotebookContentProvider(
|
||||
this.gitHubContentProvider,
|
||||
contents.JupyterContentProvider
|
||||
);
|
||||
|
||||
this.notebookClient = new NotebookContainerClient(
|
||||
this.params.container.notebookServerInfo,
|
||||
() => this.params.container.initNotebooks(this.params.container.databaseAccount()),
|
||||
(update: MemoryUsageInfo) => this.params.container.memoryUsageInfo(update)
|
||||
);
|
||||
|
||||
this.notebookContentClient = new NotebookContentClient(
|
||||
this.params.container.notebookServerInfo,
|
||||
this.params.notebookBasePath,
|
||||
this.notebookContentProvider
|
||||
);
|
||||
|
||||
if (this.params.container.isGalleryPublishEnabled()) {
|
||||
this.publishNotebookPaneAdapter = new PublishNotebookPaneAdapter(this.params.container, this.junoClient);
|
||||
}
|
||||
|
||||
this.gitHubOAuthService.getTokenObservable().subscribe(token => {
|
||||
this.gitHubClient.setToken(token?.access_token ? token.access_token : config.AZURESAMPLESCOSMOSDBPAT);
|
||||
|
||||
if (this.gitHubReposPane.visible()) {
|
||||
this.gitHubReposPane.open();
|
||||
}
|
||||
|
||||
this.params.refreshCommandBarButtons();
|
||||
this.params.refreshNotebookList();
|
||||
});
|
||||
|
||||
this.junoClient.subscribeToPinnedRepos(pinnedRepos => {
|
||||
this.params.resourceTree.initializeGitHubRepos(pinnedRepos);
|
||||
this.params.resourceTree.triggerRender();
|
||||
});
|
||||
this.junoClient.getPinnedRepos(this.gitHubOAuthService.getTokenObservable()()?.scope);
|
||||
}
|
||||
|
||||
public openPublishNotebookPane(name: string, content: string): void {
|
||||
this.publishNotebookPaneAdapter.open(name, getFullName(), content);
|
||||
}
|
||||
|
||||
// Octokit's error handler uses any
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private onGitHubClientError = (error: any): void => {
|
||||
Logger.logError(error, "NotebookManager/onGitHubClientError");
|
||||
|
||||
if (error.status === HttpStatusCodes.Unauthorized) {
|
||||
this.gitHubOAuthService.resetToken();
|
||||
|
||||
this.params.container.showOkCancelModalDialog(
|
||||
undefined,
|
||||
"Cosmos DB cannot access your Github account anymore. Please connect to GitHub again.",
|
||||
"Connect to GitHub",
|
||||
() => this.gitHubReposPane.open(),
|
||||
"Cancel",
|
||||
undefined
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
private promptForCommitMsg = (title: string, primaryButtonLabel: string) => {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
let commitMsg = "Committed from Azure Cosmos DB Notebooks";
|
||||
this.params.container.showOkCancelTextFieldModalDialog(
|
||||
title || "Commit",
|
||||
undefined,
|
||||
primaryButtonLabel || "Commit",
|
||||
() => {
|
||||
TelemetryProcessor.trace(Action.NotebooksGitHubCommit, ActionModifiers.Mark, {
|
||||
databaseAccountName:
|
||||
this.params.container.databaseAccount() && this.params.container.databaseAccount().name,
|
||||
defaultExperience: this.params.container.defaultExperience && this.params.container.defaultExperience(),
|
||||
dataExplorerArea: Areas.Notebook
|
||||
});
|
||||
resolve(commitMsg);
|
||||
},
|
||||
"Cancel",
|
||||
() => reject(new Error("Commit dialog canceled")),
|
||||
{
|
||||
label: "Commit message",
|
||||
autoAdjustHeight: true,
|
||||
multiline: true,
|
||||
defaultValue: commitMsg,
|
||||
rows: 3,
|
||||
onChange: (_, newValue: string) => {
|
||||
commitMsg = newValue;
|
||||
this.params.dialogProps().primaryButtonDisabled = !commitMsg;
|
||||
this.params.dialogProps.valueHasMutated();
|
||||
}
|
||||
},
|
||||
!commitMsg
|
||||
);
|
||||
});
|
||||
};
|
||||
}
|
@ -6,8 +6,6 @@ import Q from "q";
|
||||
import { ArcadiaWorkspaceItem } from "./Controls/Arcadia/ArcadiaMenuPicker";
|
||||
import { CassandraTableKey, CassandraTableKeys, TableDataClient } from "../../src/Explorer/Tables/TableDataClient";
|
||||
import { ConsoleData } from "../../src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import { GitHubOAuthService } from "../GitHub/GitHubOAuthService";
|
||||
import { IContentProvider } from "@nteract/core";
|
||||
import { MostRecentActivity } from "./MostRecentActivity/MostRecentActivity";
|
||||
import { NotebookContentItem } from "./Notebook/NotebookContentItem";
|
||||
import { PlatformType } from "../../src/PlatformType";
|
||||
@ -22,6 +20,8 @@ import { UploadFilePane } from "./Panes/UploadFilePane";
|
||||
import { UploadItemsPaneAdapter } from "./Panes/UploadItemsPaneAdapter";
|
||||
import { Versions } from "../../src/Contracts/ExplorerContracts";
|
||||
import { CollectionCreationDefaults } from "../Shared/Constants";
|
||||
import { IGalleryItem } from "../Juno/JunoClient";
|
||||
import { ReactAdapter } from "../Bindings/ReactBindingHandler";
|
||||
|
||||
export class ExplorerStub implements ViewModels.Explorer {
|
||||
public flight: ko.Observable<string>;
|
||||
@ -97,7 +97,9 @@ export class ExplorerStub implements ViewModels.Explorer {
|
||||
public setupSparkClusterPane: ViewModels.ContextualPane;
|
||||
public manageSparkClusterPane: ViewModels.ContextualPane;
|
||||
public isGalleryEnabled: ko.Computed<boolean>;
|
||||
public isGalleryPublishEnabled: ko.Computed<boolean>;
|
||||
public isGitHubPaneEnabled: ko.Observable<boolean>;
|
||||
public isPublishNotebookPaneEnabled: ko.Observable<boolean>;
|
||||
public isRightPanelV2Enabled: ko.Computed<boolean>;
|
||||
public canExceedMaximumValue: ko.Computed<boolean>;
|
||||
public isHostedDataExplorerEnabled: ko.Computed<boolean>;
|
||||
@ -111,19 +113,19 @@ export class ExplorerStub implements ViewModels.Explorer {
|
||||
public arcadiaToken: ko.Observable<string>;
|
||||
public notebookWorkspaceManager: ViewModels.NotebookWorkspaceManager;
|
||||
public sparkClusterManager: ViewModels.SparkClusterManager;
|
||||
public notebookContentProvider: IContentProvider;
|
||||
public gitHubOAuthService: GitHubOAuthService;
|
||||
public notebookServerInfo: ko.Observable<DataModels.NotebookWorkspaceConnectionInfo>;
|
||||
public sparkClusterConnectionInfo: ko.Observable<DataModels.SparkClusterConnectionInfo>;
|
||||
public libraryManagePane: ViewModels.ContextualPane;
|
||||
public clusterLibraryPane: ViewModels.ContextualPane;
|
||||
public gitHubReposPane: ViewModels.ContextualPane;
|
||||
public publishNotebookPaneAdapter: ReactAdapter;
|
||||
public arcadiaWorkspaces: ko.ObservableArray<ArcadiaWorkspaceItem>;
|
||||
public hasStorageAnalyticsAfecFeature: ko.Observable<boolean>;
|
||||
public isSynapseLinkUpdating: ko.Observable<boolean>;
|
||||
public isNotebookTabActive: ko.Computed<boolean>;
|
||||
public memoryUsageInfo: ko.Observable<DataModels.MemoryUsageInfo>;
|
||||
public openGallery: () => void;
|
||||
public notebookManager?: any;
|
||||
public openGallery: (notebookUrl?: string, galleryItem?: IGalleryItem, isFavorite?: boolean) => void;
|
||||
public openNotebookViewer: (notebookUrl: string) => void;
|
||||
public resourceTokenDatabaseId: ko.Observable<string>;
|
||||
public resourceTokenCollectionId: ko.Observable<string>;
|
||||
@ -331,7 +333,11 @@ export class ExplorerStub implements ViewModels.Explorer {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
public importAndOpenFromGallery(path: string, newName: string, content: any): Promise<boolean> {
|
||||
public importAndOpenFromGallery(name: string, content: string): Promise<boolean> {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
public publishNotebook(name: string, content: string): void {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
|
@ -132,12 +132,13 @@ export class GitHubReposPane extends ContextualPaneBase {
|
||||
|
||||
private getOAuthScope(): string {
|
||||
return (
|
||||
this.container.gitHubOAuthService?.getTokenObservable()()?.scope || AuthorizeAccessComponent.Scopes.Public.key
|
||||
this.container.notebookManager?.gitHubOAuthService.getTokenObservable()()?.scope ||
|
||||
AuthorizeAccessComponent.Scopes.Public.key
|
||||
);
|
||||
}
|
||||
|
||||
private setup(forceShowConnectToGitHub = false): void {
|
||||
forceShowConnectToGitHub || !this.container.gitHubOAuthService.isLoggedIn()
|
||||
forceShowConnectToGitHub || !this.container.notebookManager?.gitHubOAuthService.isLoggedIn()
|
||||
? this.setupForConnectToGitHub()
|
||||
: this.setupForManageRepos();
|
||||
}
|
||||
@ -294,7 +295,7 @@ export class GitHubReposPane extends ContextualPaneBase {
|
||||
|
||||
try {
|
||||
const response = await this.junoClient.getPinnedRepos(
|
||||
this.container.gitHubOAuthService?.getTokenObservable()()?.scope
|
||||
this.container.notebookManager?.gitHubOAuthService.getTokenObservable()()?.scope
|
||||
);
|
||||
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
|
||||
throw new Error(`Received HTTP ${response.status} when fetching pinned repos`);
|
||||
@ -350,7 +351,7 @@ export class GitHubReposPane extends ContextualPaneBase {
|
||||
dataExplorerArea: Areas.Notebook,
|
||||
scopesSelected: scope
|
||||
});
|
||||
this.container.gitHubOAuthService.startOAuth(scope);
|
||||
this.container.notebookManager?.gitHubOAuthService.startOAuth(scope);
|
||||
}
|
||||
|
||||
private triggerRender(): void {
|
||||
|
164
src/Explorer/Panes/PublishNotebookPaneAdapter.tsx
Normal file
164
src/Explorer/Panes/PublishNotebookPaneAdapter.tsx
Normal file
@ -0,0 +1,164 @@
|
||||
import ko from "knockout";
|
||||
import { ITextFieldProps, Stack, Text, TextField } from "office-ui-fabric-react";
|
||||
import * as React from "react";
|
||||
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
|
||||
import * as Logger from "../../Common/Logger";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { JunoClient } from "../../Juno/JunoClient";
|
||||
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils";
|
||||
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import { FileSystemUtil } from "../Notebook/FileSystemUtil";
|
||||
import { GenericRightPaneComponent, GenericRightPaneProps } from "./GenericRightPaneComponent";
|
||||
|
||||
export class PublishNotebookPaneAdapter implements ReactAdapter {
|
||||
parameters: ko.Observable<number>;
|
||||
private isOpened: boolean;
|
||||
private isExecuting: boolean;
|
||||
private formError: string;
|
||||
private formErrorDetail: string;
|
||||
|
||||
private name: string;
|
||||
private author: string;
|
||||
private content: string;
|
||||
private description: string;
|
||||
private tags: string;
|
||||
private thumbnailUrl: string;
|
||||
|
||||
constructor(private container: ViewModels.Explorer, private junoClient: JunoClient) {
|
||||
this.parameters = ko.observable(Date.now());
|
||||
this.reset();
|
||||
this.triggerRender();
|
||||
}
|
||||
|
||||
public renderComponent(): JSX.Element {
|
||||
if (!this.isOpened) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const props: GenericRightPaneProps = {
|
||||
container: this.container,
|
||||
content: this.createContent(),
|
||||
formError: this.formError,
|
||||
formErrorDetail: this.formErrorDetail,
|
||||
id: "publishnotebookpane",
|
||||
isExecuting: this.isExecuting,
|
||||
title: "Publish to gallery",
|
||||
submitButtonText: "Publish",
|
||||
onClose: () => this.close(),
|
||||
onSubmit: () => this.submit()
|
||||
};
|
||||
|
||||
return <GenericRightPaneComponent {...props} />;
|
||||
}
|
||||
|
||||
public triggerRender(): void {
|
||||
window.requestAnimationFrame(() => this.parameters(Date.now()));
|
||||
}
|
||||
|
||||
public open(name: string, author: string, content: string): void {
|
||||
this.name = name;
|
||||
this.author = author;
|
||||
this.content = content;
|
||||
|
||||
this.isOpened = true;
|
||||
this.triggerRender();
|
||||
}
|
||||
|
||||
public close(): void {
|
||||
this.reset();
|
||||
this.triggerRender();
|
||||
}
|
||||
|
||||
public async submit(): Promise<void> {
|
||||
const notificationId = NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.InProgress,
|
||||
`Publishing ${this.name} to gallery`
|
||||
);
|
||||
this.isExecuting = true;
|
||||
this.triggerRender();
|
||||
|
||||
try {
|
||||
if (!this.name || !this.description || !this.author) {
|
||||
throw new Error("Name, description, and author are required");
|
||||
}
|
||||
|
||||
const response = await this.junoClient.publishNotebook(
|
||||
this.name,
|
||||
this.description,
|
||||
this.tags?.split(","),
|
||||
this.author,
|
||||
this.thumbnailUrl,
|
||||
this.content
|
||||
);
|
||||
if (!response.data) {
|
||||
throw new Error(`Received HTTP ${response.status} when publishing ${name} to gallery`);
|
||||
}
|
||||
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, `Published ${name} to gallery`);
|
||||
} catch (error) {
|
||||
this.formError = `Failed to publish ${this.name} to gallery`;
|
||||
this.formErrorDetail = `${error}`;
|
||||
|
||||
const message = `${this.formError}: ${this.formErrorDetail}`;
|
||||
Logger.logError(message, "PublishNotebookPaneAdapter/submit");
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message);
|
||||
return;
|
||||
} finally {
|
||||
NotificationConsoleUtils.clearInProgressMessageWithId(notificationId);
|
||||
this.isExecuting = false;
|
||||
this.triggerRender();
|
||||
}
|
||||
|
||||
this.close();
|
||||
}
|
||||
|
||||
private createContent = (): JSX.Element => {
|
||||
const descriptionPara1 =
|
||||
"This notebook has your data. Please make sure you delete any sensitive data/output before publishing.";
|
||||
const descriptionPara2 = `Would you like to publish and share ${FileSystemUtil.stripExtension(
|
||||
this.name,
|
||||
"ipynb"
|
||||
)} to the gallery?`;
|
||||
const descriptionProps: ITextFieldProps = {
|
||||
label: "Description",
|
||||
ariaLabel: "Description",
|
||||
multiline: true,
|
||||
rows: 3,
|
||||
required: true,
|
||||
onChange: (event, newValue) => (this.description = newValue)
|
||||
};
|
||||
const tagsProps: ITextFieldProps = {
|
||||
label: "Tags",
|
||||
ariaLabel: "Tags",
|
||||
placeholder: "Optional tag 1, Optional tag 2",
|
||||
onChange: (event, newValue) => (this.tags = newValue)
|
||||
};
|
||||
const thumbnailProps: ITextFieldProps = {
|
||||
label: "Cover image url",
|
||||
ariaLabel: "Cover image url",
|
||||
onChange: (event, newValue) => (this.thumbnailUrl = newValue)
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="panelContent">
|
||||
<Stack className="paneMainContent" tokens={{ childrenGap: 20 }}>
|
||||
<Text>{descriptionPara1}</Text>
|
||||
<Text>{descriptionPara2}</Text>
|
||||
<TextField {...descriptionProps} />
|
||||
<TextField {...tagsProps} />
|
||||
<TextField {...thumbnailProps} />
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
private reset = (): void => {
|
||||
this.isOpened = false;
|
||||
this.isExecuting = false;
|
||||
this.formError = undefined;
|
||||
this.formErrorDetail = undefined;
|
||||
this.name = undefined;
|
||||
this.author = undefined;
|
||||
this.content = undefined;
|
||||
};
|
||||
}
|
@ -1,45 +1,151 @@
|
||||
import * as ko from "knockout";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import TabsBase from "./TabsBase";
|
||||
import * as React from "react";
|
||||
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
|
||||
import { GalleryViewerContainerComponent } from "../Controls/NotebookGallery/GalleryViewerComponent";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { IGalleryItem, JunoClient } from "../../Juno/JunoClient";
|
||||
import * as GalleryUtils from "../../Utils/GalleryUtils";
|
||||
import {
|
||||
GalleryTab as GalleryViewerTab,
|
||||
GalleryViewerComponent,
|
||||
GalleryViewerComponentProps,
|
||||
SortBy
|
||||
} from "../Controls/NotebookGallery/GalleryViewerComponent";
|
||||
import {
|
||||
NotebookViewerComponent,
|
||||
NotebookViewerComponentProps
|
||||
} from "../Controls/NotebookViewer/NotebookViewerComponent";
|
||||
import TabsBase from "./TabsBase";
|
||||
|
||||
/**
|
||||
* 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.Computed<boolean>;
|
||||
constructor(private getContainer: () => ViewModels.Explorer) {}
|
||||
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 {
|
||||
return this.parameters() ? <GalleryViewerContainerComponent container={this.getContainer()} /> : <></>;
|
||||
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 {
|
||||
private container: ViewModels.Explorer;
|
||||
private galleryComponentAdapterProps: GalleryComponentAdapterProps;
|
||||
private galleryComponentAdapter: GalleryComponentAdapter;
|
||||
|
||||
constructor(options: ViewModels.GalleryTabOptions) {
|
||||
super(options);
|
||||
this.container = options.container;
|
||||
this.galleryComponentAdapter = new GalleryComponentAdapter(() => this.getContainer());
|
||||
|
||||
this.galleryComponentAdapter.parameters = ko.computed<boolean>(() => {
|
||||
return this.isTemplateReady() && this.container.isNotebookEnabled();
|
||||
});
|
||||
this.container = options.container;
|
||||
this.galleryComponentAdapterProps = {
|
||||
container: options.container,
|
||||
junoClient: options.junoClient,
|
||||
notebookUrl: options.notebookUrl,
|
||||
galleryItem: options.galleryItem,
|
||||
isFavorite: options.isFavorite,
|
||||
selectedTab: GalleryViewerTab.OfficialSamples,
|
||||
sortBy: SortBy.MostViewed,
|
||||
searchText: undefined
|
||||
};
|
||||
|
||||
this.galleryComponentAdapter = new GalleryComponentAdapter(this.galleryComponentAdapterProps);
|
||||
}
|
||||
|
||||
protected getContainer(): ViewModels.Explorer {
|
||||
return this.container;
|
||||
}
|
||||
|
||||
protected getTabsButtons(): ViewModels.NavbarButtonConfig[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
protected buildCommandBarOptions(): void {
|
||||
this.updateNavbarWithTabsButtons();
|
||||
public updateGalleryParams(notebookUrl?: string, galleryItem?: IGalleryItem, isFavorite?: boolean): void {
|
||||
this.galleryComponentAdapter.setState({
|
||||
notebookUrl,
|
||||
galleryItem,
|
||||
isFavorite
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -45,7 +45,7 @@ export default class NotebookTabV2 extends TabsBase implements ViewModels.Tab {
|
||||
connectionInfo: this.container.notebookServerInfo(),
|
||||
databaseAccountName: this.container.databaseAccount().name,
|
||||
defaultExperience: this.container.defaultExperience(),
|
||||
contentProvider: this.container.notebookContentProvider
|
||||
contentProvider: this.container.notebookManager?.notebookContentProvider
|
||||
});
|
||||
}
|
||||
|
||||
@ -112,6 +112,7 @@ export default class NotebookTabV2 extends TabsBase implements ViewModels.Tab {
|
||||
const availableKernels = NotebookTabV2.clientManager.getAvailableKernelSpecs();
|
||||
|
||||
const saveLabel = "Save";
|
||||
const publishLabel = "Publish to gallery";
|
||||
const workspaceLabel = "No Workspace";
|
||||
const kernelLabel = "No Kernel";
|
||||
const runLabel = "Run";
|
||||
@ -142,8 +143,28 @@ export default class NotebookTabV2 extends TabsBase implements ViewModels.Tab {
|
||||
commandButtonLabel: saveLabel,
|
||||
hasPopup: false,
|
||||
disabled: false,
|
||||
ariaLabel: saveLabel,
|
||||
children: this.container.isGalleryPublishEnabled()
|
||||
? [
|
||||
{
|
||||
iconName: "Save",
|
||||
onCommandClick: () => this.notebookComponentAdapter.notebookSave(),
|
||||
commandButtonLabel: saveLabel,
|
||||
hasPopup: false,
|
||||
disabled: false,
|
||||
ariaLabel: saveLabel
|
||||
},
|
||||
{
|
||||
iconName: "PublishContent",
|
||||
onCommandClick: () => this.publishToGallery(),
|
||||
commandButtonLabel: publishLabel,
|
||||
hasPopup: false,
|
||||
disabled: false,
|
||||
ariaLabel: publishLabel
|
||||
}
|
||||
]
|
||||
: undefined
|
||||
},
|
||||
{
|
||||
iconSrc: null,
|
||||
iconAlt: kernelLabel,
|
||||
@ -425,6 +446,11 @@ export default class NotebookTabV2 extends TabsBase implements ViewModels.Tab {
|
||||
);
|
||||
}
|
||||
|
||||
private publishToGallery = () => {
|
||||
const notebookContent = this.notebookComponentAdapter.getContent();
|
||||
this.container.publishNotebook(notebookContent.name, notebookContent.content);
|
||||
};
|
||||
|
||||
private traceTelemetry(actionType: number) {
|
||||
TelemetryProcessor.trace(actionType, ActionModifiers.Mark, {
|
||||
databaseAccountName: this.container.databaseAccount() && this.container.databaseAccount().name,
|
||||
|
@ -1,10 +1,12 @@
|
||||
import * as ko from "knockout";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import TabsBase from "./TabsBase";
|
||||
import * as React from "react";
|
||||
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
|
||||
import { NotebookViewerComponent } from "../Controls/NotebookViewer/NotebookViewerComponent";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import {
|
||||
NotebookViewerComponent,
|
||||
NotebookViewerComponentProps
|
||||
} from "../Controls/NotebookViewer/NotebookViewerComponent";
|
||||
import TabsBase from "./TabsBase";
|
||||
|
||||
/**
|
||||
* Notebook Viewer tab
|
||||
@ -12,48 +14,32 @@ import { NotebookViewerComponent } from "../Controls/NotebookViewer/NotebookView
|
||||
class NotebookViewerComponentAdapter implements ReactAdapter {
|
||||
// parameters: true: show, false: hide
|
||||
public parameters: ko.Computed<boolean>;
|
||||
constructor(
|
||||
private notebookUrl: string,
|
||||
private notebookName: string,
|
||||
private container: ViewModels.Explorer,
|
||||
private notebookMetadata: DataModels.NotebookMetadata,
|
||||
private onNotebookMetadataChange: (newNotebookMetadata: DataModels.NotebookMetadata) => Promise<void>,
|
||||
private isLikedNotebook: boolean
|
||||
) {}
|
||||
constructor(private notebookUrl: string) {}
|
||||
|
||||
public renderComponent(): JSX.Element {
|
||||
return this.parameters() ? (
|
||||
<NotebookViewerComponent
|
||||
notebookUrl={this.notebookUrl}
|
||||
notebookMetadata={this.notebookMetadata}
|
||||
notebookName={this.notebookName}
|
||||
container={this.container}
|
||||
onNotebookMetadataChange={this.onNotebookMetadataChange}
|
||||
isLikedNotebook={this.isLikedNotebook}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
const props: NotebookViewerComponentProps = {
|
||||
notebookUrl: this.notebookUrl,
|
||||
backNavigationText: undefined,
|
||||
onBackClick: undefined,
|
||||
onTagClick: undefined
|
||||
};
|
||||
|
||||
return this.parameters() ? <NotebookViewerComponent {...props} /> : <></>;
|
||||
}
|
||||
}
|
||||
|
||||
export default class NotebookViewerTab extends TabsBase implements ViewModels.Tab {
|
||||
private container: ViewModels.Explorer;
|
||||
public notebookViewerComponentAdapter: NotebookViewerComponentAdapter;
|
||||
public notebookUrl: string;
|
||||
|
||||
public notebookViewerComponentAdapter: NotebookViewerComponentAdapter;
|
||||
|
||||
constructor(options: ViewModels.NotebookViewerTabOptions) {
|
||||
super(options);
|
||||
this.container = options.container;
|
||||
this.notebookUrl = options.notebookUrl;
|
||||
this.notebookViewerComponentAdapter = new NotebookViewerComponentAdapter(
|
||||
options.notebookUrl,
|
||||
options.notebookName,
|
||||
options.container,
|
||||
options.notebookMetadata,
|
||||
options.onNotebookMetadataChange,
|
||||
options.isLikedNotebook
|
||||
);
|
||||
|
||||
this.notebookViewerComponentAdapter = new NotebookViewerComponentAdapter(options.notebookUrl);
|
||||
|
||||
this.notebookViewerComponentAdapter.parameters = ko.computed<boolean>(() => {
|
||||
if (this.isTemplateReady() && this.container.isNotebookEnabled()) {
|
||||
|
@ -19,12 +19,15 @@ import { ArrayHashMap } from "../../Common/ArrayHashMap";
|
||||
import { NotebookUtil } from "../Notebook/NotebookUtil";
|
||||
import _ from "underscore";
|
||||
import { StringUtils } from "../../Utils/StringUtils";
|
||||
import { JunoClient, IPinnedRepo } from "../../Juno/JunoClient";
|
||||
import { IPinnedRepo } from "../../Juno/JunoClient";
|
||||
import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
import { Areas } from "../../Common/Constants";
|
||||
import * as GitHubUtils from "../../Utils/GitHubUtils";
|
||||
import { SamplesRepo, SamplesBranch } from "../Notebook/NotebookSamples";
|
||||
import GalleryIcon from "../../../images/GalleryIcon.svg";
|
||||
import { Callout, Text, Link, DirectionalHint, Stack, ICalloutProps, ILinkProps } from "office-ui-fabric-react";
|
||||
import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility";
|
||||
|
||||
export class ResourceTreeAdapter implements ReactAdapter {
|
||||
private static readonly DataTitle = "DATA";
|
||||
@ -33,17 +36,16 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
||||
|
||||
public parameters: ko.Observable<number>;
|
||||
|
||||
public galleryContentRoot: NotebookContentItem;
|
||||
public sampleNotebooksContentRoot: NotebookContentItem;
|
||||
public myNotebooksContentRoot: NotebookContentItem;
|
||||
public gitHubNotebooksContentRoot: NotebookContentItem;
|
||||
|
||||
private pinnedReposSubscription: ko.Subscription;
|
||||
|
||||
private koSubsDatabaseIdMap: ArrayHashMap<ko.Subscription>; // database id -> ko subs
|
||||
private koSubsCollectionIdMap: ArrayHashMap<ko.Subscription>; // collection id -> ko subs
|
||||
private databaseCollectionIdMap: ArrayHashMap<string>; // database id -> collection ids
|
||||
|
||||
public constructor(private container: ViewModels.Explorer, private junoClient: JunoClient) {
|
||||
public constructor(private container: ViewModels.Explorer) {
|
||||
this.parameters = ko.observable(Date.now());
|
||||
|
||||
this.container.selectedNode.subscribe((newValue: any) => this.triggerRender());
|
||||
@ -72,6 +74,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
||||
|
||||
if (this.container.isNotebookEnabled()) {
|
||||
return (
|
||||
<>
|
||||
<AccordionComponent>
|
||||
<AccordionItemComponent title={ResourceTreeAdapter.DataTitle} isExpanded={!this.gitHubNotebooksContentRoot}>
|
||||
<TreeComponent className="dataResourceTree" rootNode={dataRootNode} />
|
||||
@ -80,6 +83,9 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
||||
<TreeComponent className="notebookResourceTree" rootNode={notebooksRootNode} />
|
||||
</AccordionItemComponent>
|
||||
</AccordionComponent>
|
||||
|
||||
{this.galleryContentRoot && this.buildGalleryCallout()}
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
return <TreeComponent className="dataResourceTree" rootNode={dataRootNode} />;
|
||||
@ -89,6 +95,17 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
||||
public async initialize(): Promise<void[]> {
|
||||
const refreshTasks: Promise<void>[] = [];
|
||||
|
||||
if (this.container.isGalleryEnabled()) {
|
||||
this.galleryContentRoot = {
|
||||
name: "Gallery",
|
||||
path: "Gallery",
|
||||
type: NotebookContentItemType.File
|
||||
};
|
||||
|
||||
this.sampleNotebooksContentRoot = undefined;
|
||||
} else {
|
||||
this.galleryContentRoot = undefined;
|
||||
|
||||
this.sampleNotebooksContentRoot = {
|
||||
name: "Sample Notebooks (View Only)",
|
||||
path: GitHubUtils.toContentUri(SamplesRepo.owner, SamplesRepo.name, SamplesBranch.name, ""),
|
||||
@ -97,6 +114,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
||||
refreshTasks.push(
|
||||
this.container.refreshContentItem(this.sampleNotebooksContentRoot).then(() => this.triggerRender())
|
||||
);
|
||||
}
|
||||
|
||||
this.myNotebooksContentRoot = {
|
||||
name: "My Notebooks",
|
||||
@ -111,14 +129,12 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
||||
);
|
||||
}
|
||||
|
||||
if (this.container.gitHubOAuthService?.isLoggedIn()) {
|
||||
if (this.container.notebookManager?.gitHubOAuthService.isLoggedIn()) {
|
||||
this.gitHubNotebooksContentRoot = {
|
||||
name: "GitHub repos",
|
||||
path: ResourceTreeAdapter.PseudoDirPath,
|
||||
type: NotebookContentItemType.Directory
|
||||
};
|
||||
|
||||
refreshTasks.push(this.refreshGitHubReposAndTriggerRender(this.gitHubNotebooksContentRoot));
|
||||
} else {
|
||||
this.gitHubNotebooksContentRoot = undefined;
|
||||
}
|
||||
@ -126,10 +142,10 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
||||
return Promise.all(refreshTasks);
|
||||
}
|
||||
|
||||
private async refreshGitHubReposAndTriggerRender(item: NotebookContentItem): Promise<void> {
|
||||
const updateGitHubReposAndRender = (pinnedRepos: IPinnedRepo[]) => {
|
||||
item.children = [];
|
||||
pinnedRepos.forEach(pinnedRepo => {
|
||||
public initializeGitHubRepos(pinnedRepos: IPinnedRepo[]): void {
|
||||
if (this.gitHubNotebooksContentRoot) {
|
||||
this.gitHubNotebooksContentRoot.children = [];
|
||||
pinnedRepos?.forEach(pinnedRepo => {
|
||||
const repoFullName = GitHubUtils.toRepoFullName(pinnedRepo.owner, pinnedRepo.name);
|
||||
const repoTreeItem: NotebookContentItem = {
|
||||
name: repoFullName,
|
||||
@ -146,20 +162,11 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
||||
});
|
||||
});
|
||||
|
||||
item.children.push(repoTreeItem);
|
||||
this.gitHubNotebooksContentRoot.children.push(repoTreeItem);
|
||||
});
|
||||
|
||||
this.triggerRender();
|
||||
};
|
||||
|
||||
if (this.pinnedReposSubscription) {
|
||||
this.pinnedReposSubscription.dispose();
|
||||
}
|
||||
this.pinnedReposSubscription = this.junoClient.subscribeToPinnedRepos(pinnedRepos =>
|
||||
updateGitHubReposAndRender(pinnedRepos)
|
||||
);
|
||||
|
||||
await this.junoClient.getPinnedRepos(this.container.gitHubOAuthService?.getTokenObservable()()?.scope);
|
||||
}
|
||||
|
||||
private buildDataTree(): TreeNode {
|
||||
@ -347,10 +354,13 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
||||
let notebooksTree: TreeNode = {
|
||||
label: undefined,
|
||||
isExpanded: true,
|
||||
isLeavesParentsSeparate: true,
|
||||
children: []
|
||||
};
|
||||
|
||||
if (this.galleryContentRoot) {
|
||||
notebooksTree.children.push(this.buildGalleryNotebooksTree());
|
||||
}
|
||||
|
||||
if (this.sampleNotebooksContentRoot) {
|
||||
notebooksTree.children.push(this.buildSampleNotebooksTree());
|
||||
}
|
||||
@ -368,6 +378,65 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
||||
return notebooksTree;
|
||||
}
|
||||
|
||||
private buildGalleryCallout(): JSX.Element {
|
||||
if (
|
||||
LocalStorageUtility.hasItem(StorageKey.GalleryCalloutDismissed) &&
|
||||
LocalStorageUtility.getEntryBoolean(StorageKey.GalleryCalloutDismissed)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const calloutProps: ICalloutProps = {
|
||||
calloutMaxWidth: 350,
|
||||
ariaLabel: "New gallery",
|
||||
role: "alertdialog",
|
||||
gapSpace: 0,
|
||||
target: ".galleryHeader",
|
||||
directionalHint: DirectionalHint.leftTopEdge,
|
||||
onDismiss: () => {
|
||||
LocalStorageUtility.setEntryBoolean(StorageKey.GalleryCalloutDismissed, true);
|
||||
this.triggerRender();
|
||||
},
|
||||
setInitialFocus: true
|
||||
};
|
||||
|
||||
const openGalleryProps: ILinkProps = {
|
||||
onClick: () => {
|
||||
LocalStorageUtility.setEntryBoolean(StorageKey.GalleryCalloutDismissed, true);
|
||||
this.container.openGallery();
|
||||
this.triggerRender();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Callout {...calloutProps}>
|
||||
<Stack tokens={{ childrenGap: 10, padding: 20 }}>
|
||||
<Text variant="xLarge" block>
|
||||
New gallery
|
||||
</Text>
|
||||
<Text block>
|
||||
Sample notebooks are now combined in gallery. View and try out samples provided by Microsoft and other
|
||||
contributors.
|
||||
</Text>
|
||||
<Link {...openGalleryProps}>Open gallery</Link>
|
||||
</Stack>
|
||||
</Callout>
|
||||
);
|
||||
}
|
||||
|
||||
private buildGalleryNotebooksTree(): TreeNode {
|
||||
return {
|
||||
label: "Gallery",
|
||||
iconSrc: GalleryIcon,
|
||||
className: "notebookHeader galleryHeader",
|
||||
onClick: () => this.container.openGallery(),
|
||||
isSelected: () => {
|
||||
const activeTab = this.container.findActiveTab();
|
||||
return activeTab && activeTab.tabKind === ViewModels.CollectionTabKind.Gallery;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private buildSampleNotebooksTree(): TreeNode {
|
||||
const sampleNotebooksTree: TreeNode = this.buildNotebookDirectoryNode(
|
||||
this.sampleNotebooksContentRoot,
|
||||
@ -467,7 +536,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
||||
defaultExperience: this.container.defaultExperience && this.container.defaultExperience(),
|
||||
dataExplorerArea: Areas.Notebook
|
||||
});
|
||||
this.container.gitHubOAuthService.logout();
|
||||
this.container.notebookManager?.gitHubOAuthService.logout();
|
||||
}
|
||||
}
|
||||
];
|
||||
|
@ -1,19 +1,33 @@
|
||||
import * as ReactDOM from "react-dom";
|
||||
import "bootstrap/dist/css/bootstrap.css";
|
||||
import { CosmosClient } from "../Common/CosmosClient";
|
||||
import { GalleryViewerComponent } from "../Explorer/Controls/NotebookGallery/GalleryViewerComponent";
|
||||
import { JunoUtils } from "../Utils/JunoUtils";
|
||||
import { initializeIcons } from "office-ui-fabric-react/lib/Icons";
|
||||
import * as React from "react";
|
||||
import * as ReactDOM from "react-dom";
|
||||
import { initializeConfiguration } from "../Config";
|
||||
import {
|
||||
GalleryTab,
|
||||
GalleryViewerComponent,
|
||||
GalleryViewerComponentProps,
|
||||
SortBy
|
||||
} from "../Explorer/Controls/NotebookGallery/GalleryViewerComponent";
|
||||
import { JunoClient } from "../Juno/JunoClient";
|
||||
import * as GalleryUtils from "../Utils/GalleryUtils";
|
||||
|
||||
const onInit = async () => {
|
||||
initializeIcons();
|
||||
const officialSamplesData = await JunoUtils.getOfficialSampleNotebooks(CosmosClient.authorizationToken());
|
||||
const galleryViewerComponent = new GalleryViewerComponent({
|
||||
officialSamplesData: officialSamplesData,
|
||||
likedNotebookData: undefined,
|
||||
container: undefined
|
||||
});
|
||||
ReactDOM.render(galleryViewerComponent.render(), document.getElementById("galleryContent"));
|
||||
await initializeConfiguration();
|
||||
const galleryViewerProps = GalleryUtils.getGalleryViewerProps(window);
|
||||
|
||||
const props: GalleryViewerComponentProps = {
|
||||
junoClient: new JunoClient(),
|
||||
selectedTab: galleryViewerProps.selectedTab || GalleryTab.OfficialSamples,
|
||||
sortBy: galleryViewerProps.sortBy || SortBy.MostViewed,
|
||||
searchText: galleryViewerProps.searchText,
|
||||
onSelectedTabChange: undefined,
|
||||
onSortByChange: undefined,
|
||||
onSearchTextChange: undefined
|
||||
};
|
||||
|
||||
ReactDOM.render(<GalleryViewerComponent {...props} />, document.getElementById("galleryContent"));
|
||||
};
|
||||
|
||||
// Entry point
|
||||
|
@ -8,6 +8,6 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="galleryComponentContainer" id="galleryContent"></div>
|
||||
<div class="galleryContent" id="galleryContent"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -5,6 +5,7 @@ import { JunoClient } from "../Juno/JunoClient";
|
||||
import { GitHubConnector, IGitHubConnectorParams } from "./GitHubConnector";
|
||||
import { GitHubOAuthService } from "./GitHubOAuthService";
|
||||
import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import NotebookManager from "../Explorer/Notebook/NotebookManager";
|
||||
|
||||
const sampleDatabaseAccount: ViewModels.DatabaseAccount = {
|
||||
id: "id",
|
||||
@ -32,10 +33,12 @@ describe("GitHubOAuthService", () => {
|
||||
originalDataExplorer = window.dataExplorer;
|
||||
window.dataExplorer = {
|
||||
...originalDataExplorer,
|
||||
gitHubOAuthService,
|
||||
logConsoleData: (data): void =>
|
||||
data.type === ConsoleDataType.Error ? console.error(data.message) : console.log(data.message)
|
||||
} as ViewModels.Explorer;
|
||||
window.dataExplorer.notebookManager = new NotebookManager();
|
||||
window.dataExplorer.notebookManager.junoClient = junoClient;
|
||||
window.dataExplorer.notebookManager.gitHubOAuthService = gitHubOAuthService;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -17,7 +17,7 @@ window.addEventListener("message", (event: MessageEvent) => {
|
||||
const msg = event.data;
|
||||
if (msg.type === GitHubConnectorMsgType) {
|
||||
const params = msg.data as IGitHubConnectorParams;
|
||||
window.dataExplorer.gitHubOAuthService.finishOAuth(params);
|
||||
window.dataExplorer.notebookManager?.gitHubOAuthService.finishOAuth(params);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -2,10 +2,10 @@ import ko from "knockout";
|
||||
import { HttpStatusCodes } from "../Common/Constants";
|
||||
import { config } from "../Config";
|
||||
import * as ViewModels from "../Contracts/ViewModels";
|
||||
import { AuthorizeAccessComponent } from "../Explorer/Controls/GitHub/AuthorizeAccessComponent";
|
||||
import { IGitHubResponse } from "../GitHub/GitHubClient";
|
||||
import { IGitHubOAuthToken } from "../GitHub/GitHubOAuthService";
|
||||
import { getAuthorizationHeader } from "../Utils/AuthorizationUtils";
|
||||
import { AuthorizeAccessComponent } from "../Explorer/Controls/GitHub/AuthorizeAccessComponent";
|
||||
|
||||
export interface IJunoResponse<T> {
|
||||
status: number;
|
||||
@ -23,10 +23,39 @@ export interface IPinnedBranch {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface IGalleryItem {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
gitSha: string;
|
||||
tags: string[];
|
||||
author: string;
|
||||
thumbnailUrl: string;
|
||||
created: string;
|
||||
isSample: boolean;
|
||||
downloads: number;
|
||||
favorites: number;
|
||||
views: number;
|
||||
}
|
||||
|
||||
export interface IUserGallery {
|
||||
favorites: string[];
|
||||
published: string[];
|
||||
}
|
||||
|
||||
interface IPublishNotebookRequest {
|
||||
name: string;
|
||||
description: string;
|
||||
tags: string[];
|
||||
author: string;
|
||||
thumbnailUrl: string;
|
||||
content: any;
|
||||
}
|
||||
|
||||
export class JunoClient {
|
||||
private cachedPinnedRepos: ko.Observable<IPinnedRepo[]>;
|
||||
|
||||
constructor(public databaseAccount: ko.Observable<ViewModels.DatabaseAccount>) {
|
||||
constructor(private databaseAccount?: ko.Observable<ViewModels.DatabaseAccount>) {
|
||||
this.cachedPinnedRepos = ko.observable<IPinnedRepo[]>([]);
|
||||
}
|
||||
|
||||
@ -35,8 +64,8 @@ export class JunoClient {
|
||||
}
|
||||
|
||||
public async getPinnedRepos(scope: string): Promise<IJunoResponse<IPinnedRepo[]>> {
|
||||
const response = await window.fetch(`${this.getJunoGitHubUrl()}/pinnedrepos`, {
|
||||
headers: this.getHeaders()
|
||||
const response = await window.fetch(`${this.getNotebooksAccountUrl()}/github/pinnedrepos`, {
|
||||
headers: JunoClient.getHeaders()
|
||||
});
|
||||
|
||||
let pinnedRepos: IPinnedRepo[];
|
||||
@ -58,10 +87,10 @@ export class JunoClient {
|
||||
}
|
||||
|
||||
public async updatePinnedRepos(repos: IPinnedRepo[]): Promise<IJunoResponse<undefined>> {
|
||||
const response = await window.fetch(`${this.getJunoGitHubUrl()}/pinnedrepos`, {
|
||||
const response = await window.fetch(`${this.getNotebooksAccountUrl()}/github/pinnedrepos`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(repos),
|
||||
headers: this.getHeaders()
|
||||
headers: JunoClient.getHeaders()
|
||||
});
|
||||
|
||||
if (response.status === HttpStatusCodes.OK) {
|
||||
@ -75,9 +104,9 @@ export class JunoClient {
|
||||
}
|
||||
|
||||
public async deleteGitHubInfo(): Promise<IJunoResponse<undefined>> {
|
||||
const response = await window.fetch(this.getJunoGitHubUrl(), {
|
||||
const response = await window.fetch(`${this.getNotebooksAccountUrl()}/github`, {
|
||||
method: "DELETE",
|
||||
headers: this.getHeaders()
|
||||
headers: JunoClient.getHeaders()
|
||||
});
|
||||
|
||||
return {
|
||||
@ -90,8 +119,8 @@ export class JunoClient {
|
||||
const githubParams = JunoClient.getGitHubClientParams();
|
||||
githubParams.append("code", code);
|
||||
|
||||
const response = await window.fetch(`${this.getJunoGitHubUrl()}/token?${githubParams.toString()}`, {
|
||||
headers: this.getHeaders()
|
||||
const response = await window.fetch(`${this.getNotebooksAccountUrl()}/github/token?${githubParams.toString()}`, {
|
||||
headers: JunoClient.getHeaders()
|
||||
});
|
||||
|
||||
let data: IGitHubOAuthToken;
|
||||
@ -114,9 +143,9 @@ export class JunoClient {
|
||||
const githubParams = JunoClient.getGitHubClientParams();
|
||||
githubParams.append("access_token", token);
|
||||
|
||||
const response = await window.fetch(`${this.getJunoGitHubUrl()}/token?${githubParams.toString()}`, {
|
||||
const response = await window.fetch(`${this.getNotebooksAccountUrl()}/github/token?${githubParams.toString()}`, {
|
||||
method: "DELETE",
|
||||
headers: this.getHeaders()
|
||||
headers: JunoClient.getHeaders()
|
||||
});
|
||||
|
||||
return {
|
||||
@ -125,11 +154,201 @@ export class JunoClient {
|
||||
};
|
||||
}
|
||||
|
||||
private getJunoGitHubUrl(): string {
|
||||
return `${config.JUNO_ENDPOINT}/api/notebooks/${this.databaseAccount().name}/github`;
|
||||
public async getSampleNotebooks(): Promise<IJunoResponse<IGalleryItem[]>> {
|
||||
return this.getNotebooks(`${this.getNotebooksUrl()}/gallery/samples`);
|
||||
}
|
||||
|
||||
private getHeaders(): HeadersInit {
|
||||
public async getPublicNotebooks(): Promise<IJunoResponse<IGalleryItem[]>> {
|
||||
return this.getNotebooks(`${this.getNotebooksUrl()}/gallery/public`);
|
||||
}
|
||||
|
||||
public async getNotebook(id: string): Promise<IJunoResponse<IGalleryItem>> {
|
||||
const response = await window.fetch(this.getNotebookInfoUrl(id));
|
||||
|
||||
let data: IGalleryItem;
|
||||
if (response.status === HttpStatusCodes.OK) {
|
||||
data = await response.json();
|
||||
}
|
||||
|
||||
return {
|
||||
status: response.status,
|
||||
data
|
||||
};
|
||||
}
|
||||
|
||||
public async getNotebookContent(id: string): Promise<IJunoResponse<string>> {
|
||||
const response = await window.fetch(this.getNotebookContentUrl(id));
|
||||
|
||||
let data: string;
|
||||
if (response.status === HttpStatusCodes.OK) {
|
||||
data = await response.text();
|
||||
}
|
||||
|
||||
return {
|
||||
status: response.status,
|
||||
data
|
||||
};
|
||||
}
|
||||
|
||||
public async increaseNotebookViews(id: string): Promise<IJunoResponse<IGalleryItem>> {
|
||||
const response = await window.fetch(`${this.getNotebooksUrl()}/gallery/${id}/views`, {
|
||||
method: "PATCH"
|
||||
});
|
||||
|
||||
let data: IGalleryItem;
|
||||
if (response.status === HttpStatusCodes.OK) {
|
||||
data = await response.json();
|
||||
}
|
||||
|
||||
return {
|
||||
status: response.status,
|
||||
data
|
||||
};
|
||||
}
|
||||
|
||||
public async increaseNotebookDownloadCount(id: string): Promise<IJunoResponse<IGalleryItem>> {
|
||||
const response = await window.fetch(`${this.getNotebooksAccountUrl()}/gallery/${id}/downloads`, {
|
||||
method: "PATCH",
|
||||
headers: JunoClient.getHeaders()
|
||||
});
|
||||
|
||||
let data: IGalleryItem;
|
||||
if (response.status === HttpStatusCodes.OK) {
|
||||
data = await response.json();
|
||||
}
|
||||
|
||||
return {
|
||||
status: response.status,
|
||||
data
|
||||
};
|
||||
}
|
||||
|
||||
public async favoriteNotebook(id: string): Promise<IJunoResponse<IGalleryItem>> {
|
||||
const response = await window.fetch(`${this.getNotebooksAccountUrl()}/gallery/${id}/favorite`, {
|
||||
method: "PATCH",
|
||||
headers: JunoClient.getHeaders()
|
||||
});
|
||||
|
||||
let data: IGalleryItem;
|
||||
if (response.status === HttpStatusCodes.OK) {
|
||||
data = await response.json();
|
||||
}
|
||||
|
||||
return {
|
||||
status: response.status,
|
||||
data
|
||||
};
|
||||
}
|
||||
|
||||
public async unfavoriteNotebook(id: string): Promise<IJunoResponse<IGalleryItem>> {
|
||||
const response = await window.fetch(`${this.getNotebooksUrl()}/gallery/${id}/unfavorite`, {
|
||||
method: "PATCH",
|
||||
headers: JunoClient.getHeaders()
|
||||
});
|
||||
|
||||
let data: IGalleryItem;
|
||||
if (response.status === HttpStatusCodes.OK) {
|
||||
data = await response.json();
|
||||
}
|
||||
|
||||
return {
|
||||
status: response.status,
|
||||
data
|
||||
};
|
||||
}
|
||||
|
||||
public async getFavoriteNotebooks(): Promise<IJunoResponse<IGalleryItem[]>> {
|
||||
return await this.getNotebooks(`${this.getNotebooksUrl()}/gallery/favorites`, {
|
||||
headers: JunoClient.getHeaders()
|
||||
});
|
||||
}
|
||||
|
||||
public async getPublishedNotebooks(): Promise<IJunoResponse<IGalleryItem[]>> {
|
||||
return await this.getNotebooks(`${this.getNotebooksUrl()}/gallery/published`, {
|
||||
headers: JunoClient.getHeaders()
|
||||
});
|
||||
}
|
||||
|
||||
public async deleteNotebook(id: string): Promise<IJunoResponse<IGalleryItem>> {
|
||||
const response = await window.fetch(`${this.getNotebooksUrl()}/gallery/${id}`, {
|
||||
method: "DELETE",
|
||||
headers: JunoClient.getHeaders()
|
||||
});
|
||||
|
||||
let data: IGalleryItem;
|
||||
if (response.status === HttpStatusCodes.OK) {
|
||||
data = await response.json();
|
||||
}
|
||||
|
||||
return {
|
||||
status: response.status,
|
||||
data
|
||||
};
|
||||
}
|
||||
|
||||
public async publishNotebook(
|
||||
name: string,
|
||||
description: string,
|
||||
tags: string[],
|
||||
author: string,
|
||||
thumbnailUrl: string,
|
||||
content: string
|
||||
): Promise<IJunoResponse<IGalleryItem>> {
|
||||
const response = await window.fetch(`${this.getNotebooksAccountUrl()}/gallery`, {
|
||||
method: "PUT",
|
||||
headers: JunoClient.getHeaders(),
|
||||
body: JSON.stringify({
|
||||
name,
|
||||
description,
|
||||
tags,
|
||||
author,
|
||||
thumbnailUrl,
|
||||
content: JSON.parse(content)
|
||||
} as IPublishNotebookRequest)
|
||||
});
|
||||
|
||||
let data: IGalleryItem;
|
||||
if (response.status === HttpStatusCodes.OK) {
|
||||
data = await response.json();
|
||||
}
|
||||
|
||||
return {
|
||||
status: response.status,
|
||||
data
|
||||
};
|
||||
}
|
||||
|
||||
public getNotebookContentUrl(id: string): string {
|
||||
return `${this.getNotebooksUrl()}/gallery/${id}/content`;
|
||||
}
|
||||
|
||||
public getNotebookInfoUrl(id: string): string {
|
||||
return `${this.getNotebooksUrl()}/gallery/${id}`;
|
||||
}
|
||||
|
||||
private async getNotebooks(input: RequestInfo, init?: RequestInit): Promise<IJunoResponse<IGalleryItem[]>> {
|
||||
const response = await window.fetch(input, init);
|
||||
|
||||
let data: IGalleryItem[];
|
||||
if (response.status === HttpStatusCodes.OK) {
|
||||
data = await response.json();
|
||||
}
|
||||
|
||||
return {
|
||||
status: response.status,
|
||||
data
|
||||
};
|
||||
}
|
||||
|
||||
private getNotebooksUrl(): string {
|
||||
return `${config.JUNO_ENDPOINT}/api/notebooks`;
|
||||
}
|
||||
|
||||
private getNotebooksAccountUrl(): string {
|
||||
return `${config.JUNO_ENDPOINT}/api/notebooks/${this.databaseAccount().name}`;
|
||||
}
|
||||
|
||||
private static getHeaders(): HeadersInit {
|
||||
const authorizationHeader = getAuthorizationHeader();
|
||||
return {
|
||||
[authorizationHeader.header]: authorizationHeader.token,
|
||||
@ -137,7 +356,7 @@ export class JunoClient {
|
||||
};
|
||||
}
|
||||
|
||||
public static getGitHubClientParams(): URLSearchParams {
|
||||
private static getGitHubClientParams(): URLSearchParams {
|
||||
const githubParams = new URLSearchParams({
|
||||
client_id: config.GITHUB_CLIENT_ID
|
||||
});
|
||||
|
@ -1,42 +1,44 @@
|
||||
import "bootstrap/dist/css/bootstrap.css";
|
||||
import { initializeIcons } from "office-ui-fabric-react/lib/Icons";
|
||||
import React from "react";
|
||||
import * as ReactDOM from "react-dom";
|
||||
import "bootstrap/dist/css/bootstrap.css";
|
||||
import { NotebookMetadata } from "../Contracts/DataModels";
|
||||
import { NotebookViewerComponent } from "../Explorer/Controls/NotebookViewer/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]);
|
||||
};
|
||||
import { initializeConfiguration } from "../Config";
|
||||
import {
|
||||
NotebookViewerComponent,
|
||||
NotebookViewerComponentProps
|
||||
} from "../Explorer/Controls/NotebookViewer/NotebookViewerComponent";
|
||||
import { IGalleryItem, JunoClient } from "../Juno/JunoClient";
|
||||
import * as GalleryUtils from "../Utils/GalleryUtils";
|
||||
|
||||
const onInit = async () => {
|
||||
var notebookMetadata: NotebookMetadata;
|
||||
const notebookMetadataString = SessionStorageUtility.getEntryString(StorageKey.NotebookMetadata);
|
||||
const notebookName = SessionStorageUtility.getEntryString(StorageKey.NotebookName);
|
||||
initializeIcons();
|
||||
await initializeConfiguration();
|
||||
const galleryViewerProps = GalleryUtils.getGalleryViewerProps(window);
|
||||
const notebookViewerProps = GalleryUtils.getNotebookViewerProps(window);
|
||||
const backNavigationText = galleryViewerProps.selectedTab && GalleryUtils.getTabTitle(galleryViewerProps.selectedTab);
|
||||
|
||||
if (notebookMetadataString == "null" || notebookMetadataString != null) {
|
||||
notebookMetadata = (await JSON.parse(notebookMetadataString)) as NotebookMetadata;
|
||||
SessionStorageUtility.removeEntry(StorageKey.NotebookMetadata);
|
||||
SessionStorageUtility.removeEntry(StorageKey.NotebookName);
|
||||
const notebookUrl = decodeURIComponent(notebookViewerProps.notebookUrl);
|
||||
render(notebookUrl, backNavigationText);
|
||||
|
||||
const galleryItemId = notebookViewerProps.galleryItemId;
|
||||
if (galleryItemId) {
|
||||
const junoClient = new JunoClient();
|
||||
const notebook = await junoClient.getNotebook(galleryItemId);
|
||||
render(notebookUrl, backNavigationText, notebook.data);
|
||||
}
|
||||
};
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const render = (notebookUrl: string, backNavigationText: string, galleryItem?: IGalleryItem) => {
|
||||
const props: NotebookViewerComponentProps = {
|
||||
junoClient: galleryItem ? new JunoClient() : undefined,
|
||||
notebookUrl,
|
||||
galleryItem,
|
||||
backNavigationText,
|
||||
onBackClick: undefined,
|
||||
onTagClick: undefined
|
||||
};
|
||||
|
||||
const notebookViewerComponent = (
|
||||
<NotebookViewerComponent
|
||||
notebookMetadata={notebookMetadata}
|
||||
notebookName={notebookName}
|
||||
notebookUrl={getNotebookUrl()}
|
||||
hideInputs={urlParams.get("hideinputs") === "true"}
|
||||
/>
|
||||
);
|
||||
ReactDOM.render(notebookViewerComponent, document.getElementById("notebookContent"));
|
||||
ReactDOM.render(<NotebookViewerComponent {...props} />, document.getElementById("notebookContent"));
|
||||
};
|
||||
|
||||
// Entry point
|
||||
|
@ -71,6 +71,5 @@ export enum StorageKey {
|
||||
TenantId,
|
||||
MostRecentActivity,
|
||||
SetPartitionKeyUndefined,
|
||||
NotebookMetadata,
|
||||
NotebookName
|
||||
GalleryCalloutDismissed
|
||||
}
|
||||
|
260
src/Utils/GalleryUtils.ts
Normal file
260
src/Utils/GalleryUtils.ts
Normal file
@ -0,0 +1,260 @@
|
||||
import { LinkProps, DialogProps } from "../Explorer/Controls/DialogReactComponent/DialogComponent";
|
||||
import { IGalleryItem, JunoClient } from "../Juno/JunoClient";
|
||||
import * as ViewModels from "../Contracts/ViewModels";
|
||||
import { NotificationConsoleUtils } from "./NotificationConsoleUtils";
|
||||
import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import * as Logger from "../Common/Logger";
|
||||
import {
|
||||
GalleryTab,
|
||||
SortBy,
|
||||
GalleryViewerComponent
|
||||
} from "../Explorer/Controls/NotebookGallery/GalleryViewerComponent";
|
||||
|
||||
export interface DialogEnabledComponent {
|
||||
setDialogProps: (dialogProps: DialogProps) => void;
|
||||
}
|
||||
|
||||
export enum NotebookViewerParams {
|
||||
NotebookUrl = "notebookUrl",
|
||||
GalleryItemId = "galleryItemId"
|
||||
}
|
||||
|
||||
export interface NotebookViewerProps {
|
||||
notebookUrl: string;
|
||||
galleryItemId: string;
|
||||
}
|
||||
|
||||
export enum GalleryViewerParams {
|
||||
SelectedTab = "tab",
|
||||
SortBy = "sort",
|
||||
SearchText = "q"
|
||||
}
|
||||
|
||||
export interface GalleryViewerProps {
|
||||
selectedTab: GalleryTab;
|
||||
sortBy: SortBy;
|
||||
searchText: string;
|
||||
}
|
||||
|
||||
export function showOkCancelModalDialog(
|
||||
component: DialogEnabledComponent,
|
||||
title: string,
|
||||
msg: string,
|
||||
linkProps: LinkProps,
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function downloadItem(
|
||||
component: DialogEnabledComponent,
|
||||
container: ViewModels.Explorer,
|
||||
junoClient: JunoClient,
|
||||
data: IGalleryItem,
|
||||
onComplete: (item: IGalleryItem) => void
|
||||
): void {
|
||||
const name = data.name;
|
||||
|
||||
if (container) {
|
||||
container.showOkCancelModalDialog(
|
||||
"Download to My Notebooks",
|
||||
`Download ${name} from gallery as a copy to your notebooks to run and/or edit the notebook.`,
|
||||
"Download",
|
||||
async () => {
|
||||
const notificationId = NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.InProgress,
|
||||
`Downloading ${name} to My Notebooks`
|
||||
);
|
||||
|
||||
try {
|
||||
const response = await junoClient.getNotebookContent(data.id);
|
||||
if (!response.data) {
|
||||
throw new Error(`Received HTTP ${response.status} when fetching ${data.name}`);
|
||||
}
|
||||
|
||||
await container.importAndOpenFromGallery(data.name, response.data);
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Info,
|
||||
`Successfully downloaded ${name} to My Notebooks`
|
||||
);
|
||||
|
||||
const increaseDownloadResponse = await junoClient.increaseNotebookDownloadCount(data.id);
|
||||
if (increaseDownloadResponse.data) {
|
||||
onComplete(increaseDownloadResponse.data);
|
||||
}
|
||||
} catch (error) {
|
||||
const message = `Failed to download ${data.name}: ${error}`;
|
||||
Logger.logError(message, "GalleryUtils/downloadItem");
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message);
|
||||
}
|
||||
|
||||
NotificationConsoleUtils.clearInProgressMessageWithId(notificationId);
|
||||
},
|
||||
"Cancel",
|
||||
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"
|
||||
},
|
||||
"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(
|
||||
container: ViewModels.Explorer,
|
||||
junoClient: JunoClient,
|
||||
data: IGalleryItem,
|
||||
onComplete: (item: IGalleryItem) => void
|
||||
): Promise<void> {
|
||||
if (container) {
|
||||
try {
|
||||
const response = await junoClient.favoriteNotebook(data.id);
|
||||
if (!response.data) {
|
||||
throw new Error(`Received HTTP ${response.status} when favoriting ${data.name}`);
|
||||
}
|
||||
|
||||
onComplete(response.data);
|
||||
} catch (error) {
|
||||
const message = `Failed to favorite ${data.name}: ${error}`;
|
||||
Logger.logError(message, "GalleryUtils/favoriteItem");
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function unfavoriteItem(
|
||||
container: ViewModels.Explorer,
|
||||
junoClient: JunoClient,
|
||||
data: IGalleryItem,
|
||||
onComplete: (item: IGalleryItem) => void
|
||||
): Promise<void> {
|
||||
if (container) {
|
||||
try {
|
||||
const response = await junoClient.unfavoriteNotebook(data.id);
|
||||
if (!response.data) {
|
||||
throw new Error(`Received HTTP ${response.status} when unfavoriting ${data.name}`);
|
||||
}
|
||||
|
||||
onComplete(response.data);
|
||||
} catch (error) {
|
||||
const message = `Failed to unfavorite ${data.name}: ${error}`;
|
||||
Logger.logError(message, "GalleryUtils/unfavoriteItem");
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function deleteItem(
|
||||
container: ViewModels.Explorer,
|
||||
junoClient: JunoClient,
|
||||
data: IGalleryItem,
|
||||
onComplete: (item: IGalleryItem) => void
|
||||
): void {
|
||||
if (container) {
|
||||
container.showOkCancelModalDialog(
|
||||
"Remove published notebook",
|
||||
`Would you like to remove ${data.name} from the gallery?`,
|
||||
"Remove",
|
||||
async () => {
|
||||
const name = data.name;
|
||||
const notificationId = NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.InProgress,
|
||||
`Removing ${name} from gallery`
|
||||
);
|
||||
|
||||
try {
|
||||
const response = await junoClient.deleteNotebook(data.id);
|
||||
if (!response.data) {
|
||||
throw new Error(`Received HTTP ${response.status} while removing ${name}`);
|
||||
}
|
||||
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, `Successfully removed ${name} from gallery`);
|
||||
onComplete(response.data);
|
||||
} catch (error) {
|
||||
const message = `Failed to remove ${name} from gallery: ${error}`;
|
||||
Logger.logError(message, "GalleryUtils/deleteItem");
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message);
|
||||
}
|
||||
|
||||
NotificationConsoleUtils.clearInProgressMessageWithId(notificationId);
|
||||
},
|
||||
"Cancel",
|
||||
undefined
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function getGalleryViewerProps(window: Window & typeof globalThis): GalleryViewerProps {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
let selectedTab: GalleryTab;
|
||||
if (params.has(GalleryViewerParams.SelectedTab)) {
|
||||
selectedTab = GalleryTab[params.get(GalleryViewerParams.SelectedTab) as keyof typeof GalleryTab];
|
||||
}
|
||||
|
||||
let sortBy: SortBy;
|
||||
if (params.has(GalleryViewerParams.SortBy)) {
|
||||
sortBy = SortBy[params.get(GalleryViewerParams.SortBy) as keyof typeof SortBy];
|
||||
}
|
||||
|
||||
return {
|
||||
selectedTab,
|
||||
sortBy,
|
||||
searchText: params.get(GalleryViewerParams.SearchText)
|
||||
};
|
||||
}
|
||||
|
||||
export function getNotebookViewerProps(window: Window & typeof globalThis): NotebookViewerProps {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
return {
|
||||
notebookUrl: params.get(NotebookViewerParams.NotebookUrl),
|
||||
galleryItemId: params.get(NotebookViewerParams.GalleryItemId)
|
||||
};
|
||||
}
|
||||
|
||||
export function getTabTitle(tab: GalleryTab): string {
|
||||
switch (tab) {
|
||||
case GalleryTab.OfficialSamples:
|
||||
return GalleryViewerComponent.OfficialSamplesTitle;
|
||||
case GalleryTab.PublicGallery:
|
||||
return GalleryViewerComponent.PublicGalleryTitle;
|
||||
case GalleryTab.Favorites:
|
||||
return GalleryViewerComponent.FavoritesTitle;
|
||||
case GalleryTab.Published:
|
||||
return GalleryViewerComponent.PublishedTitle;
|
||||
default:
|
||||
throw new Error(`Unknown tab ${tab}`);
|
||||
}
|
||||
}
|
@ -1,57 +1,8 @@
|
||||
import * as DataModels from "../Contracts/DataModels";
|
||||
import { config } from "../Config";
|
||||
import { RepoListItem } from "../Explorer/Controls/GitHub/GitHubReposComponent";
|
||||
import { IPinnedRepo } from "../Juno/JunoClient";
|
||||
import { IGitHubRepo } from "../GitHub/GitHubClient";
|
||||
import { IPinnedRepo } from "../Juno/JunoClient";
|
||||
|
||||
export class JunoUtils {
|
||||
public static async getLikedNotebooks(authorizationToken: string): Promise<DataModels.LikedNotebooksJunoResponse> {
|
||||
//TODO: Add Get method once juno has it implemented
|
||||
return {
|
||||
likedNotebooksContent: [],
|
||||
userMetadata: {
|
||||
likedNotebooks: []
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static async getOfficialSampleNotebooks(
|
||||
authorizationToken: string
|
||||
): Promise<DataModels.GitHubInfoJunoResponse[]> {
|
||||
try {
|
||||
const response = await window.fetch(config.JUNO_ENDPOINT + "/api/notebooks/galleries", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
authorization: authorizationToken
|
||||
}
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error("Status code:" + response.status);
|
||||
}
|
||||
return await response.json();
|
||||
} catch (e) {
|
||||
throw new Error("Official samples fetch failed.");
|
||||
}
|
||||
}
|
||||
|
||||
public static async updateUserMetadata(
|
||||
authorizationToken: string,
|
||||
userMetadata: DataModels.UserMetadata
|
||||
): Promise<DataModels.UserMetadata> {
|
||||
return undefined;
|
||||
//TODO: add userMetadata updation code
|
||||
// TODO: Make sure to throw error if failed
|
||||
}
|
||||
|
||||
public static async updateNotebookMetadata(
|
||||
authorizationToken: string,
|
||||
notebookMetadata: DataModels.NotebookMetadata
|
||||
): Promise<DataModels.NotebookMetadata> {
|
||||
return undefined;
|
||||
//TODO: add notebookMetadata updation code
|
||||
// TODO: Make sure to throw error if failed
|
||||
}
|
||||
|
||||
public static toPinnedRepo(item: RepoListItem): IPinnedRepo {
|
||||
return {
|
||||
owner: item.repo.owner,
|
||||
|
17
src/Utils/UserUtils.ts
Normal file
17
src/Utils/UserUtils.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import AuthHeadersUtil from "../Platform/Hosted/Authorization";
|
||||
import { decryptJWTToken } from "./AuthorizationUtils";
|
||||
import { CosmosClient } from "../Common/CosmosClient";
|
||||
|
||||
export function getFullName(): string {
|
||||
let fullName: string;
|
||||
const user = AuthHeadersUtil.getCachedUser();
|
||||
if (user) {
|
||||
fullName = user.profile.name;
|
||||
} else {
|
||||
const authToken = CosmosClient.authorizationToken();
|
||||
const props = decryptJWTToken(authToken);
|
||||
fullName = props.name;
|
||||
}
|
||||
|
||||
return fullName;
|
||||
}
|
@ -450,6 +450,10 @@
|
||||
<github-repos-pane params="{data: gitHubReposPane}"></github-repos-pane>
|
||||
<!-- /ko -->
|
||||
|
||||
<!-- ko if: isPublishNotebookPaneEnabled -->
|
||||
<div data-bind="react: publishNotebookPaneAdapter"></div>
|
||||
<!-- /ko -->
|
||||
|
||||
<!-- Global access token expiration dialog - Start -->
|
||||
<div
|
||||
id="dataAccessTokenModal"
|
||||
|
Loading…
Reference in New Issue
Block a user