mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-05-14 17:27:30 +01:00
Remove gallery.html and all associated gallery functionality
Remove the standalone gallery.html entry point, the in-app gallery tab, the publish-to-gallery pane, and all gallery-related components, utilities, and API methods that are no longer needed. Deleted: - src/GalleryViewer/ (standalone entry point) - src/Explorer/Controls/NotebookGallery/ (gallery components) - src/Explorer/Controls/Header/GalleryHeaderComponent.tsx - src/Explorer/Tabs/GalleryTab.tsx - src/Explorer/Panes/PublishNotebookPane/ (publish pane) - src/Utils/GalleryUtils.ts and tests - images/GalleryIcon.svg Edited: - webpack.config.js (removed entry point and HTML plugin) - Explorer.tsx (removed openGallery, publishNotebook methods) - ResourceTreeAdapter.tsx (removed gallery tree node and publish menu) - NotebookV2Tab.ts (removed publish-to-gallery button) - NotebookManager.tsx (removed openPublishNotebookPane) - NotebookViewerComponent.tsx (stripped gallery actions) - JunoClient.ts (removed gallery interfaces and API methods) - TelemetryConstants.ts, Constants.ts, ViewModels.ts, extractFeatures.ts, useKnockoutExplorer.ts (removed gallery constants) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -1,10 +0,0 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0)">
|
||||
<path d="M4.75 0.999903C4.98438 0.999903 5.1849 1.02334 5.35156 1.07022C5.51823 1.11709 5.66927 1.17959 5.80469 1.25772C5.9401 1.33584 6.0599 1.41657 6.16406 1.4999C6.26823 1.58324 6.3724 1.66397 6.47656 1.74209C6.58073 1.82022 6.69531 1.88011 6.82031 1.92178C6.94531 1.96344 7.08854 1.98949 7.25 1.9999H15C15.1406 1.9999 15.2708 2.02594 15.3906 2.07803C15.5104 2.13011 15.6146 2.20042 15.7031 2.28897C15.7917 2.37751 15.8646 2.48428 15.9219 2.60928C15.9792 2.73428 16.0052 2.86449 16 2.9999V6.52334L15 5.52334V2.9999H7.25C7.08854 2.9999 6.94792 3.02334 6.82812 3.07022C6.70833 3.11709 6.59375 3.17959 6.48438 3.25772C6.375 3.33584 6.26823 3.41657 6.16406 3.4999C6.0599 3.58324 5.94271 3.66397 5.8125 3.74209C5.68229 3.82022 5.53125 3.88011 5.35938 3.92178C5.1875 3.96344 4.98438 3.98949 4.75 3.9999H1V12.9999H6V13.9999H0V1.9999C0 1.85928 0.0260417 1.72907 0.078125 1.60928C0.130208 1.48949 0.200521 1.38532 0.289062 1.29678C0.377604 1.20824 0.484375 1.13532 0.609375 1.07803C0.734375 1.02074 0.864583 0.994695 1 0.999903H4.75ZM4.75 2.9999C4.875 2.9999 4.98438 2.98949 5.07812 2.96865C5.17188 2.94782 5.25781 2.91397 5.33594 2.86709C5.41406 2.82022 5.48958 2.76813 5.5625 2.71084C5.63542 2.65355 5.71875 2.58324 5.8125 2.4999C5.72396 2.42178 5.64323 2.35407 5.57031 2.29678C5.4974 2.23949 5.41927 2.1874 5.33594 2.14053C5.2526 2.09365 5.16667 2.0598 5.07812 2.03897C4.98958 2.01813 4.88021 2.00511 4.75 1.9999H1V2.9999H4.75ZM12.7109 4.9999L16 8.28897V15.9999H7V4.9999H12.7109ZM13 7.9999H14.2891L13 6.71084V7.9999ZM15 8.9999H12V5.9999H8V14.9999H15V8.9999ZM9 12.9999H13V13.9999H9V12.9999ZM11 11.9999H9V10.9999H11V11.9999ZM11 9.9999H9V8.9999H11V9.9999ZM11 7.9999H9V6.9999H11V7.9999Z" fill="#0078D4"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0">
|
||||
<rect y="-9.15527e-05" width="16" height="16" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.9 KiB |
@@ -1,9 +1,3 @@
|
||||
export class CodeOfConductEndpoints {
|
||||
public static privacyStatement: string = "https://aka.ms/ms-privacy-policy";
|
||||
public static codeOfConduct: string = "https://aka.ms/cosmos-code-of-conduct";
|
||||
public static termsOfUse: string = "https://aka.ms/ms-terms-of-use";
|
||||
}
|
||||
|
||||
export class EndpointsRegex {
|
||||
public static readonly cassandra = [
|
||||
"AccountEndpoint=(.*).cassandra.cosmosdb.azure.com",
|
||||
@@ -118,7 +112,6 @@ export class Flights {
|
||||
public static readonly PhoenixNotebooks = "phoenixnotebooks";
|
||||
public static readonly PhoenixFeatures = "phoenixfeatures";
|
||||
public static readonly NotebooksDownBanner = "notebooksdownbanner";
|
||||
public static readonly PublicGallery = "publicgallery";
|
||||
}
|
||||
|
||||
export class AfecFeatures {
|
||||
|
||||
@@ -393,7 +393,7 @@ export enum CollectionTabKind {
|
||||
Terminal = 14,
|
||||
NotebookV2 = 15,
|
||||
SparkMasterTab = 16 /* Deprecated */,
|
||||
Gallery = 17,
|
||||
Gallery = 17 /* Deprecated */,
|
||||
NotebookViewer = 18,
|
||||
Schema = 19,
|
||||
CollectionSettingsV2 = 20,
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
import { CommandButton, FontIcon, FontWeights, ITextProps, Separator, Stack, Text } from "@fluentui/react";
|
||||
import * as React from "react";
|
||||
|
||||
export class GalleryHeaderComponent extends React.Component {
|
||||
private static readonly azureText = "Microsoft Azure";
|
||||
private static readonly cosmosdbText = "Cosmos DB";
|
||||
private static readonly galleryText = "Gallery";
|
||||
private static readonly loginText = "Sign In";
|
||||
private static readonly openPortal = () => window.open("https://portal.azure.com", "_blank");
|
||||
private static readonly openDataExplorer = () => (window.location.href = new URL("./", window.location.href).href);
|
||||
private static readonly headerItemStyle: React.CSSProperties = {
|
||||
color: "white",
|
||||
};
|
||||
private static readonly mainHeaderTextProps: ITextProps = {
|
||||
style: GalleryHeaderComponent.headerItemStyle,
|
||||
variant: "mediumPlus",
|
||||
styles: {
|
||||
root: {
|
||||
fontWeight: FontWeights.semibold,
|
||||
},
|
||||
},
|
||||
};
|
||||
private static readonly headerItemTextProps: ITextProps = { style: GalleryHeaderComponent.headerItemStyle };
|
||||
|
||||
private renderHeaderItem = (text: string, onClick: () => void, textProps: ITextProps): JSX.Element => {
|
||||
return (
|
||||
<CommandButton onClick={onClick} ariaLabel={text}>
|
||||
<Text {...textProps}>{text}</Text>
|
||||
</CommandButton>
|
||||
);
|
||||
};
|
||||
|
||||
public render(): JSX.Element {
|
||||
return (
|
||||
<Stack
|
||||
tokens={{ childrenGap: 10 }}
|
||||
horizontal
|
||||
styles={{ root: { background: "#0078d4", paddingLeft: 20, paddingRight: 20 } }}
|
||||
verticalAlign="center"
|
||||
>
|
||||
<Stack.Item>
|
||||
{this.renderHeaderItem(
|
||||
GalleryHeaderComponent.azureText,
|
||||
GalleryHeaderComponent.openPortal,
|
||||
GalleryHeaderComponent.mainHeaderTextProps,
|
||||
)}
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Separator vertical />
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
{this.renderHeaderItem(
|
||||
GalleryHeaderComponent.cosmosdbText,
|
||||
GalleryHeaderComponent.openDataExplorer,
|
||||
GalleryHeaderComponent.headerItemTextProps,
|
||||
)}
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<FontIcon style={GalleryHeaderComponent.headerItemStyle} iconName="ChevronRight" />
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
{this.renderHeaderItem(
|
||||
GalleryHeaderComponent.galleryText,
|
||||
() => "",
|
||||
GalleryHeaderComponent.headerItemTextProps,
|
||||
)}
|
||||
</Stack.Item>
|
||||
<Stack.Item grow>
|
||||
<></>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
{this.renderHeaderItem(
|
||||
GalleryHeaderComponent.loginText,
|
||||
GalleryHeaderComponent.openDataExplorer,
|
||||
GalleryHeaderComponent.headerItemTextProps,
|
||||
)}
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import { GalleryCardComponent, GalleryCardComponentProps } from "./GalleryCardComponent";
|
||||
|
||||
describe("GalleryCardComponent", () => {
|
||||
it("renders", () => {
|
||||
const props: GalleryCardComponentProps = {
|
||||
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,
|
||||
newCellId: undefined,
|
||||
policyViolations: undefined,
|
||||
pendingScanJobIds: undefined,
|
||||
},
|
||||
isFavorite: false,
|
||||
showDownload: true,
|
||||
showDelete: true,
|
||||
onClick: undefined,
|
||||
onTagClick: undefined,
|
||||
onFavoriteClick: undefined,
|
||||
onUnfavoriteClick: undefined,
|
||||
onDownloadClick: undefined,
|
||||
onDeleteClick: undefined,
|
||||
};
|
||||
|
||||
const wrapper = shallow(<GalleryCardComponent {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -1,205 +0,0 @@
|
||||
import {
|
||||
BaseButton,
|
||||
Button,
|
||||
DocumentCard,
|
||||
DocumentCardActivity,
|
||||
DocumentCardDetails,
|
||||
DocumentCardPreview,
|
||||
DocumentCardTitle,
|
||||
Icon,
|
||||
IconButton,
|
||||
IDocumentCardPreviewProps,
|
||||
IDocumentCardStyles,
|
||||
ImageFit,
|
||||
Link,
|
||||
Separator,
|
||||
Spinner,
|
||||
SpinnerSize,
|
||||
Text,
|
||||
TooltipHost,
|
||||
} from "@fluentui/react";
|
||||
import React, { FunctionComponent, useState } from "react";
|
||||
import CosmosDBLogo from "../../../../../images/CosmosDB-logo.svg";
|
||||
import { IGalleryItem } from "../../../../Juno/JunoClient";
|
||||
import * as FileSystemUtil from "../../../Notebook/FileSystemUtil";
|
||||
|
||||
export interface GalleryCardComponentProps {
|
||||
data: IGalleryItem;
|
||||
isFavorite: boolean;
|
||||
showDownload: boolean;
|
||||
showDelete: boolean;
|
||||
onClick: () => void;
|
||||
onTagClick: (tag: string) => void;
|
||||
onFavoriteClick: () => void;
|
||||
onUnfavoriteClick: () => void;
|
||||
onDownloadClick: () => void;
|
||||
onDeleteClick: (beforeDelete: () => void, afterDelete: () => void) => void;
|
||||
}
|
||||
|
||||
export const GalleryCardComponent: FunctionComponent<GalleryCardComponentProps> = ({
|
||||
data,
|
||||
isFavorite,
|
||||
showDownload,
|
||||
showDelete,
|
||||
onClick,
|
||||
onTagClick,
|
||||
onFavoriteClick,
|
||||
onUnfavoriteClick,
|
||||
onDownloadClick,
|
||||
onDeleteClick,
|
||||
}: GalleryCardComponentProps) => {
|
||||
const CARD_WIDTH = 256;
|
||||
const cardImageHeight = 144;
|
||||
const cardDescriptionMaxChars = 80;
|
||||
const cardItemGapSmall = 8;
|
||||
const cardDeleteSpinnerHeight = 360;
|
||||
const smallTextLineHeight = 18;
|
||||
|
||||
const [isDeletingPublishedNotebook, setIsDeletingPublishedNotebook] = useState<boolean>(false);
|
||||
|
||||
const cardButtonsVisible = isFavorite !== undefined || showDownload || showDelete;
|
||||
const options: Intl.DateTimeFormatOptions = {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
};
|
||||
const dateString = new Date(data.created).toLocaleString("default", options);
|
||||
const cardTitle = FileSystemUtil.stripExtension(data.name, "ipynb");
|
||||
|
||||
const renderTruncated = (text: string, totalLength: number): string => {
|
||||
let truncatedDescription = text.substr(0, totalLength);
|
||||
if (text.length > totalLength) {
|
||||
truncatedDescription = `${truncatedDescription} ...`;
|
||||
}
|
||||
return truncatedDescription;
|
||||
};
|
||||
|
||||
const generateIconText = (iconName: string, text: string): JSX.Element => {
|
||||
return (
|
||||
<Text variant="tiny" styles={{ root: { color: "#605E5C", paddingRight: cardItemGapSmall } }}>
|
||||
<Icon iconName={iconName} styles={{ root: { verticalAlign: "middle" } }} /> {text}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
/*
|
||||
* 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)
|
||||
*/
|
||||
const generateIconButtonWithTooltip = (
|
||||
iconName: string,
|
||||
title: string,
|
||||
horizontalAlign: "right" | "left",
|
||||
activate: () => void,
|
||||
): JSX.Element => {
|
||||
return (
|
||||
<TooltipHost
|
||||
content={title}
|
||||
id={`TooltipHost-IconButton-${iconName}`}
|
||||
calloutProps={{ gapSpace: 0 }}
|
||||
styles={{ root: { display: "inline-block", float: horizontalAlign } }}
|
||||
>
|
||||
<IconButton
|
||||
iconProps={{ iconName }}
|
||||
title={title}
|
||||
ariaLabel={title}
|
||||
onClick={(event) => handlerOnClick(event, activate)}
|
||||
/>
|
||||
</TooltipHost>
|
||||
);
|
||||
};
|
||||
|
||||
const handlerOnClick = (
|
||||
event:
|
||||
| React.MouseEvent<HTMLElement | HTMLAnchorElement | HTMLButtonElement | MouseEvent>
|
||||
| React.MouseEvent<
|
||||
HTMLAnchorElement | HTMLButtonElement | HTMLDivElement | BaseButton | Button | HTMLSpanElement,
|
||||
MouseEvent
|
||||
>,
|
||||
activate: () => void,
|
||||
): void => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
activate();
|
||||
};
|
||||
const DocumentCardActivityPeople = [{ name: data.author, profileImageSrc: data.isSample && CosmosDBLogo }];
|
||||
const previewProps: IDocumentCardPreviewProps = {
|
||||
previewImages: [
|
||||
{
|
||||
previewImageSrc: data.thumbnailUrl,
|
||||
imageFit: ImageFit.cover,
|
||||
width: CARD_WIDTH,
|
||||
height: cardImageHeight,
|
||||
},
|
||||
],
|
||||
};
|
||||
const cardStyles: IDocumentCardStyles = {
|
||||
root: { display: "inline-block", marginRight: 20, width: CARD_WIDTH },
|
||||
};
|
||||
return (
|
||||
<DocumentCard aria-label={cardTitle} styles={cardStyles} onClick={onClick}>
|
||||
{isDeletingPublishedNotebook && (
|
||||
<Spinner
|
||||
size={SpinnerSize.large}
|
||||
label={`Deleting '${cardTitle}'`}
|
||||
styles={{ root: { height: cardDeleteSpinnerHeight } }}
|
||||
/>
|
||||
)}
|
||||
{!isDeletingPublishedNotebook && (
|
||||
<>
|
||||
<DocumentCardActivity activity={dateString} people={DocumentCardActivityPeople} />
|
||||
<DocumentCardPreview {...previewProps} />
|
||||
<DocumentCardDetails>
|
||||
<Text variant="small" nowrap styles={{ root: { height: smallTextLineHeight, padding: "2px 16px" } }}>
|
||||
{data.tags ? (
|
||||
data.tags.map((tag, index, array) => (
|
||||
<span key={tag}>
|
||||
<Link onClick={(event) => handlerOnClick(event, () => onTagClick(tag))}>{tag}</Link>
|
||||
{index === array.length - 1 ? <></> : ", "}
|
||||
</span>
|
||||
))
|
||||
) : (
|
||||
<br />
|
||||
)}
|
||||
</Text>
|
||||
<DocumentCardTitle title={renderTruncated(cardTitle, 20)} shouldTruncate />
|
||||
<DocumentCardTitle
|
||||
title={renderTruncated(data.description, cardDescriptionMaxChars)}
|
||||
showAsSecondaryTitle
|
||||
/>
|
||||
<span style={{ padding: "8px 16px" }}>
|
||||
{data.views !== undefined && generateIconText("RedEye", data.views.toString())}
|
||||
{data.downloads !== undefined && generateIconText("Download", data.downloads.toString())}
|
||||
{data.favorites !== undefined && generateIconText("Heart", data.favorites.toString())}
|
||||
</span>
|
||||
</DocumentCardDetails>
|
||||
{cardButtonsVisible && (
|
||||
<DocumentCardDetails>
|
||||
<Separator styles={{ root: { padding: 0, height: 1 } }} />
|
||||
|
||||
<span style={{ padding: "0px 16px" }}>
|
||||
{isFavorite !== undefined &&
|
||||
generateIconButtonWithTooltip(
|
||||
isFavorite ? "HeartFill" : "Heart",
|
||||
isFavorite ? "Unfavorite" : "Favorite",
|
||||
"left",
|
||||
isFavorite ? onUnfavoriteClick : onFavoriteClick,
|
||||
)}
|
||||
|
||||
{showDownload && generateIconButtonWithTooltip("Download", "Download", "left", onDownloadClick)}
|
||||
|
||||
{showDelete &&
|
||||
generateIconButtonWithTooltip("Delete", "Remove", "right", () =>
|
||||
onDeleteClick(
|
||||
() => setIsDeletingPublishedNotebook(true),
|
||||
() => setIsDeletingPublishedNotebook(false),
|
||||
),
|
||||
)}
|
||||
</span>
|
||||
</DocumentCardDetails>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</DocumentCard>
|
||||
);
|
||||
};
|
||||
-256
@@ -1,256 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`GalleryCardComponent renders 1`] = `
|
||||
<StyledDocumentCardBase
|
||||
aria-label="name"
|
||||
styles={
|
||||
{
|
||||
"root": {
|
||||
"display": "inline-block",
|
||||
"marginRight": 20,
|
||||
"width": 256,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<StyledDocumentCardActivityBase
|
||||
activity="Invalid Date"
|
||||
people={
|
||||
[
|
||||
{
|
||||
"name": "author",
|
||||
"profileImageSrc": false,
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
<StyledDocumentCardPreviewBase
|
||||
previewImages={
|
||||
[
|
||||
{
|
||||
"height": 144,
|
||||
"imageFit": 2,
|
||||
"previewImageSrc": "thumbnailUrl",
|
||||
"width": 256,
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
<StyledDocumentCardDetailsBase>
|
||||
<Text
|
||||
nowrap={true}
|
||||
styles={
|
||||
{
|
||||
"root": {
|
||||
"height": 18,
|
||||
"padding": "2px 16px",
|
||||
},
|
||||
}
|
||||
}
|
||||
variant="small"
|
||||
>
|
||||
<span
|
||||
key="tag"
|
||||
>
|
||||
<StyledLinkBase
|
||||
onClick={[Function]}
|
||||
>
|
||||
tag
|
||||
</StyledLinkBase>
|
||||
</span>
|
||||
</Text>
|
||||
<StyledDocumentCardTitleBase
|
||||
shouldTruncate={true}
|
||||
title="name"
|
||||
/>
|
||||
<StyledDocumentCardTitleBase
|
||||
showAsSecondaryTitle={true}
|
||||
title="description"
|
||||
/>
|
||||
<span
|
||||
style={
|
||||
{
|
||||
"padding": "8px 16px",
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
styles={
|
||||
{
|
||||
"root": {
|
||||
"color": "#605E5C",
|
||||
"paddingRight": 8,
|
||||
},
|
||||
}
|
||||
}
|
||||
variant="tiny"
|
||||
>
|
||||
<Icon
|
||||
iconName="RedEye"
|
||||
styles={
|
||||
{
|
||||
"root": {
|
||||
"verticalAlign": "middle",
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
|
||||
0
|
||||
</Text>
|
||||
<Text
|
||||
styles={
|
||||
{
|
||||
"root": {
|
||||
"color": "#605E5C",
|
||||
"paddingRight": 8,
|
||||
},
|
||||
}
|
||||
}
|
||||
variant="tiny"
|
||||
>
|
||||
<Icon
|
||||
iconName="Download"
|
||||
styles={
|
||||
{
|
||||
"root": {
|
||||
"verticalAlign": "middle",
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
|
||||
0
|
||||
</Text>
|
||||
<Text
|
||||
styles={
|
||||
{
|
||||
"root": {
|
||||
"color": "#605E5C",
|
||||
"paddingRight": 8,
|
||||
},
|
||||
}
|
||||
}
|
||||
variant="tiny"
|
||||
>
|
||||
<Icon
|
||||
iconName="Heart"
|
||||
styles={
|
||||
{
|
||||
"root": {
|
||||
"verticalAlign": "middle",
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
|
||||
0
|
||||
</Text>
|
||||
</span>
|
||||
</StyledDocumentCardDetailsBase>
|
||||
<StyledDocumentCardDetailsBase>
|
||||
<Separator
|
||||
styles={
|
||||
{
|
||||
"root": {
|
||||
"height": 1,
|
||||
"padding": 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
<span
|
||||
style={
|
||||
{
|
||||
"padding": "0px 16px",
|
||||
}
|
||||
}
|
||||
>
|
||||
<StyledTooltipHostBase
|
||||
calloutProps={
|
||||
{
|
||||
"gapSpace": 0,
|
||||
}
|
||||
}
|
||||
content="Favorite"
|
||||
id="TooltipHost-IconButton-Heart"
|
||||
styles={
|
||||
{
|
||||
"root": {
|
||||
"display": "inline-block",
|
||||
"float": "left",
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<CustomizedIconButton
|
||||
ariaLabel="Favorite"
|
||||
iconProps={
|
||||
{
|
||||
"iconName": "Heart",
|
||||
}
|
||||
}
|
||||
onClick={[Function]}
|
||||
title="Favorite"
|
||||
/>
|
||||
</StyledTooltipHostBase>
|
||||
<StyledTooltipHostBase
|
||||
calloutProps={
|
||||
{
|
||||
"gapSpace": 0,
|
||||
}
|
||||
}
|
||||
content="Download"
|
||||
id="TooltipHost-IconButton-Download"
|
||||
styles={
|
||||
{
|
||||
"root": {
|
||||
"display": "inline-block",
|
||||
"float": "left",
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<CustomizedIconButton
|
||||
ariaLabel="Download"
|
||||
iconProps={
|
||||
{
|
||||
"iconName": "Download",
|
||||
}
|
||||
}
|
||||
onClick={[Function]}
|
||||
title="Download"
|
||||
/>
|
||||
</StyledTooltipHostBase>
|
||||
<StyledTooltipHostBase
|
||||
calloutProps={
|
||||
{
|
||||
"gapSpace": 0,
|
||||
}
|
||||
}
|
||||
content="Remove"
|
||||
id="TooltipHost-IconButton-Delete"
|
||||
styles={
|
||||
{
|
||||
"root": {
|
||||
"display": "inline-block",
|
||||
"float": "right",
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<CustomizedIconButton
|
||||
ariaLabel="Remove"
|
||||
iconProps={
|
||||
{
|
||||
"iconName": "Delete",
|
||||
}
|
||||
}
|
||||
onClick={[Function]}
|
||||
title="Remove"
|
||||
/>
|
||||
</StyledTooltipHostBase>
|
||||
</span>
|
||||
</StyledDocumentCardDetailsBase>
|
||||
</StyledDocumentCardBase>
|
||||
`;
|
||||
@@ -1,34 +0,0 @@
|
||||
jest.mock("../../../../Juno/JunoClient");
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import { HttpStatusCodes } from "../../../../Common/Constants";
|
||||
import { JunoClient } from "../../../../Juno/JunoClient";
|
||||
import { CodeOfConduct, CodeOfConductProps } from "./CodeOfConduct";
|
||||
|
||||
describe("CodeOfConduct", () => {
|
||||
let codeOfConductProps: CodeOfConductProps;
|
||||
|
||||
beforeEach(() => {
|
||||
const junoClient = new JunoClient();
|
||||
junoClient.acceptCodeOfConduct = jest.fn().mockReturnValue({
|
||||
status: HttpStatusCodes.OK,
|
||||
data: true,
|
||||
});
|
||||
codeOfConductProps = {
|
||||
junoClient: junoClient,
|
||||
onAcceptCodeOfConduct: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
const wrapper = shallow(<CodeOfConduct {...codeOfConductProps} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("onAcceptedCodeOfConductCalled", async () => {
|
||||
const wrapper = shallow(<CodeOfConduct {...codeOfConductProps} />);
|
||||
wrapper.find(".genericPaneSubmitBtn").first().simulate("click");
|
||||
await Promise.resolve();
|
||||
expect(codeOfConductProps.onAcceptCodeOfConduct).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,110 +0,0 @@
|
||||
import { Checkbox, Link, PrimaryButton, Stack, Text } from "@fluentui/react";
|
||||
import React, { FunctionComponent, useEffect, useState } from "react";
|
||||
import { CodeOfConductEndpoints, HttpStatusCodes } from "../../../../Common/Constants";
|
||||
import { getErrorMessage, getErrorStack, handleError } from "../../../../Common/ErrorHandlingUtils";
|
||||
import { JunoClient } from "../../../../Juno/JunoClient";
|
||||
import { Action } from "../../../../Shared/Telemetry/TelemetryConstants";
|
||||
import { trace, traceFailure, traceStart, traceSuccess } from "../../../../Shared/Telemetry/TelemetryProcessor";
|
||||
|
||||
export interface CodeOfConductProps {
|
||||
junoClient: JunoClient;
|
||||
onAcceptCodeOfConduct: (result: boolean) => void;
|
||||
}
|
||||
|
||||
export const CodeOfConduct: FunctionComponent<CodeOfConductProps> = ({
|
||||
junoClient,
|
||||
onAcceptCodeOfConduct,
|
||||
}: CodeOfConductProps) => {
|
||||
const descriptionPara1 = "Azure Cosmos DB Notebook Gallery - Code of Conduct";
|
||||
const descriptionPara2 = "The notebook public gallery contains notebook samples shared by users of Azure Cosmos DB.";
|
||||
const descriptionPara3 = "In order to view and publish your samples to the gallery, you must accept the ";
|
||||
const link1: { label: string; url: string } = {
|
||||
label: "code of conduct.",
|
||||
url: CodeOfConductEndpoints.codeOfConduct,
|
||||
};
|
||||
|
||||
const [readCodeOfConduct, setReadCodeOfConduct] = useState<boolean>(false);
|
||||
|
||||
const acceptCodeOfConduct = async (): Promise<void> => {
|
||||
const startKey = traceStart(Action.NotebooksGalleryAcceptCodeOfConduct);
|
||||
|
||||
try {
|
||||
const response = await junoClient.acceptCodeOfConduct();
|
||||
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
|
||||
throw new Error(`Received HTTP ${response.status} when accepting code of conduct`);
|
||||
}
|
||||
|
||||
traceSuccess(Action.NotebooksGalleryAcceptCodeOfConduct, {}, startKey);
|
||||
|
||||
onAcceptCodeOfConduct(response.data);
|
||||
} catch (error) {
|
||||
traceFailure(
|
||||
Action.NotebooksGalleryAcceptCodeOfConduct,
|
||||
{
|
||||
error: getErrorMessage(error),
|
||||
errorStack: getErrorStack(error),
|
||||
},
|
||||
startKey,
|
||||
);
|
||||
|
||||
handleError(error, "CodeOfConduct/acceptCodeOfConduct", "Failed to accept code of conduct");
|
||||
}
|
||||
};
|
||||
|
||||
const onChangeCheckbox = (): void => {
|
||||
setReadCodeOfConduct(!readCodeOfConduct);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
trace(Action.NotebooksGalleryViewCodeOfConduct);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Stack tokens={{ childrenGap: 20 }}>
|
||||
<Stack.Item>
|
||||
<Text style={{ fontWeight: 500, fontSize: "20px" }}>{descriptionPara1}</Text>
|
||||
</Stack.Item>
|
||||
|
||||
<Stack.Item>
|
||||
<Text>{descriptionPara2}</Text>
|
||||
</Stack.Item>
|
||||
|
||||
<Stack.Item>
|
||||
<Text>
|
||||
{descriptionPara3}
|
||||
<Link href={link1.url} target="_blank">
|
||||
{link1.label}
|
||||
</Link>
|
||||
</Text>
|
||||
</Stack.Item>
|
||||
|
||||
<Stack.Item>
|
||||
<Checkbox
|
||||
styles={{
|
||||
label: {
|
||||
margin: 0,
|
||||
padding: "2 0 2 0",
|
||||
},
|
||||
text: {
|
||||
fontSize: 12,
|
||||
},
|
||||
}}
|
||||
label="I have read and accept the code of conduct."
|
||||
onChange={onChangeCheckbox}
|
||||
/>
|
||||
</Stack.Item>
|
||||
|
||||
<Stack.Item>
|
||||
<PrimaryButton
|
||||
ariaLabel="Continue"
|
||||
title="Continue"
|
||||
onClick={async () => await acceptCodeOfConduct()}
|
||||
tabIndex={0}
|
||||
className="genericPaneSubmitBtn"
|
||||
text="Continue"
|
||||
disabled={!readCodeOfConduct}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
-68
@@ -1,68 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CodeOfConduct renders 1`] = `
|
||||
<Stack
|
||||
tokens={
|
||||
{
|
||||
"childrenGap": 20,
|
||||
}
|
||||
}
|
||||
>
|
||||
<StackItem>
|
||||
<Text
|
||||
style={
|
||||
{
|
||||
"fontSize": "20px",
|
||||
"fontWeight": 500,
|
||||
}
|
||||
}
|
||||
>
|
||||
Azure Cosmos DB Notebook Gallery - Code of Conduct
|
||||
</Text>
|
||||
</StackItem>
|
||||
<StackItem>
|
||||
<Text>
|
||||
The notebook public gallery contains notebook samples shared by users of Azure Cosmos DB.
|
||||
</Text>
|
||||
</StackItem>
|
||||
<StackItem>
|
||||
<Text>
|
||||
In order to view and publish your samples to the gallery, you must accept the
|
||||
<StyledLinkBase
|
||||
href="https://aka.ms/cosmos-code-of-conduct"
|
||||
target="_blank"
|
||||
>
|
||||
code of conduct.
|
||||
</StyledLinkBase>
|
||||
</Text>
|
||||
</StackItem>
|
||||
<StackItem>
|
||||
<StyledCheckboxBase
|
||||
label="I have read and accept the code of conduct."
|
||||
onChange={[Function]}
|
||||
styles={
|
||||
{
|
||||
"label": {
|
||||
"margin": 0,
|
||||
"padding": "2 0 2 0",
|
||||
},
|
||||
"text": {
|
||||
"fontSize": 12,
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
</StackItem>
|
||||
<StackItem>
|
||||
<CustomizedPrimaryButton
|
||||
ariaLabel="Continue"
|
||||
className="genericPaneSubmitBtn"
|
||||
disabled={true}
|
||||
onClick={[Function]}
|
||||
tabIndex={0}
|
||||
text="Continue"
|
||||
title="Continue"
|
||||
/>
|
||||
</StackItem>
|
||||
</Stack>
|
||||
`;
|
||||
@@ -1,114 +0,0 @@
|
||||
import * as React from "react";
|
||||
import { JunoClient, IGalleryItem } from "../../../Juno/JunoClient";
|
||||
import { GalleryTab, SortBy, GalleryViewerComponentProps, GalleryViewerComponent } from "./GalleryViewerComponent";
|
||||
import { NotebookViewerComponentProps, NotebookViewerComponent } from "../NotebookViewer/NotebookViewerComponent";
|
||||
import * as GalleryUtils from "../../../Utils/GalleryUtils";
|
||||
import Explorer from "../../Explorer";
|
||||
|
||||
export interface GalleryAndNotebookViewerComponentProps {
|
||||
container?: Explorer;
|
||||
junoClient: JunoClient;
|
||||
notebookUrl?: string;
|
||||
galleryItem?: IGalleryItem;
|
||||
isFavorite?: boolean;
|
||||
selectedTab: GalleryTab;
|
||||
sortBy: SortBy;
|
||||
searchText: string;
|
||||
}
|
||||
|
||||
interface GalleryAndNotebookViewerComponentState {
|
||||
notebookUrl: string;
|
||||
galleryItem: IGalleryItem;
|
||||
isFavorite: boolean;
|
||||
selectedTab: GalleryTab;
|
||||
sortBy: SortBy;
|
||||
searchText: string;
|
||||
}
|
||||
|
||||
export class GalleryAndNotebookViewerComponent extends React.Component<
|
||||
GalleryAndNotebookViewerComponentProps,
|
||||
GalleryAndNotebookViewerComponentState
|
||||
> {
|
||||
constructor(props: GalleryAndNotebookViewerComponentProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
notebookUrl: props.notebookUrl,
|
||||
galleryItem: props.galleryItem,
|
||||
isFavorite: props.isFavorite,
|
||||
selectedTab: props.selectedTab,
|
||||
sortBy: props.sortBy,
|
||||
searchText: props.searchText,
|
||||
};
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
if (this.state.notebookUrl) {
|
||||
const props: NotebookViewerComponentProps = {
|
||||
container: this.props.container,
|
||||
junoClient: this.props.junoClient,
|
||||
notebookUrl: this.state.notebookUrl,
|
||||
galleryItem: this.state.galleryItem,
|
||||
isFavorite: this.state.isFavorite,
|
||||
backNavigationText: GalleryUtils.getTabTitle(this.state.selectedTab),
|
||||
onBackClick: this.onBackClick,
|
||||
onTagClick: this.loadTaggedItems,
|
||||
};
|
||||
|
||||
return <NotebookViewerComponent {...props} />;
|
||||
}
|
||||
|
||||
const props: GalleryViewerComponentProps = {
|
||||
container: this.props.container,
|
||||
junoClient: this.props.junoClient,
|
||||
selectedTab: this.state.selectedTab,
|
||||
sortBy: this.state.sortBy,
|
||||
searchText: this.state.searchText,
|
||||
openNotebook: this.openNotebook,
|
||||
onSelectedTabChange: this.onSelectedTabChange,
|
||||
onSortByChange: this.onSortByChange,
|
||||
onSearchTextChange: this.onSearchTextChange,
|
||||
};
|
||||
|
||||
return <GalleryViewerComponent {...props} />;
|
||||
}
|
||||
|
||||
private onBackClick = (): void => {
|
||||
this.setState({
|
||||
notebookUrl: undefined,
|
||||
});
|
||||
};
|
||||
|
||||
private loadTaggedItems = (tag: string): void => {
|
||||
this.setState({
|
||||
notebookUrl: undefined,
|
||||
searchText: tag,
|
||||
});
|
||||
};
|
||||
|
||||
private openNotebook = (data: IGalleryItem, isFavorite: boolean): void => {
|
||||
this.setState({
|
||||
notebookUrl: this.props.junoClient.getNotebookContentUrl(data.id),
|
||||
galleryItem: data,
|
||||
isFavorite,
|
||||
});
|
||||
};
|
||||
|
||||
private onSelectedTabChange = (selectedTab: GalleryTab): void => {
|
||||
this.setState({
|
||||
selectedTab,
|
||||
});
|
||||
};
|
||||
|
||||
private onSortByChange = (sortBy: SortBy): void => {
|
||||
this.setState({
|
||||
sortBy,
|
||||
});
|
||||
};
|
||||
|
||||
private onSearchTextChange = (searchText: string): void => {
|
||||
this.setState({
|
||||
searchText,
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
@import "../../../../less/Common/Constants";
|
||||
|
||||
.galleryContainer {
|
||||
padding: @LargeSpace @LargeSpace 30px @LargeSpace;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
width: 100%;
|
||||
font-family: @DataExplorerFont;
|
||||
background: @GalleryBackgroundColor;
|
||||
}
|
||||
|
||||
.publicGalleryTabContainer {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.publicGalleryTabOverlayContent {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
margin: 10%;
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import { GalleryViewerComponent, GalleryViewerComponentProps, GalleryTab, SortBy } from "./GalleryViewerComponent";
|
||||
|
||||
describe("GalleryViewerComponent", () => {
|
||||
it("renders", () => {
|
||||
const props: GalleryViewerComponentProps = {
|
||||
junoClient: undefined,
|
||||
selectedTab: GalleryTab.OfficialSamples,
|
||||
sortBy: SortBy.MostViewed,
|
||||
searchText: undefined,
|
||||
openNotebook: undefined,
|
||||
onSelectedTabChange: undefined,
|
||||
onSortByChange: undefined,
|
||||
onSearchTextChange: undefined,
|
||||
};
|
||||
|
||||
const wrapper = shallow(<GalleryViewerComponent {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -1,760 +0,0 @@
|
||||
import {
|
||||
Dropdown,
|
||||
FocusZone,
|
||||
FontIcon,
|
||||
FontWeights,
|
||||
IDropdownOption,
|
||||
IPageSpecification,
|
||||
IPivotItemProps,
|
||||
IPivotProps,
|
||||
IRectangle,
|
||||
Label,
|
||||
Link,
|
||||
List,
|
||||
Overlay,
|
||||
Pivot,
|
||||
PivotItem,
|
||||
SearchBox,
|
||||
Spinner,
|
||||
SpinnerSize,
|
||||
Stack,
|
||||
Text,
|
||||
} from "@fluentui/react";
|
||||
import * as React from "react";
|
||||
import { userContext } from "UserContext";
|
||||
import { HttpStatusCodes } from "../../../Common/Constants";
|
||||
import { handleError } from "../../../Common/ErrorHandlingUtils";
|
||||
import { IGalleryItem, IJunoResponse, IPublicGalleryData, JunoClient } from "../../../Juno/JunoClient";
|
||||
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import { trace } from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import * as GalleryUtils from "../../../Utils/GalleryUtils";
|
||||
import Explorer from "../../Explorer";
|
||||
import { GalleryCardComponent, GalleryCardComponentProps } from "./Cards/GalleryCardComponent";
|
||||
import { CodeOfConduct } from "./CodeOfConduct/CodeOfConduct";
|
||||
import "./GalleryViewerComponent.less";
|
||||
import { InfoComponent } from "./InfoComponent/InfoComponent";
|
||||
|
||||
export interface GalleryViewerComponentProps {
|
||||
container?: Explorer;
|
||||
junoClient: JunoClient;
|
||||
selectedTab: GalleryTab;
|
||||
sortBy: SortBy;
|
||||
searchText: string;
|
||||
openNotebook: (data: IGalleryItem, isFavorite: boolean) => void;
|
||||
onSelectedTabChange: (newTab: GalleryTab) => void;
|
||||
onSortByChange: (sortBy: SortBy) => void;
|
||||
onSearchTextChange: (searchText: string) => void;
|
||||
}
|
||||
|
||||
export enum GalleryTab {
|
||||
PublicGallery,
|
||||
OfficialSamples,
|
||||
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;
|
||||
isCodeOfConductAccepted: boolean;
|
||||
isFetchingPublishedNotebooks: boolean;
|
||||
isFetchingFavouriteNotebooks: boolean;
|
||||
}
|
||||
|
||||
interface GalleryTabInfo {
|
||||
tab: GalleryTab;
|
||||
content: JSX.Element;
|
||||
}
|
||||
|
||||
export class GalleryViewerComponent extends React.Component<GalleryViewerComponentProps, GalleryViewerComponentState> {
|
||||
public static readonly OfficialSamplesTitle = "Official samples";
|
||||
public static readonly PublicGalleryTitle = "Public gallery";
|
||||
public static readonly FavoritesTitle = "My favorites";
|
||||
public static readonly PublishedTitle = "My published work";
|
||||
|
||||
private static readonly rowsPerPage = 5;
|
||||
private static readonly CARD_WIDTH = 256;
|
||||
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 readonly sortingOptions: IDropdownOption[];
|
||||
|
||||
private viewGalleryTraced: boolean;
|
||||
private viewOfficialSamplesTraced: boolean;
|
||||
private viewPublicGalleryTraced: boolean;
|
||||
private viewFavoritesTraced: boolean;
|
||||
private viewPublishedNotebooksTraced: boolean;
|
||||
|
||||
private sampleNotebooks: IGalleryItem[];
|
||||
private publicNotebooks: IGalleryItem[];
|
||||
private favoriteNotebooks: IGalleryItem[];
|
||||
private publishedNotebooks: IGalleryItem[];
|
||||
private isCodeOfConductAccepted: boolean;
|
||||
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,
|
||||
isCodeOfConductAccepted: undefined,
|
||||
isFetchingFavouriteNotebooks: true,
|
||||
isFetchingPublishedNotebooks: true,
|
||||
};
|
||||
|
||||
this.sortingOptions = [
|
||||
{
|
||||
key: SortBy.MostViewed,
|
||||
text: GalleryViewerComponent.mostViewedText,
|
||||
},
|
||||
{
|
||||
key: SortBy.MostDownloaded,
|
||||
text: GalleryViewerComponent.mostDownloadedText,
|
||||
},
|
||||
{
|
||||
key: SortBy.MostRecent,
|
||||
text: GalleryViewerComponent.mostRecentText,
|
||||
},
|
||||
{
|
||||
key: SortBy.MostFavorited,
|
||||
text: GalleryViewerComponent.mostFavoritedText,
|
||||
},
|
||||
];
|
||||
|
||||
this.loadTabContent(this.state.selectedTab, this.state.searchText, this.state.sortBy, false);
|
||||
this.loadFavoriteNotebooks(this.state.searchText, this.state.sortBy, false); // Need this to show correct favorite button state
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
this.traceViewGallery();
|
||||
|
||||
const tabs: GalleryTabInfo[] = [];
|
||||
if (userContext.features.publicGallery) {
|
||||
tabs.push(
|
||||
this.createPublicGalleryTab(
|
||||
GalleryTab.PublicGallery,
|
||||
this.state.publicNotebooks,
|
||||
this.state.isCodeOfConductAccepted,
|
||||
),
|
||||
);
|
||||
}
|
||||
tabs.push(this.createSamplesTab(GalleryTab.OfficialSamples, this.state.sampleNotebooks));
|
||||
|
||||
if (this.props.container) {
|
||||
tabs.push(this.createFavoritesTab(GalleryTab.Favorites, this.state.favoriteNotebooks));
|
||||
if (userContext.features.publicGallery) {
|
||||
tabs.push(this.createPublishedNotebooksTab(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),
|
||||
};
|
||||
|
||||
return (
|
||||
<PivotItem key={pivotItemProps.itemKey} {...pivotItemProps}>
|
||||
{tab.content}
|
||||
</PivotItem>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="galleryContainer">
|
||||
<Pivot {...pivotProps}>{pivotItems}</Pivot>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private traceViewGallery = (): void => {
|
||||
if (!this.viewGalleryTraced) {
|
||||
this.viewGalleryTraced = true;
|
||||
trace(Action.NotebooksGalleryViewGallery);
|
||||
}
|
||||
|
||||
switch (this.state.selectedTab) {
|
||||
case GalleryTab.PublicGallery:
|
||||
if (!this.viewPublicGalleryTraced) {
|
||||
this.resetViewGalleryTabTracedFlags();
|
||||
this.viewPublicGalleryTraced = true;
|
||||
trace(Action.NotebooksGalleryViewPublicGallery);
|
||||
}
|
||||
break;
|
||||
case GalleryTab.OfficialSamples:
|
||||
if (!this.viewOfficialSamplesTraced) {
|
||||
this.resetViewGalleryTabTracedFlags();
|
||||
this.viewOfficialSamplesTraced = true;
|
||||
trace(Action.NotebooksGalleryViewOfficialSamples);
|
||||
}
|
||||
break;
|
||||
case GalleryTab.Favorites:
|
||||
if (!this.viewFavoritesTraced) {
|
||||
this.resetViewGalleryTabTracedFlags();
|
||||
this.viewFavoritesTraced = true;
|
||||
trace(Action.NotebooksGalleryViewFavorites);
|
||||
}
|
||||
break;
|
||||
case GalleryTab.Published:
|
||||
if (!this.viewPublishedNotebooksTraced) {
|
||||
this.resetViewGalleryTabTracedFlags();
|
||||
this.viewPublishedNotebooksTraced = true;
|
||||
trace(Action.NotebooksGalleryViewPublishedNotebooks);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown selected tab ${this.state.selectedTab}`);
|
||||
}
|
||||
};
|
||||
|
||||
private resetViewGalleryTabTracedFlags = (): void => {
|
||||
this.viewOfficialSamplesTraced = false;
|
||||
this.viewPublicGalleryTraced = false;
|
||||
this.viewFavoritesTraced = false;
|
||||
this.viewPublishedNotebooksTraced = false;
|
||||
};
|
||||
|
||||
private isEmptyData = (data: IGalleryItem[]): boolean => {
|
||||
return !data || data.length === 0;
|
||||
};
|
||||
|
||||
private createEmptyTabContent = (iconName: string, line1: JSX.Element, line2: JSX.Element): JSX.Element => {
|
||||
return (
|
||||
<Stack horizontalAlign="center" tokens={{ childrenGap: 10 }}>
|
||||
<FontIcon iconName={iconName} style={{ fontSize: 100, color: "lightgray", marginTop: 20 }} />
|
||||
<Text styles={{ root: { fontWeight: FontWeights.semibold } }}>{line1}</Text>
|
||||
<Text>{line2}</Text>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
private createSamplesTab = (tab: GalleryTab, data: IGalleryItem[]): GalleryTabInfo => {
|
||||
return {
|
||||
tab,
|
||||
content: this.createSearchBarHeader(this.createCardsTabContent(data)),
|
||||
};
|
||||
};
|
||||
|
||||
private createPublicGalleryTab(
|
||||
tab: GalleryTab,
|
||||
data: IGalleryItem[],
|
||||
acceptedCodeOfConduct: boolean,
|
||||
): GalleryTabInfo {
|
||||
return {
|
||||
tab,
|
||||
content: this.createPublicGalleryTabContent(data, acceptedCodeOfConduct),
|
||||
};
|
||||
}
|
||||
|
||||
private getFavouriteNotebooksTabContent = (data: IGalleryItem[]) => {
|
||||
if (this.isEmptyData(data)) {
|
||||
if (this.state.isFetchingFavouriteNotebooks) {
|
||||
return <Spinner size={SpinnerSize.large} />;
|
||||
}
|
||||
return this.createEmptyTabContent(
|
||||
"ContactHeart",
|
||||
<>You don't have any favorites yet</>,
|
||||
<>
|
||||
Favorite any notebook from the{" "}
|
||||
<Link onClick={() => this.setState({ selectedTab: GalleryTab.OfficialSamples })}>official samples</Link> or{" "}
|
||||
<Link onClick={() => this.setState({ selectedTab: GalleryTab.PublicGallery })}>public gallery</Link>
|
||||
</>,
|
||||
);
|
||||
}
|
||||
return this.createSearchBarHeader(this.createCardsTabContent(data));
|
||||
};
|
||||
|
||||
private createFavoritesTab(tab: GalleryTab, data: IGalleryItem[]): GalleryTabInfo {
|
||||
return {
|
||||
tab,
|
||||
content: this.getFavouriteNotebooksTabContent(data),
|
||||
};
|
||||
}
|
||||
|
||||
private getPublishedNotebooksTabContent = (data: IGalleryItem[]) => {
|
||||
if (this.isEmptyData(data)) {
|
||||
if (this.state.isFetchingPublishedNotebooks) {
|
||||
return <Spinner size={SpinnerSize.large} />;
|
||||
}
|
||||
return this.createEmptyTabContent(
|
||||
"Contact",
|
||||
<>
|
||||
You have not published anything to the{" "}
|
||||
<Link onClick={() => this.setState({ selectedTab: GalleryTab.PublicGallery })}>public gallery</Link> yet
|
||||
</>,
|
||||
<>Publish your notebooks to share your work with other users</>,
|
||||
);
|
||||
}
|
||||
return this.createPublishedNotebooksTabContent(data);
|
||||
};
|
||||
|
||||
private createPublishedNotebooksTab = (tab: GalleryTab, data: IGalleryItem[]): GalleryTabInfo => {
|
||||
return {
|
||||
tab,
|
||||
content: this.getPublishedNotebooksTabContent(data),
|
||||
};
|
||||
};
|
||||
|
||||
private createPublishedNotebooksTabContent = (data: IGalleryItem[]): JSX.Element => {
|
||||
const { published, underReview, removed } = GalleryUtils.filterPublishedNotebooks(data);
|
||||
const content = (
|
||||
<Stack tokens={{ childrenGap: 20 }}>
|
||||
{published?.length > 0 &&
|
||||
this.createPublishedNotebooksSectionContent(
|
||||
undefined,
|
||||
"You have successfully published and shared the following notebook(s) to the public gallery.",
|
||||
this.createCardsTabContent(published),
|
||||
)}
|
||||
{underReview?.length > 0 &&
|
||||
this.createPublishedNotebooksSectionContent(
|
||||
"Under Review",
|
||||
"Content of a notebook you published is currently being scanned for illegal content. It will not be available to public gallery until the review is completed (may take a few days)",
|
||||
this.createCardsTabContent(underReview),
|
||||
)}
|
||||
{removed?.length > 0 &&
|
||||
this.createPublishedNotebooksSectionContent(
|
||||
"Removed",
|
||||
"These notebooks were found to contain illegal content and has been taken down.",
|
||||
this.createPolicyViolationsListContent(removed),
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
|
||||
return this.createSearchBarHeader(content);
|
||||
};
|
||||
|
||||
private createPublishedNotebooksSectionContent = (
|
||||
title: string,
|
||||
description: string,
|
||||
content: JSX.Element,
|
||||
): JSX.Element => {
|
||||
return (
|
||||
<Stack tokens={{ childrenGap: 10 }}>
|
||||
{title && (
|
||||
<Text styles={{ root: { fontWeight: FontWeights.semibold, marginLeft: 10, marginRight: 10 } }}>{title}</Text>
|
||||
)}
|
||||
{description && <Text styles={{ root: { marginLeft: 10, marginRight: 10 } }}>{description}</Text>}
|
||||
{content}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
private createPublicGalleryTabContent(data: IGalleryItem[], acceptedCodeOfConduct: boolean): JSX.Element {
|
||||
return (
|
||||
<div className="publicGalleryTabContainer">
|
||||
{this.createSearchBarHeader(this.createCardsTabContent(data))}
|
||||
{acceptedCodeOfConduct === false && (
|
||||
<Overlay isDarkThemed>
|
||||
<div className="publicGalleryTabOverlayContent">
|
||||
<CodeOfConduct
|
||||
junoClient={this.props.junoClient}
|
||||
onAcceptCodeOfConduct={(result: boolean) => {
|
||||
this.setState({ isCodeOfConductAccepted: result });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Overlay>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private createSearchBarHeader(content: JSX.Element): JSX.Element {
|
||||
return (
|
||||
<Stack tokens={{ childrenGap: 10 }}>
|
||||
<Stack horizontal wrap tokens={{ childrenGap: 20, padding: 10 }}>
|
||||
<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={this.sortingOptions} selectedKey={this.state.sortBy} onChange={this.onDropdownChange} />
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<InfoComponent />
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
<Stack.Item>{content}</Stack.Item>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
private createCardsTabContent(data: IGalleryItem[]): JSX.Element {
|
||||
return data ? (
|
||||
<FocusZone>
|
||||
<List
|
||||
items={data}
|
||||
getPageSpecification={this.getPageSpecification}
|
||||
renderedWindowsAhead={3}
|
||||
onRenderCell={this.onRenderCell}
|
||||
/>
|
||||
</FocusZone>
|
||||
) : (
|
||||
<Spinner size={SpinnerSize.large} />
|
||||
);
|
||||
}
|
||||
|
||||
private createPolicyViolationsListContent(data: IGalleryItem[]): JSX.Element {
|
||||
return (
|
||||
<table style={{ margin: 10 }}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Policy violations</th>
|
||||
</tr>
|
||||
{data.map((item) => (
|
||||
<tr key={`policy-violations-tr-${item.id}`}>
|
||||
<td>{item.name}</td>
|
||||
<td>{item.policyViolations.join(", ")}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
private loadTabContent(tab: GalleryTab, searchText: string, sortBy: SortBy, offline: boolean): void {
|
||||
switch (tab) {
|
||||
case GalleryTab.PublicGallery:
|
||||
this.loadPublicNotebooks(searchText, sortBy, offline);
|
||||
break;
|
||||
|
||||
case GalleryTab.OfficialSamples:
|
||||
this.loadSampleNotebooks(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}`);
|
||||
}
|
||||
}
|
||||
|
||||
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`);
|
||||
}
|
||||
|
||||
this.sampleNotebooks = response.data;
|
||||
|
||||
trace(Action.NotebooksGalleryOfficialSamplesCount, ActionModifiers.Mark, {
|
||||
count: this.sampleNotebooks?.length,
|
||||
});
|
||||
} catch (error) {
|
||||
handleError(error, "GalleryViewerComponent/loadSampleNotebooks", "Failed to load sample notebooks");
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
sampleNotebooks: this.sampleNotebooks && [...this.sort(sortBy, this.search(searchText, this.sampleNotebooks))],
|
||||
});
|
||||
}
|
||||
|
||||
private async loadPublicNotebooks(searchText: string, sortBy: SortBy, offline: boolean): Promise<void> {
|
||||
if (!offline) {
|
||||
try {
|
||||
let response: IJunoResponse<IGalleryItem[]> | IJunoResponse<IPublicGalleryData>;
|
||||
if (this.props.container) {
|
||||
response = await this.props.junoClient.getPublicGalleryData();
|
||||
this.isCodeOfConductAccepted = response.data?.metadata.acceptedCodeOfConduct;
|
||||
this.publicNotebooks = response.data?.notebooksData;
|
||||
} else {
|
||||
response = await this.props.junoClient.getPublicNotebooks();
|
||||
this.publicNotebooks = response.data;
|
||||
}
|
||||
|
||||
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
|
||||
throw new Error(`Received HTTP ${response.status} when loading public notebooks`);
|
||||
}
|
||||
|
||||
trace(Action.NotebooksGalleryPublicGalleryCount, ActionModifiers.Mark, { count: this.publicNotebooks?.length });
|
||||
} catch (error) {
|
||||
handleError(error, "GalleryViewerComponent/loadPublicNotebooks", "Failed to load public notebooks");
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
publicNotebooks: this.publicNotebooks && [...this.sort(sortBy, this.search(searchText, this.publicNotebooks))],
|
||||
isCodeOfConductAccepted: this.isCodeOfConductAccepted,
|
||||
});
|
||||
}
|
||||
|
||||
private async loadFavoriteNotebooks(searchText: string, sortBy: SortBy, offline: boolean): Promise<void> {
|
||||
if (!offline) {
|
||||
try {
|
||||
this.setState({ isFetchingFavouriteNotebooks: true });
|
||||
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;
|
||||
|
||||
trace(Action.NotebooksGalleryFavoritesCount, ActionModifiers.Mark, { count: this.favoriteNotebooks?.length });
|
||||
} catch (error) {
|
||||
handleError(error, "GalleryViewerComponent/loadFavoriteNotebooks", "Failed to load favorite notebooks");
|
||||
} finally {
|
||||
this.setState({ isFetchingFavouriteNotebooks: false });
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
this.setState({ isFetchingPublishedNotebooks: true });
|
||||
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;
|
||||
|
||||
const { published, underReview, removed } = GalleryUtils.filterPublishedNotebooks(this.publishedNotebooks);
|
||||
trace(Action.NotebooksGalleryPublishedCount, ActionModifiers.Mark, {
|
||||
count: this.publishedNotebooks?.length,
|
||||
publishedCount: published.length,
|
||||
underReviewCount: underReview.length,
|
||||
removedCount: removed.length,
|
||||
});
|
||||
} catch (error) {
|
||||
handleError(error, "GalleryViewerComponent/loadPublishedNotebooks", "Failed to load published notebooks");
|
||||
} finally {
|
||||
this.setState({ isFetchingPublishedNotebooks: false });
|
||||
}
|
||||
}
|
||||
|
||||
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()];
|
||||
|
||||
if (item.tags) {
|
||||
searchData.push(...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 => {
|
||||
if (itemIndex === 0) {
|
||||
this.columnCount = Math.floor(visibleRect.width / GalleryViewerComponent.CARD_WIDTH) || this.columnCount;
|
||||
this.rowCount = GalleryViewerComponent.rowsPerPage;
|
||||
}
|
||||
|
||||
return {
|
||||
height: visibleRect.height,
|
||||
itemCount: this.columnCount * this.rowCount,
|
||||
};
|
||||
};
|
||||
|
||||
private onRenderCell = (data?: IGalleryItem): JSX.Element => {
|
||||
const isFavorite =
|
||||
this.props.container && this.favoriteNotebooks?.find((item) => item.id === data.id) !== undefined;
|
||||
const props: GalleryCardComponentProps = {
|
||||
data,
|
||||
isFavorite,
|
||||
showDownload: !!this.props.container,
|
||||
showDelete: this.state.selectedTab === GalleryTab.Published,
|
||||
onClick: () => this.props.openNotebook(data, isFavorite),
|
||||
onTagClick: this.loadTaggedItems,
|
||||
onFavoriteClick: () => this.favoriteItem(data),
|
||||
onUnfavoriteClick: () => this.unfavoriteItem(data),
|
||||
onDownloadClick: () => this.downloadItem(data),
|
||||
onDeleteClick: (beforeDelete: () => void, afterDelete: () => void) =>
|
||||
this.deleteItem(data, beforeDelete, afterDelete),
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ float: "left", padding: 5 }}>
|
||||
<GalleryCardComponent {...props} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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.props.container, this.props.junoClient, data, (item) =>
|
||||
this.refreshSelectedTab(item),
|
||||
);
|
||||
};
|
||||
|
||||
private deleteItem = async (data: IGalleryItem, beforeDelete: () => void, afterDelete: () => void): 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);
|
||||
},
|
||||
beforeDelete,
|
||||
afterDelete,
|
||||
);
|
||||
};
|
||||
|
||||
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,26 +0,0 @@
|
||||
@import "../../../../../less/Common/Constants.less";
|
||||
.infoPanel, .infoPanelMain {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.infoPanel {
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
.infoLabel, .infoLabelMain {
|
||||
padding-left: 5px
|
||||
}
|
||||
|
||||
.infoLabel {
|
||||
font-weight: 400
|
||||
}
|
||||
|
||||
.infoIconMain {
|
||||
color: @AccentMedium
|
||||
}
|
||||
|
||||
.infoIconMain:hover {
|
||||
color: @BaseMedium
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import { InfoComponent } from "./InfoComponent";
|
||||
|
||||
describe("InfoComponent", () => {
|
||||
it("renders", () => {
|
||||
const wrapper = shallow(<InfoComponent />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -1,51 +0,0 @@
|
||||
import { HoverCard, HoverCardType, Icon, Label, Link, Stack } from "@fluentui/react";
|
||||
import * as React from "react";
|
||||
import { CodeOfConductEndpoints } from "../../../../Common/Constants";
|
||||
import "./InfoComponent.less";
|
||||
|
||||
export interface InfoComponentProps {
|
||||
onReportAbuseClick?: () => void;
|
||||
}
|
||||
|
||||
export class InfoComponent extends React.Component<InfoComponentProps> {
|
||||
private getInfoPanel = (iconName: string, labelText: string, url?: string, onClick?: () => void): JSX.Element => {
|
||||
return (
|
||||
<Link href={url} target={url && "_blank"} onClick={onClick}>
|
||||
<div className="infoPanel">
|
||||
<Icon iconName={iconName} styles={{ root: { verticalAlign: "middle" } }} />
|
||||
<Label className="infoLabel">{labelText}</Label>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
private onHover = (): JSX.Element => {
|
||||
return (
|
||||
<Stack tokens={{ childrenGap: 5, padding: 5 }}>
|
||||
<Stack.Item>{this.getInfoPanel("Script", "Code of Conduct", CodeOfConductEndpoints.codeOfConduct)}</Stack.Item>
|
||||
<Stack.Item>
|
||||
{this.getInfoPanel("RedEye", "Privacy Statement", CodeOfConductEndpoints.privacyStatement)}
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
{this.getInfoPanel("KnowledgeArticle", "Microsoft Terms of Use", CodeOfConductEndpoints.termsOfUse)}
|
||||
</Stack.Item>
|
||||
{this.props.onReportAbuseClick && (
|
||||
<Stack.Item>
|
||||
{this.getInfoPanel("ReportHacked", "Report Abuse", undefined, () => this.props.onReportAbuseClick())}
|
||||
</Stack.Item>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
public render(): JSX.Element {
|
||||
return (
|
||||
<HoverCard plainCardProps={{ onRenderPlainCard: this.onHover }} instantOpenOnClick type={HoverCardType.plain}>
|
||||
<div className="infoPanelMain" tabIndex={0}>
|
||||
<Icon className="infoIconMain" iconName="Help" styles={{ root: { verticalAlign: "middle" } }} />
|
||||
<Label className="infoLabelMain">Help</Label>
|
||||
</div>
|
||||
</HoverCard>
|
||||
);
|
||||
}
|
||||
}
|
||||
-35
@@ -1,35 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`InfoComponent renders 1`] = `
|
||||
<StyledHoverCardBase
|
||||
instantOpenOnClick={true}
|
||||
plainCardProps={
|
||||
{
|
||||
"onRenderPlainCard": [Function],
|
||||
}
|
||||
}
|
||||
type="PlainCard"
|
||||
>
|
||||
<div
|
||||
className="infoPanelMain"
|
||||
tabIndex={0}
|
||||
>
|
||||
<Icon
|
||||
className="infoIconMain"
|
||||
iconName="Help"
|
||||
styles={
|
||||
{
|
||||
"root": {
|
||||
"verticalAlign": "middle",
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
<StyledLabelBase
|
||||
className="infoLabelMain"
|
||||
>
|
||||
Help
|
||||
</StyledLabelBase>
|
||||
</div>
|
||||
</StyledHoverCardBase>
|
||||
`;
|
||||
-98
@@ -1,98 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`GalleryViewerComponent renders 1`] = `
|
||||
<div
|
||||
className="galleryContainer"
|
||||
>
|
||||
<StyledPivot
|
||||
onLinkClick={[Function]}
|
||||
selectedKey="OfficialSamples"
|
||||
>
|
||||
<PivotItem
|
||||
headerText="Official samples"
|
||||
itemKey="OfficialSamples"
|
||||
key="OfficialSamples"
|
||||
style={
|
||||
{
|
||||
"marginTop": 20,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Stack
|
||||
tokens={
|
||||
{
|
||||
"childrenGap": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
tokens={
|
||||
{
|
||||
"childrenGap": 20,
|
||||
"padding": 10,
|
||||
}
|
||||
}
|
||||
wrap={true}
|
||||
>
|
||||
<StackItem
|
||||
grow={true}
|
||||
>
|
||||
<StyledSearchBox
|
||||
onChange={[Function]}
|
||||
placeholder="Search"
|
||||
/>
|
||||
</StackItem>
|
||||
<StackItem>
|
||||
<StyledLabelBase>
|
||||
Sort by
|
||||
</StyledLabelBase>
|
||||
</StackItem>
|
||||
<StackItem
|
||||
styles={
|
||||
{
|
||||
"root": {
|
||||
"minWidth": 200,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<Dropdown
|
||||
onChange={[Function]}
|
||||
options={
|
||||
[
|
||||
{
|
||||
"key": 0,
|
||||
"text": "Most viewed",
|
||||
},
|
||||
{
|
||||
"key": 1,
|
||||
"text": "Most downloaded",
|
||||
},
|
||||
{
|
||||
"key": 3,
|
||||
"text": "Most recent",
|
||||
},
|
||||
{
|
||||
"key": 2,
|
||||
"text": "Most favorited",
|
||||
},
|
||||
]
|
||||
}
|
||||
selectedKey={0}
|
||||
/>
|
||||
</StackItem>
|
||||
<StackItem>
|
||||
<InfoComponent />
|
||||
</StackItem>
|
||||
</Stack>
|
||||
<StackItem>
|
||||
<StyledSpinnerBase
|
||||
size={3}
|
||||
/>
|
||||
</StackItem>
|
||||
</Stack>
|
||||
</PivotItem>
|
||||
</StyledPivot>
|
||||
</div>
|
||||
`;
|
||||
@@ -25,10 +25,6 @@ describe("NotebookMetadataComponent", () => {
|
||||
isFavorite: false,
|
||||
downloadButtonText: "Download",
|
||||
onTagClick: undefined,
|
||||
onDownloadClick: undefined,
|
||||
onFavoriteClick: undefined,
|
||||
onUnfavoriteClick: undefined,
|
||||
onReportAbuseClick: undefined,
|
||||
};
|
||||
|
||||
const wrapper = shallow(<NotebookMetadataComponent {...props} />);
|
||||
@@ -57,10 +53,6 @@ describe("NotebookMetadataComponent", () => {
|
||||
isFavorite: true,
|
||||
downloadButtonText: "Download",
|
||||
onTagClick: undefined,
|
||||
onDownloadClick: undefined,
|
||||
onFavoriteClick: undefined,
|
||||
onUnfavoriteClick: undefined,
|
||||
onReportAbuseClick: undefined,
|
||||
};
|
||||
|
||||
const wrapper = shallow(<NotebookMetadataComponent {...props} />);
|
||||
|
||||
@@ -1,46 +1,21 @@
|
||||
/**
|
||||
* Wrapper around Notebook metadata
|
||||
*/
|
||||
import { FontWeights, Icon, IconButton, Link, Persona, PersonaSize, PrimaryButton, Stack, Text } from "@fluentui/react";
|
||||
import { FontWeights, Icon, Link, Persona, PersonaSize, Stack, Text } from "@fluentui/react";
|
||||
import * as React from "react";
|
||||
import { IGalleryItem } from "../../../Juno/JunoClient";
|
||||
import * as FileSystemUtil from "../../Notebook/FileSystemUtil";
|
||||
import "./NotebookViewerComponent.less";
|
||||
import CosmosDBLogo from "../../../../images/CosmosDB-logo.svg";
|
||||
import { InfoComponent } from "../NotebookGallery/InfoComponent/InfoComponent";
|
||||
|
||||
export interface NotebookMetadataComponentProps {
|
||||
data: IGalleryItem;
|
||||
isFavorite: boolean;
|
||||
downloadButtonText?: string;
|
||||
onTagClick: (tag: string) => void;
|
||||
onFavoriteClick: () => void;
|
||||
onUnfavoriteClick: () => void;
|
||||
onDownloadClick: () => void;
|
||||
onReportAbuseClick: () => void;
|
||||
}
|
||||
|
||||
export class NotebookMetadataComponent extends React.Component<NotebookMetadataComponentProps> {
|
||||
private renderFavouriteButton = (): JSX.Element => {
|
||||
return (
|
||||
<Text>
|
||||
{this.props.isFavorite !== undefined ? (
|
||||
<>
|
||||
<IconButton
|
||||
iconProps={{ iconName: this.props.isFavorite ? "HeartFill" : "Heart" }}
|
||||
onClick={this.props.isFavorite ? this.props.onUnfavoriteClick : this.props.onFavoriteClick}
|
||||
/>
|
||||
{this.props.data.favorites} likes
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Icon iconName="Heart" /> {this.props.data.favorites} likes
|
||||
</>
|
||||
)}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
public render(): JSX.Element {
|
||||
const options: Intl.DateTimeFormatOptions = {
|
||||
year: "numeric",
|
||||
@@ -59,20 +34,10 @@ export class NotebookMetadataComponent extends React.Component<NotebookMetadataC
|
||||
</Text>
|
||||
</Stack.Item>
|
||||
|
||||
<Stack.Item>{this.renderFavouriteButton()}</Stack.Item>
|
||||
|
||||
{this.props.downloadButtonText && (
|
||||
<Stack.Item>
|
||||
<PrimaryButton text={this.props.downloadButtonText} onClick={this.props.onDownloadClick} />
|
||||
</Stack.Item>
|
||||
)}
|
||||
|
||||
<Stack.Item grow>
|
||||
<></>
|
||||
</Stack.Item>
|
||||
|
||||
<Stack.Item>
|
||||
<InfoComponent onReportAbuseClick={this.props.onReportAbuseClick} />
|
||||
<Text>
|
||||
<Icon iconName="Heart" /> {this.props.data.favorites} likes
|
||||
</Text>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Wrapper around Notebook Viewer Read only content
|
||||
*/
|
||||
import { IChoiceGroupProps, Icon, IProgressIndicatorProps, Link, ProgressIndicator } from "@fluentui/react";
|
||||
import { Icon, Link, ProgressIndicator } from "@fluentui/react";
|
||||
import { Notebook } from "@nteract/commutable";
|
||||
import { createContentRef } from "@nteract/core";
|
||||
import * as React from "react";
|
||||
@@ -11,14 +11,11 @@ import { IGalleryItem, JunoClient } from "../../../Juno/JunoClient";
|
||||
import { SessionStorageUtility } from "../../../Shared/StorageUtility";
|
||||
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import { traceFailure, traceStart, traceSuccess } from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import * as GalleryUtils from "../../../Utils/GalleryUtils";
|
||||
import { DialogHost } from "../../../Utils/GalleryUtils";
|
||||
import Explorer from "../../Explorer";
|
||||
import { NotebookClientV2 } from "../../Notebook/NotebookClientV2";
|
||||
import { NotebookComponentBootstrapper } from "../../Notebook/NotebookComponent/NotebookComponentBootstrapper";
|
||||
import NotebookReadOnlyRenderer from "../../Notebook/NotebookRenderer/NotebookReadOnlyRenderer";
|
||||
import { useNotebook } from "../../Notebook/useNotebook";
|
||||
import { Dialog, TextFieldProps, useDialog } from "../Dialog";
|
||||
import { NotebookMetadataComponent } from "./NotebookMetadataComponent";
|
||||
import "./NotebookViewerComponent.less";
|
||||
|
||||
@@ -42,10 +39,10 @@ interface NotebookViewerComponentState {
|
||||
showProgressBar: boolean;
|
||||
}
|
||||
|
||||
export class NotebookViewerComponent
|
||||
extends React.Component<NotebookViewerComponentProps, NotebookViewerComponentState>
|
||||
implements DialogHost
|
||||
{
|
||||
export class NotebookViewerComponent extends React.Component<
|
||||
NotebookViewerComponentProps,
|
||||
NotebookViewerComponentState
|
||||
> {
|
||||
private clientManager: NotebookClientV2;
|
||||
private notebookComponentBootstrapper: NotebookComponentBootstrapper;
|
||||
|
||||
@@ -102,7 +99,6 @@ export class NotebookViewerComponent
|
||||
);
|
||||
|
||||
const notebook: Notebook = await response.json();
|
||||
GalleryUtils.removeNotebookViewerLink(notebook, this.props.galleryItem?.newCellId);
|
||||
this.notebookComponentBootstrapper.setContent("json", notebook);
|
||||
this.setState({ content: notebook, showProgressBar: false });
|
||||
|
||||
@@ -150,10 +146,6 @@ export class NotebookViewerComponent
|
||||
isFavorite={this.state.isFavorite}
|
||||
downloadButtonText={this.props.container && `Download to ${useNotebook.getState().notebookFolderName}`}
|
||||
onTagClick={this.props.onTagClick}
|
||||
onFavoriteClick={this.favoriteItem}
|
||||
onUnfavoriteClick={this.unfavoriteItem}
|
||||
onDownloadClick={this.downloadItem}
|
||||
onReportAbuseClick={this.state.galleryItem.isSample ? undefined : this.reportAbuse}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
@@ -166,7 +158,6 @@ export class NotebookViewerComponent
|
||||
hideInputs: this.props.hideInputs,
|
||||
hidePrompts: this.props.hidePrompts,
|
||||
})}
|
||||
<Dialog />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -191,81 +182,4 @@ export class NotebookViewerComponent
|
||||
isFavorite,
|
||||
};
|
||||
}
|
||||
|
||||
showOkModalDialog(
|
||||
title: string,
|
||||
msg: string,
|
||||
okLabel: string,
|
||||
onOk: () => void,
|
||||
progressIndicatorProps?: IProgressIndicatorProps,
|
||||
): void {
|
||||
useDialog.getState().openDialog({
|
||||
isModal: true,
|
||||
title,
|
||||
subText: msg,
|
||||
primaryButtonText: okLabel,
|
||||
onPrimaryButtonClick: () => {
|
||||
useDialog.getState().closeDialog();
|
||||
onOk && onOk();
|
||||
},
|
||||
secondaryButtonText: undefined,
|
||||
onSecondaryButtonClick: undefined,
|
||||
progressIndicatorProps,
|
||||
});
|
||||
}
|
||||
|
||||
showOkCancelModalDialog(
|
||||
title: string,
|
||||
msg: string,
|
||||
okLabel: string,
|
||||
onOk: () => void,
|
||||
cancelLabel: string,
|
||||
onCancel: () => void,
|
||||
progressIndicatorProps?: IProgressIndicatorProps,
|
||||
choiceGroupProps?: IChoiceGroupProps,
|
||||
textFieldProps?: TextFieldProps,
|
||||
primaryButtonDisabled?: boolean,
|
||||
): void {
|
||||
useDialog.getState().openDialog({
|
||||
isModal: true,
|
||||
title,
|
||||
subText: msg,
|
||||
primaryButtonText: okLabel,
|
||||
secondaryButtonText: cancelLabel,
|
||||
onPrimaryButtonClick: () => {
|
||||
useDialog.getState().closeDialog();
|
||||
onOk && onOk();
|
||||
},
|
||||
onSecondaryButtonClick: () => {
|
||||
useDialog.getState().closeDialog();
|
||||
onCancel && onCancel();
|
||||
},
|
||||
progressIndicatorProps,
|
||||
choiceGroupProps,
|
||||
textFieldProps,
|
||||
primaryButtonDisabled,
|
||||
});
|
||||
}
|
||||
|
||||
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.props.container, this.props.junoClient, this.state.galleryItem, (item) =>
|
||||
this.setState({ galleryItem: item }),
|
||||
);
|
||||
};
|
||||
|
||||
private reportAbuse = (): void => {
|
||||
GalleryUtils.reportAbuse(this.props.junoClient, this.state.galleryItem, this, () => {});
|
||||
};
|
||||
}
|
||||
|
||||
+6
-34
@@ -27,28 +27,14 @@ exports[`NotebookMetadataComponent renders liked notebook 1`] = `
|
||||
</StackItem>
|
||||
<StackItem>
|
||||
<Text>
|
||||
<CustomizedIconButton
|
||||
iconProps={
|
||||
{
|
||||
"iconName": "HeartFill",
|
||||
}
|
||||
}
|
||||
<Icon
|
||||
iconName="Heart"
|
||||
/>
|
||||
|
||||
0
|
||||
likes
|
||||
</Text>
|
||||
</StackItem>
|
||||
<StackItem>
|
||||
<CustomizedPrimaryButton
|
||||
text="Download"
|
||||
/>
|
||||
</StackItem>
|
||||
<StackItem
|
||||
grow={true}
|
||||
/>
|
||||
<StackItem>
|
||||
<InfoComponent />
|
||||
</StackItem>
|
||||
</Stack>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
@@ -139,28 +125,14 @@ exports[`NotebookMetadataComponent renders un-liked notebook 1`] = `
|
||||
</StackItem>
|
||||
<StackItem>
|
||||
<Text>
|
||||
<CustomizedIconButton
|
||||
iconProps={
|
||||
{
|
||||
"iconName": "Heart",
|
||||
}
|
||||
}
|
||||
<Icon
|
||||
iconName="Heart"
|
||||
/>
|
||||
|
||||
0
|
||||
likes
|
||||
</Text>
|
||||
</StackItem>
|
||||
<StackItem>
|
||||
<CustomizedPrimaryButton
|
||||
text="Download"
|
||||
/>
|
||||
</StackItem>
|
||||
<StackItem
|
||||
grow={true}
|
||||
/>
|
||||
<StackItem>
|
||||
<InfoComponent />
|
||||
</StackItem>
|
||||
</Stack>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
|
||||
@@ -5,7 +5,6 @@ import { sendMessage } from "Common/MessageHandler";
|
||||
import { Platform, configContext } from "ConfigContext";
|
||||
import { MessageTypes } from "Contracts/ExplorerContracts";
|
||||
import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane";
|
||||
import { IGalleryItem } from "Juno/JunoClient";
|
||||
import {
|
||||
isFabricMirrored,
|
||||
isFabricMirroredKey,
|
||||
@@ -52,13 +51,10 @@ import { useSidePanel } from "../hooks/useSidePanel";
|
||||
import { ReactTabKind, useTabs } from "../hooks/useTabs";
|
||||
import "./ComponentRegisterer";
|
||||
import { DialogProps, useDialog } from "./Controls/Dialog";
|
||||
import { GalleryTab as GalleryTabKind } from "./Controls/NotebookGallery/GalleryViewerComponent";
|
||||
import { useCommandBar } from "./Menus/CommandBar/CommandBarComponentAdapter";
|
||||
import * as FileSystemUtil from "./Notebook/FileSystemUtil";
|
||||
import { SnapshotRequest } from "./Notebook/NotebookComponent/types";
|
||||
import { NotebookContentItem, NotebookContentItemType } from "./Notebook/NotebookContentItem";
|
||||
import type NotebookManager from "./Notebook/NotebookManager";
|
||||
import { NotebookPaneContent } from "./Notebook/NotebookManager";
|
||||
import { NotebookUtil } from "./Notebook/NotebookUtil";
|
||||
import { useNotebook } from "./Notebook/useNotebook";
|
||||
import { AddCollectionPanel } from "./Panes/AddCollectionPanel/AddCollectionPanel";
|
||||
@@ -708,24 +704,6 @@ export default class Explorer {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
public async publishNotebook(
|
||||
name: string,
|
||||
content: NotebookPaneContent,
|
||||
notebookContentRef?: string,
|
||||
onTakeSnapshot?: (request: SnapshotRequest) => void,
|
||||
onClosePanel?: () => void,
|
||||
): Promise<void> {
|
||||
if (this.notebookManager) {
|
||||
await this.notebookManager.openPublishNotebookPane(
|
||||
name,
|
||||
content,
|
||||
notebookContentRef,
|
||||
onTakeSnapshot,
|
||||
onClosePanel,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public copyNotebook(name: string, content: string): void {
|
||||
this.notebookManager?.openCopyNotebookPane(name, content);
|
||||
}
|
||||
@@ -1045,45 +1023,6 @@ export default class Explorer {
|
||||
useTabs.getState().activateNewTab(newTab);
|
||||
}
|
||||
|
||||
public async openGallery(
|
||||
selectedTab?: GalleryTabKind,
|
||||
notebookUrl?: string,
|
||||
galleryItem?: IGalleryItem,
|
||||
isFavorite?: boolean,
|
||||
): Promise<void> {
|
||||
const title = "Gallery";
|
||||
const GalleryTab = await (await import(/* webpackChunkName: "GalleryTab" */ "./Tabs/GalleryTab")).default;
|
||||
const galleryTab = useTabs
|
||||
.getState()
|
||||
.getTabs(ViewModels.CollectionTabKind.Gallery)
|
||||
.find((tab) => tab.tabTitle() === title);
|
||||
|
||||
if (galleryTab instanceof GalleryTab) {
|
||||
useTabs.getState().activateTab(galleryTab);
|
||||
} else {
|
||||
useTabs.getState().activateNewTab(
|
||||
new GalleryTab(
|
||||
{
|
||||
tabKind: ViewModels.CollectionTabKind.Gallery,
|
||||
title,
|
||||
tabPath: title,
|
||||
onLoadStartKey: undefined,
|
||||
isTabsContentExpanded: ko.observable(true),
|
||||
},
|
||||
{
|
||||
account: userContext.databaseAccount,
|
||||
container: this,
|
||||
junoClient: this.notebookManager?.junoClient,
|
||||
selectedTab: selectedTab || GalleryTabKind.OfficialSamples,
|
||||
notebookUrl,
|
||||
galleryItem,
|
||||
isFavorite,
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async onNewCollectionClicked(
|
||||
options: {
|
||||
databaseId?: string;
|
||||
|
||||
@@ -17,16 +17,13 @@ import { JunoClient } from "../../Juno/JunoClient";
|
||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { getFullName } from "../../Utils/UserUtils";
|
||||
import { useDialog } from "../Controls/Dialog";
|
||||
import Explorer from "../Explorer";
|
||||
import { CopyNotebookPane } from "../Panes/CopyNotebookPane/CopyNotebookPane";
|
||||
import { GitHubReposPanel } from "../Panes/GitHubReposPanel/GitHubReposPanel";
|
||||
import { PublishNotebookPane } from "../Panes/PublishNotebookPane/PublishNotebookPane";
|
||||
import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter";
|
||||
import { InMemoryContentProvider } from "./NotebookComponent/ContentProviders/InMemoryContentProvider";
|
||||
import { NotebookContentProvider } from "./NotebookComponent/NotebookContentProvider";
|
||||
import { SnapshotRequest } from "./NotebookComponent/types";
|
||||
import { NotebookContainerClient } from "./NotebookContainerClient";
|
||||
import { NotebookContentClient } from "./NotebookContentClient";
|
||||
import { SchemaAnalyzerNotebook } from "./SchemaAnalyzer/SchemaAnalyzerUtils";
|
||||
@@ -124,31 +121,6 @@ export default class NotebookManager {
|
||||
}
|
||||
}
|
||||
|
||||
public async openPublishNotebookPane(
|
||||
name: string,
|
||||
content: NotebookPaneContent,
|
||||
notebookContentRef: string,
|
||||
onTakeSnapshot: (request: SnapshotRequest) => void,
|
||||
onClosePanel: () => void,
|
||||
): Promise<void> {
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
"Publish Notebook",
|
||||
<PublishNotebookPane
|
||||
explorer={this.params.container}
|
||||
junoClient={this.junoClient}
|
||||
name={name}
|
||||
author={getFullName()}
|
||||
notebookContent={content}
|
||||
notebookContentRef={notebookContentRef}
|
||||
onTakeSnapshot={onTakeSnapshot}
|
||||
/>,
|
||||
"440px",
|
||||
onClosePanel,
|
||||
);
|
||||
}
|
||||
|
||||
public openCopyNotebookPane(name: string, content: string): void {
|
||||
const { container } = this.params;
|
||||
useSidePanel
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import { PublishNotebookPaneComponent, PublishNotebookPaneProps } from "./PublishNotebookPaneComponent";
|
||||
|
||||
describe("PublishNotebookPaneComponent", () => {
|
||||
it("renders", () => {
|
||||
const props: PublishNotebookPaneProps = {
|
||||
notebookName: "SampleNotebook.ipynb",
|
||||
notebookDescription: "sample description",
|
||||
notebookTags: "tag1, tag2",
|
||||
imageSrc: "https://i.ytimg.com/vi/E_lByLdKeKY/maxresdefault.jpg",
|
||||
notebookAuthor: "CosmosDB",
|
||||
notebookCreatedDate: "2020-07-17T00:00:00Z",
|
||||
notebookObject: undefined,
|
||||
notebookContentRef: undefined,
|
||||
setNotebookName: undefined,
|
||||
setNotebookDescription: undefined,
|
||||
setNotebookTags: undefined,
|
||||
setImageSrc: undefined,
|
||||
onError: undefined,
|
||||
clearFormError: undefined,
|
||||
onTakeSnapshot: undefined,
|
||||
};
|
||||
|
||||
const wrapper = shallow(<PublishNotebookPaneComponent {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -1,214 +0,0 @@
|
||||
import { ImmutableNotebook, toJS } from "@nteract/commutable";
|
||||
import React, { FunctionComponent, useEffect, useState } from "react";
|
||||
import { HttpStatusCodes } from "../../../Common/Constants";
|
||||
import { getErrorMessage, getErrorStack, handleError } from "../../../Common/ErrorHandlingUtils";
|
||||
import { useNotebookSnapshotStore } from "../../../hooks/useNotebookSnapshotStore";
|
||||
import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||
import { JunoClient } from "../../../Juno/JunoClient";
|
||||
import { Keys, t } from "Localization";
|
||||
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import { traceFailure, traceStart, traceSuccess } from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
|
||||
import { CodeOfConduct } from "../../Controls/NotebookGallery/CodeOfConduct/CodeOfConduct";
|
||||
import { GalleryTab } from "../../Controls/NotebookGallery/GalleryViewerComponent";
|
||||
import Explorer from "../../Explorer";
|
||||
import * as FileSystemUtil from "../../Notebook/FileSystemUtil";
|
||||
import { SnapshotRequest } from "../../Notebook/NotebookComponent/types";
|
||||
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
|
||||
import { PublishNotebookPaneComponent, PublishNotebookPaneProps } from "./PublishNotebookPaneComponent";
|
||||
|
||||
export interface PublishNotebookPaneAProps {
|
||||
explorer: Explorer;
|
||||
junoClient: JunoClient;
|
||||
name: string;
|
||||
author: string;
|
||||
notebookContent: string | ImmutableNotebook;
|
||||
notebookContentRef: string;
|
||||
onTakeSnapshot: (request: SnapshotRequest) => void;
|
||||
}
|
||||
export const PublishNotebookPane: FunctionComponent<PublishNotebookPaneAProps> = ({
|
||||
explorer: container,
|
||||
junoClient,
|
||||
name,
|
||||
author,
|
||||
notebookContent,
|
||||
notebookContentRef,
|
||||
onTakeSnapshot,
|
||||
}: PublishNotebookPaneAProps): JSX.Element => {
|
||||
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
|
||||
|
||||
const [isCodeOfConductAccepted, setIsCodeOfConductAccepted] = useState<boolean>(false);
|
||||
const [content, setContent] = useState<string>("");
|
||||
const [formError, setFormError] = useState<string>("");
|
||||
const [formErrorDetail, setFormErrorDetail] = useState<string>("");
|
||||
const [isExecuting, setIsExecuting] = useState<boolean>();
|
||||
|
||||
const [notebookName, setNotebookName] = useState<string>(name);
|
||||
const [notebookDescription, setNotebookDescription] = useState<string>("");
|
||||
const [notebookTags, setNotebookTags] = useState<string>("");
|
||||
const [imageSrc, setImageSrc] = useState<string>();
|
||||
const { snapshot: notebookSnapshot, error: notebookSnapshotError } = useNotebookSnapshotStore();
|
||||
|
||||
const CodeOfConductAccepted = async () => {
|
||||
try {
|
||||
const response = await junoClient.isCodeOfConductAccepted();
|
||||
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
|
||||
throw new Error(`Received HTTP ${response.status} when accepting code of conduct`);
|
||||
}
|
||||
setIsCodeOfConductAccepted(response.data);
|
||||
} catch (error) {
|
||||
handleError(
|
||||
error,
|
||||
"PublishNotebookPaneAdapter/isCodeOfConductAccepted",
|
||||
"Failed to check if code of conduct was accepted",
|
||||
);
|
||||
}
|
||||
};
|
||||
const [notebookObject, setNotebookObject] = useState<ImmutableNotebook>();
|
||||
useEffect(() => {
|
||||
CodeOfConductAccepted();
|
||||
let newContent;
|
||||
if (typeof notebookContent === "string") {
|
||||
newContent = notebookContent as string;
|
||||
} else {
|
||||
newContent = JSON.stringify(toJS(notebookContent));
|
||||
setNotebookObject(notebookContent);
|
||||
}
|
||||
setContent(newContent);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setImageSrc(notebookSnapshot);
|
||||
}, [notebookSnapshot]);
|
||||
|
||||
useEffect(() => {
|
||||
setFormError(notebookSnapshotError);
|
||||
}, [notebookSnapshotError]);
|
||||
|
||||
const submit = async (): Promise<void> => {
|
||||
const clearPublishingMessage = NotificationConsoleUtils.logConsoleProgress(`Publishing ${name} to gallery`);
|
||||
setIsExecuting(true);
|
||||
|
||||
let startKey: number;
|
||||
|
||||
if (!notebookName || !notebookDescription || !author || !imageSrc) {
|
||||
setFormError(t(Keys.panes.publishNotebook.publishFailedError, { notebookName }));
|
||||
setFormErrorDetail("Name, description, author and cover image are required");
|
||||
createFormError(formError, formErrorDetail, "PublishNotebookPaneAdapter/submit");
|
||||
setIsExecuting(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
startKey = traceStart(Action.NotebooksGalleryPublish, {});
|
||||
|
||||
const response = await junoClient.publishNotebook(
|
||||
notebookName,
|
||||
notebookDescription,
|
||||
notebookTags?.split(","),
|
||||
imageSrc,
|
||||
content,
|
||||
);
|
||||
|
||||
const data = response.data;
|
||||
if (data) {
|
||||
let isPublishPending = false;
|
||||
|
||||
if (data.pendingScanJobIds?.length > 0) {
|
||||
isPublishPending = true;
|
||||
NotificationConsoleUtils.logConsoleInfo(
|
||||
`Content of ${name} is currently being scanned for illegal content. It will not be available in the public gallery until the review is complete (may take a few days).`,
|
||||
);
|
||||
} else {
|
||||
NotificationConsoleUtils.logConsoleInfo(`Published ${notebookName} to gallery`);
|
||||
container.openGallery(GalleryTab.Published);
|
||||
}
|
||||
|
||||
traceSuccess(
|
||||
Action.NotebooksGalleryPublish,
|
||||
{
|
||||
notebookId: data.id,
|
||||
isPublishPending,
|
||||
},
|
||||
startKey,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
traceFailure(
|
||||
Action.NotebooksGalleryPublish,
|
||||
{
|
||||
error: getErrorMessage(error),
|
||||
errorStack: getErrorStack(error),
|
||||
},
|
||||
startKey,
|
||||
);
|
||||
|
||||
const errorMessage = getErrorMessage(error);
|
||||
setFormError(
|
||||
t(Keys.panes.publishNotebook.publishFailedError, {
|
||||
notebookName: FileSystemUtil.stripExtension(notebookName, "ipynb"),
|
||||
}),
|
||||
);
|
||||
setFormErrorDetail(`${errorMessage}`);
|
||||
handleError(errorMessage, "PublishNotebookPaneAdapter/submit", formError);
|
||||
return;
|
||||
} finally {
|
||||
clearPublishingMessage();
|
||||
setIsExecuting(false);
|
||||
}
|
||||
closeSidePanel();
|
||||
};
|
||||
|
||||
const createFormError = (formError: string, formErrorDetail: string, area: string): void => {
|
||||
setFormError(formError);
|
||||
setFormErrorDetail(formErrorDetail);
|
||||
handleError(formErrorDetail, area, formError);
|
||||
};
|
||||
|
||||
const clearFormError = (): void => {
|
||||
setFormError("");
|
||||
setFormErrorDetail("");
|
||||
};
|
||||
|
||||
const props: RightPaneFormProps = {
|
||||
formError: formError,
|
||||
isExecuting: isExecuting,
|
||||
submitButtonText: "Publish",
|
||||
onSubmit: () => submit(),
|
||||
isSubmitButtonHidden: !isCodeOfConductAccepted,
|
||||
};
|
||||
|
||||
const publishNotebookPaneProps: PublishNotebookPaneProps = {
|
||||
notebookDescription,
|
||||
notebookTags,
|
||||
imageSrc,
|
||||
notebookName,
|
||||
notebookAuthor: author,
|
||||
notebookCreatedDate: new Date().toISOString(),
|
||||
notebookObject: notebookObject,
|
||||
notebookContentRef,
|
||||
onError: createFormError,
|
||||
clearFormError: clearFormError,
|
||||
setNotebookName,
|
||||
setNotebookDescription,
|
||||
setNotebookTags,
|
||||
setImageSrc,
|
||||
onTakeSnapshot,
|
||||
};
|
||||
return (
|
||||
<RightPaneForm {...props}>
|
||||
{!isCodeOfConductAccepted ? (
|
||||
<div style={{ padding: "25px", marginTop: "10px" }}>
|
||||
<CodeOfConduct
|
||||
junoClient={junoClient}
|
||||
onAcceptCodeOfConduct={(isAccepted) => {
|
||||
setIsCodeOfConductAccepted(isAccepted);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<PublishNotebookPaneComponent {...publishNotebookPaneProps} />
|
||||
)}
|
||||
</RightPaneForm>
|
||||
);
|
||||
};
|
||||
@@ -1,264 +0,0 @@
|
||||
import { Dropdown, IDropdownProps, ITextFieldProps, Stack, Text, TextField } from "@fluentui/react";
|
||||
import { ImmutableNotebook } from "@nteract/commutable";
|
||||
import React, { FunctionComponent, useState } from "react";
|
||||
import { Keys, t } from "Localization";
|
||||
import { GalleryCardComponent } from "../../Controls/NotebookGallery/Cards/GalleryCardComponent";
|
||||
import * as FileSystemUtil from "../../Notebook/FileSystemUtil";
|
||||
import { SnapshotRequest } from "../../Notebook/NotebookComponent/types";
|
||||
import { NotebookUtil } from "../../Notebook/NotebookUtil";
|
||||
import "./styled.less";
|
||||
|
||||
export interface PublishNotebookPaneProps {
|
||||
notebookName: string;
|
||||
notebookAuthor: string;
|
||||
notebookTags: string;
|
||||
notebookDescription: string;
|
||||
notebookCreatedDate: string;
|
||||
notebookObject: ImmutableNotebook;
|
||||
notebookContentRef: string;
|
||||
imageSrc: string;
|
||||
|
||||
onError: (formError: string, formErrorDetail: string, area: string) => void;
|
||||
clearFormError: () => void;
|
||||
setNotebookName: (newValue: string) => void;
|
||||
setNotebookDescription: (newValue: string) => void;
|
||||
setNotebookTags: (newValue: string) => void;
|
||||
setImageSrc: (newValue: string) => void;
|
||||
onTakeSnapshot: (request: SnapshotRequest) => void;
|
||||
}
|
||||
|
||||
enum ImageTypes {
|
||||
Url = "URL",
|
||||
CustomImage = "Custom Image",
|
||||
TakeScreenshot = "Take Screenshot",
|
||||
UseFirstDisplayOutput = "Use First Display Output",
|
||||
}
|
||||
|
||||
export const PublishNotebookPaneComponent: FunctionComponent<PublishNotebookPaneProps> = ({
|
||||
notebookName,
|
||||
notebookTags,
|
||||
notebookDescription,
|
||||
notebookAuthor,
|
||||
notebookCreatedDate,
|
||||
notebookObject,
|
||||
notebookContentRef,
|
||||
imageSrc,
|
||||
onError,
|
||||
clearFormError,
|
||||
setNotebookName,
|
||||
setNotebookDescription,
|
||||
setNotebookTags,
|
||||
setImageSrc,
|
||||
onTakeSnapshot,
|
||||
}: PublishNotebookPaneProps) => {
|
||||
const [type, setType] = useState<string>(ImageTypes.CustomImage);
|
||||
const CARD_WIDTH = 256;
|
||||
const cardImageHeight = 144;
|
||||
const cardHeightToWidthRatio = cardImageHeight / CARD_WIDTH;
|
||||
|
||||
const maxImageSizeInMib = 1.5;
|
||||
|
||||
const descriptionPara1 = t(Keys.panes.publishNotebook.publishDescription);
|
||||
|
||||
const descriptionPara2 = t(Keys.panes.publishNotebook.publishPrompt, {
|
||||
name: FileSystemUtil.stripExtension(notebookName, "ipynb"),
|
||||
});
|
||||
|
||||
const options: ImageTypes[] = [ImageTypes.CustomImage, ImageTypes.Url];
|
||||
if (onTakeSnapshot) {
|
||||
options.push(ImageTypes.TakeScreenshot);
|
||||
if (notebookObject) {
|
||||
options.push(ImageTypes.UseFirstDisplayOutput);
|
||||
}
|
||||
}
|
||||
|
||||
const thumbnailSelectorProps: IDropdownProps = {
|
||||
label: t(Keys.panes.publishNotebook.coverImage),
|
||||
selectedKey: type,
|
||||
ariaLabel: t(Keys.panes.publishNotebook.coverImage),
|
||||
options: options.map((value: string) => ({ text: value, key: value })),
|
||||
onChange: async (event, options) => {
|
||||
setImageSrc("");
|
||||
clearFormError();
|
||||
if (options.text === ImageTypes.TakeScreenshot) {
|
||||
onTakeSnapshot({
|
||||
aspectRatio: cardHeightToWidthRatio,
|
||||
requestId: new Date().getTime().toString(),
|
||||
type: "notebook",
|
||||
notebookContentRef,
|
||||
});
|
||||
} else if (options.text === ImageTypes.UseFirstDisplayOutput) {
|
||||
const cellIds = NotebookUtil.findCodeCellWithDisplay(notebookObject);
|
||||
if (cellIds.length > 0) {
|
||||
onTakeSnapshot({
|
||||
aspectRatio: cardHeightToWidthRatio,
|
||||
requestId: new Date().getTime().toString(),
|
||||
type: "celloutput",
|
||||
cellId: cellIds[0],
|
||||
notebookContentRef,
|
||||
});
|
||||
} else {
|
||||
firstOutputErrorHandler(new Error(t(Keys.panes.publishNotebook.outputDoesNotExist)));
|
||||
}
|
||||
}
|
||||
setType(options.text);
|
||||
},
|
||||
};
|
||||
|
||||
const thumbnailUrlProps: ITextFieldProps = {
|
||||
label: t(Keys.panes.publishNotebook.coverImageUrl),
|
||||
ariaLabel: t(Keys.panes.publishNotebook.coverImageUrl),
|
||||
required: true,
|
||||
onChange: (event, newValue) => {
|
||||
setImageSrc(newValue);
|
||||
},
|
||||
};
|
||||
|
||||
const firstOutputErrorHandler = (error: Error) => {
|
||||
const formError = t(Keys.panes.publishNotebook.failedToCaptureOutput);
|
||||
const formErrorDetail = `${error}`;
|
||||
const area = "PublishNotebookPaneComponent/UseFirstOutput";
|
||||
onError(formError, formErrorDetail, area);
|
||||
};
|
||||
|
||||
const imageToBase64 = (file: File, updateImageSrc: (result: string) => void) => {
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
reader.onload = () => {
|
||||
updateImageSrc(reader.result.toString());
|
||||
};
|
||||
|
||||
reader.onerror = (error) => {
|
||||
const formError = t(Keys.panes.publishNotebook.failedToConvertError, { fileName: file.name });
|
||||
const formErrorDetail = `${error}`;
|
||||
const area = "PublishNotebookPaneComponent/selectImageFile";
|
||||
onError(formError, formErrorDetail, area);
|
||||
};
|
||||
};
|
||||
|
||||
const renderThumbnailSelectors = (type: string) => {
|
||||
switch (type) {
|
||||
case ImageTypes.Url:
|
||||
return <TextField {...thumbnailUrlProps} />;
|
||||
case ImageTypes.CustomImage:
|
||||
return (
|
||||
<input
|
||||
id="selectImageFile"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={(event) => {
|
||||
const file = event.target.files[0];
|
||||
if (file.size / 1024 ** 2 > maxImageSizeInMib) {
|
||||
event.target.value = "";
|
||||
const formError = t(Keys.panes.publishNotebook.failedToUploadError, { fileName: file.name });
|
||||
const formErrorDetail = `Image is larger than ${maxImageSizeInMib} MiB. Please Choose a different image.`;
|
||||
const area = "PublishNotebookPaneComponent/selectImageFile";
|
||||
|
||||
onError(formError, formErrorDetail, area);
|
||||
setImageSrc("");
|
||||
return;
|
||||
} else {
|
||||
clearFormError();
|
||||
}
|
||||
imageToBase64(file, (result: string) => {
|
||||
setImageSrc(result);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return <></>;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="publishNotebookPanelContent">
|
||||
<Stack className="panelMainContent" tokens={{ childrenGap: 20 }}>
|
||||
<Stack.Item>
|
||||
<Text>{descriptionPara1}</Text>
|
||||
</Stack.Item>
|
||||
|
||||
<Stack.Item>
|
||||
<Text>{descriptionPara2}</Text>
|
||||
</Stack.Item>
|
||||
|
||||
<Stack.Item>
|
||||
<TextField
|
||||
label={t(Keys.panes.publishNotebook.name)}
|
||||
ariaLabel={t(Keys.panes.publishNotebook.name)}
|
||||
defaultValue={FileSystemUtil.stripExtension(notebookName, "ipynb")}
|
||||
required
|
||||
onChange={(event, newValue) => {
|
||||
const notebookName = newValue + ".ipynb";
|
||||
setNotebookName(notebookName);
|
||||
}}
|
||||
/>
|
||||
</Stack.Item>
|
||||
|
||||
<Stack.Item>
|
||||
<TextField
|
||||
label={t(Keys.panes.publishNotebook.description)}
|
||||
ariaLabel={t(Keys.panes.publishNotebook.description)}
|
||||
multiline
|
||||
rows={3}
|
||||
required
|
||||
onChange={(event, newValue) => {
|
||||
setNotebookDescription(newValue);
|
||||
}}
|
||||
/>
|
||||
</Stack.Item>
|
||||
|
||||
<Stack.Item>
|
||||
<TextField
|
||||
label={t(Keys.panes.publishNotebook.tags)}
|
||||
ariaLabel={t(Keys.panes.publishNotebook.tags)}
|
||||
placeholder={t(Keys.panes.publishNotebook.tagsPlaceholder)}
|
||||
onChange={(event, newValue) => {
|
||||
setNotebookTags(newValue);
|
||||
}}
|
||||
/>
|
||||
</Stack.Item>
|
||||
|
||||
<Stack.Item>
|
||||
<Dropdown {...thumbnailSelectorProps} />
|
||||
</Stack.Item>
|
||||
|
||||
<Stack.Item>{renderThumbnailSelectors(type)}</Stack.Item>
|
||||
|
||||
<Stack.Item>
|
||||
<Text>{t(Keys.panes.publishNotebook.preview)}</Text>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<GalleryCardComponent
|
||||
data={{
|
||||
id: undefined,
|
||||
name: notebookName,
|
||||
description: notebookDescription,
|
||||
gitSha: undefined,
|
||||
tags: notebookTags.split(","),
|
||||
author: notebookAuthor,
|
||||
thumbnailUrl: imageSrc,
|
||||
created: notebookCreatedDate,
|
||||
isSample: false,
|
||||
downloads: undefined,
|
||||
favorites: undefined,
|
||||
views: undefined,
|
||||
newCellId: undefined,
|
||||
policyViolations: undefined,
|
||||
pendingScanJobIds: undefined,
|
||||
}}
|
||||
isFavorite={undefined}
|
||||
showDownload={false}
|
||||
showDelete={false}
|
||||
onClick={() => undefined}
|
||||
onTagClick={undefined}
|
||||
onFavoriteClick={undefined}
|
||||
onUnfavoriteClick={undefined}
|
||||
onDownloadClick={undefined}
|
||||
onDeleteClick={undefined}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
-116
@@ -1,116 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`PublishNotebookPaneComponent renders 1`] = `
|
||||
<div
|
||||
className="publishNotebookPanelContent"
|
||||
>
|
||||
<Stack
|
||||
className="panelMainContent"
|
||||
tokens={
|
||||
{
|
||||
"childrenGap": 20,
|
||||
}
|
||||
}
|
||||
>
|
||||
<StackItem>
|
||||
<Text>
|
||||
When published, this notebook will appear in the Azure Cosmos DB notebooks public gallery. Make sure you have removed any sensitive data or output before publishing.
|
||||
</Text>
|
||||
</StackItem>
|
||||
<StackItem>
|
||||
<Text>
|
||||
Would you like to publish and share "SampleNotebook" to the gallery?
|
||||
</Text>
|
||||
</StackItem>
|
||||
<StackItem>
|
||||
<StyledTextFieldBase
|
||||
ariaLabel="Name"
|
||||
defaultValue="SampleNotebook"
|
||||
label="Name"
|
||||
onChange={[Function]}
|
||||
required={true}
|
||||
/>
|
||||
</StackItem>
|
||||
<StackItem>
|
||||
<StyledTextFieldBase
|
||||
ariaLabel="Description"
|
||||
label="Description"
|
||||
multiline={true}
|
||||
onChange={[Function]}
|
||||
required={true}
|
||||
rows={3}
|
||||
/>
|
||||
</StackItem>
|
||||
<StackItem>
|
||||
<StyledTextFieldBase
|
||||
ariaLabel="Tags"
|
||||
label="Tags"
|
||||
onChange={[Function]}
|
||||
placeholder="Optional tag 1, Optional tag 2"
|
||||
/>
|
||||
</StackItem>
|
||||
<StackItem>
|
||||
<Dropdown
|
||||
ariaLabel="Cover image"
|
||||
label="Cover image"
|
||||
onChange={[Function]}
|
||||
options={
|
||||
[
|
||||
{
|
||||
"key": "Custom Image",
|
||||
"text": "Custom Image",
|
||||
},
|
||||
{
|
||||
"key": "URL",
|
||||
"text": "URL",
|
||||
},
|
||||
]
|
||||
}
|
||||
selectedKey="Custom Image"
|
||||
/>
|
||||
</StackItem>
|
||||
<StackItem>
|
||||
<input
|
||||
accept="image/*"
|
||||
id="selectImageFile"
|
||||
onChange={[Function]}
|
||||
type="file"
|
||||
/>
|
||||
</StackItem>
|
||||
<StackItem>
|
||||
<Text>
|
||||
Preview
|
||||
</Text>
|
||||
</StackItem>
|
||||
<StackItem>
|
||||
<GalleryCardComponent
|
||||
data={
|
||||
{
|
||||
"author": "CosmosDB",
|
||||
"created": "2020-07-17T00:00:00Z",
|
||||
"description": "sample description",
|
||||
"downloads": undefined,
|
||||
"favorites": undefined,
|
||||
"gitSha": undefined,
|
||||
"id": undefined,
|
||||
"isSample": false,
|
||||
"name": "SampleNotebook.ipynb",
|
||||
"newCellId": undefined,
|
||||
"pendingScanJobIds": undefined,
|
||||
"policyViolations": undefined,
|
||||
"tags": [
|
||||
"tag1",
|
||||
" tag2",
|
||||
],
|
||||
"thumbnailUrl": "https://i.ytimg.com/vi/E_lByLdKeKY/maxresdefault.jpg",
|
||||
"views": undefined,
|
||||
}
|
||||
}
|
||||
onClick={[Function]}
|
||||
showDelete={false}
|
||||
showDownload={false}
|
||||
/>
|
||||
</StackItem>
|
||||
</Stack>
|
||||
</div>
|
||||
`;
|
||||
@@ -1,6 +0,0 @@
|
||||
.publishNotebookPanelContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import React from "react";
|
||||
import type { DatabaseAccount } from "../../Contracts/DataModels";
|
||||
import type { TabOptions } from "../../Contracts/ViewModels";
|
||||
import type { IGalleryItem, JunoClient } from "../../Juno/JunoClient";
|
||||
import { GalleryAndNotebookViewerComponent as GalleryViewer } from "../Controls/NotebookGallery/GalleryAndNotebookViewerComponent";
|
||||
import type { GalleryTab as GalleryViewerTab } from "../Controls/NotebookGallery/GalleryViewerComponent";
|
||||
import { SortBy } from "../Controls/NotebookGallery/GalleryViewerComponent";
|
||||
import type Explorer from "../Explorer";
|
||||
import TabsBase from "./TabsBase";
|
||||
|
||||
interface Props {
|
||||
account: DatabaseAccount;
|
||||
container: Explorer;
|
||||
junoClient: JunoClient;
|
||||
selectedTab: GalleryViewerTab;
|
||||
notebookUrl?: string;
|
||||
galleryItem?: IGalleryItem;
|
||||
isFavorite?: boolean;
|
||||
}
|
||||
|
||||
export default class GalleryTab extends TabsBase {
|
||||
constructor(
|
||||
options: TabOptions,
|
||||
private props: Props,
|
||||
) {
|
||||
super(options);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return <GalleryViewer {...this.props} sortBy={SortBy.MostRecent} searchText={undefined} />;
|
||||
}
|
||||
|
||||
public getContainer(): Explorer {
|
||||
return this.props.container;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import { stringifyNotebook, toJS } from "@nteract/commutable";
|
||||
import * as ko from "knockout";
|
||||
import * as Q from "q";
|
||||
import { userContext } from "UserContext";
|
||||
import ClearAllOutputsIcon from "../../../images/notebook/Notebook-clear-all-outputs.svg";
|
||||
import CopyIcon from "../../../images/notebook/Notebook-copy.svg";
|
||||
import CutIcon from "../../../images/notebook/Notebook-cut.svg";
|
||||
@@ -12,8 +11,7 @@ import RunAllIcon from "../../../images/notebook/Notebook-run-all.svg";
|
||||
import RunIcon from "../../../images/notebook/Notebook-run.svg";
|
||||
import { default as InterruptKernelIcon, default as KillKernelIcon } from "../../../images/notebook/Notebook-stop.svg";
|
||||
import SaveIcon from "../../../images/save-cosmos.svg";
|
||||
import { useNotebookSnapshotStore } from "../../hooks/useNotebookSnapshotStore";
|
||||
import { Action, ActionModifiers, Source } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
import * as NotebookConfigurationUtils from "../../Utils/NotebookConfigurationUtils";
|
||||
import { logConsoleInfo } from "../../Utils/NotificationConsoleUtils";
|
||||
@@ -21,9 +19,7 @@ import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandBu
|
||||
import { useDialog } from "../Controls/Dialog";
|
||||
import * as CommandBarComponentButtonFactory from "../Menus/CommandBar/CommandBarComponentButtonFactory";
|
||||
import { KernelSpecsDisplay } from "../Notebook/NotebookClientV2";
|
||||
import * as CdbActions from "../Notebook/NotebookComponent/actions";
|
||||
import { NotebookComponentAdapter } from "../Notebook/NotebookComponent/NotebookComponentAdapter";
|
||||
import { CdbAppState, SnapshotRequest } from "../Notebook/NotebookComponent/types";
|
||||
import { NotebookContentItem } from "../Notebook/NotebookContentItem";
|
||||
import { NotebookUtil } from "../Notebook/NotebookUtil";
|
||||
import { useNotebook } from "../Notebook/useNotebook";
|
||||
@@ -97,7 +93,6 @@ export default class NotebookTabV2 extends NotebookTabBase {
|
||||
|
||||
const saveLabel = "Save";
|
||||
const copyToLabel = "Copy to ...";
|
||||
const publishLabel = "Publish to gallery";
|
||||
const kernelLabel = "No Kernel";
|
||||
const runLabel = "Run";
|
||||
const runActiveCellLabel = "Run Active Cell";
|
||||
@@ -130,17 +125,6 @@ export default class NotebookTabV2 extends NotebookTabBase {
|
||||
});
|
||||
}
|
||||
|
||||
if (userContext.features.publicGallery) {
|
||||
saveButtonChildren.push({
|
||||
iconName: "PublishContent",
|
||||
onCommandClick: async () => await this.publishToGallery(),
|
||||
commandButtonLabel: publishLabel,
|
||||
hasPopup: false,
|
||||
disabled: false,
|
||||
ariaLabel: publishLabel,
|
||||
});
|
||||
}
|
||||
|
||||
let buttons: CommandButtonComponentProps[] = [
|
||||
{
|
||||
iconSrc: SaveIcon,
|
||||
@@ -383,40 +367,6 @@ export default class NotebookTabV2 extends NotebookTabBase {
|
||||
);
|
||||
}
|
||||
|
||||
private publishToGallery = async () => {
|
||||
TelemetryProcessor.trace(Action.NotebooksGalleryClickPublishToGallery, ActionModifiers.Mark, {
|
||||
source: Source.CommandBarMenu,
|
||||
});
|
||||
|
||||
const notebookReduxStore = NotebookTabV2.clientManager.getStore();
|
||||
const unsubscribe = notebookReduxStore.subscribe(() => {
|
||||
const cdbState = (notebookReduxStore.getState() as CdbAppState).cdb;
|
||||
useNotebookSnapshotStore.setState({
|
||||
snapshot: cdbState.notebookSnapshot?.imageSrc,
|
||||
error: cdbState.notebookSnapshotError,
|
||||
});
|
||||
});
|
||||
|
||||
const notebookContent = this.notebookComponentAdapter.getContent();
|
||||
const notebookContentRef = this.notebookComponentAdapter.contentRef;
|
||||
const onPanelClose = (): void => {
|
||||
unsubscribe();
|
||||
useNotebookSnapshotStore.setState({
|
||||
snapshot: undefined,
|
||||
error: undefined,
|
||||
});
|
||||
notebookReduxStore.dispatch(CdbActions.takeNotebookSnapshot(undefined));
|
||||
};
|
||||
|
||||
await this.container.publishNotebook(
|
||||
notebookContent.name,
|
||||
notebookContent.content,
|
||||
notebookContentRef,
|
||||
(request: SnapshotRequest) => notebookReduxStore.dispatch(CdbActions.takeNotebookSnapshot(request)),
|
||||
onPanelClose,
|
||||
);
|
||||
};
|
||||
|
||||
private copyNotebook = () => {
|
||||
const notebookContent = this.notebookComponentAdapter.getContent();
|
||||
let content: string;
|
||||
|
||||
@@ -10,7 +10,6 @@ import CopyIcon from "../../../images/notebook/Notebook-copy.svg";
|
||||
import NewNotebookIcon from "../../../images/notebook/Notebook-new.svg";
|
||||
import NotebookIcon from "../../../images/notebook/Notebook-resource.svg";
|
||||
import FileIcon from "../../../images/notebook/file-cosmos.svg";
|
||||
import PublishIcon from "../../../images/notebook/publish_content.svg";
|
||||
import RefreshIcon from "../../../images/refresh-cosmos.svg";
|
||||
import CollectionIcon from "../../../images/tree-collection.svg";
|
||||
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
|
||||
@@ -18,7 +17,7 @@ import { isPublicInternetAccessAllowed } from "../../Common/DatabaseAccountUtili
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { IPinnedRepo } from "../../Juno/JunoClient";
|
||||
import { Action, ActionModifiers, Source } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { isServerlessAccount } from "../../Utils/CapabilityUtils";
|
||||
@@ -49,7 +48,6 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
||||
|
||||
public parameters: ko.Observable<number>;
|
||||
|
||||
public galleryContentRoot: NotebookContentItem;
|
||||
public myNotebooksContentRoot: NotebookContentItem;
|
||||
public gitHubNotebooksContentRoot: NotebookContentItem;
|
||||
|
||||
@@ -102,11 +100,6 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
||||
public async initialize(): Promise<void[]> {
|
||||
const refreshTasks: Promise<void>[] = [];
|
||||
|
||||
this.galleryContentRoot = {
|
||||
name: "Gallery",
|
||||
path: "Gallery",
|
||||
type: NotebookContentItemType.File,
|
||||
};
|
||||
this.myNotebooksContentRoot = {
|
||||
name: useNotebook.getState().notebookFolderName,
|
||||
path: useNotebook.getState().notebookBasePath,
|
||||
@@ -538,20 +531,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
||||
];
|
||||
|
||||
if (item.type === NotebookContentItemType.Notebook) {
|
||||
items.push({
|
||||
label: "Publish to gallery",
|
||||
iconSrc: PublishIcon,
|
||||
onClick: async () => {
|
||||
TelemetryProcessor.trace(Action.NotebooksGalleryClickPublishToGallery, ActionModifiers.Mark, {
|
||||
source: Source.ResourceTreeMenu,
|
||||
});
|
||||
|
||||
const content = await this.container.readFile(item);
|
||||
if (content) {
|
||||
await this.container.publishNotebook(item.name, content);
|
||||
}
|
||||
},
|
||||
});
|
||||
// Additional notebook-specific context menu items can be added here
|
||||
}
|
||||
|
||||
// "Copy to ..." isn't needed if github locations are not available
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
@import "../../less/Common/Constants";
|
||||
|
||||
.standalone-gallery-root {
|
||||
background: @GalleryBackgroundColor;
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
import { initializeIcons, Link, Text } from "@fluentui/react";
|
||||
import "bootstrap/dist/css/bootstrap.css";
|
||||
import * as React from "react";
|
||||
import * as ReactDOM from "react-dom";
|
||||
import { userContext } from "UserContext";
|
||||
import { initializeConfiguration } from "../ConfigContext";
|
||||
import { GalleryHeaderComponent } from "../Explorer/Controls/Header/GalleryHeaderComponent";
|
||||
import {
|
||||
GalleryAndNotebookViewerComponent,
|
||||
GalleryAndNotebookViewerComponentProps,
|
||||
} from "../Explorer/Controls/NotebookGallery/GalleryAndNotebookViewerComponent";
|
||||
import { GalleryTab, SortBy } from "../Explorer/Controls/NotebookGallery/GalleryViewerComponent";
|
||||
import { JunoClient } from "../Juno/JunoClient";
|
||||
import * as GalleryUtils from "../Utils/GalleryUtils";
|
||||
import "./GalleryViewer.less";
|
||||
|
||||
const enableNotebooksUrl = "https://aka.ms/cosmos-enable-notebooks";
|
||||
const createAccountUrl = "https://aka.ms/cosmos-create-account-portal";
|
||||
|
||||
const onInit = async () => {
|
||||
const dataExplorerUrl = new URL("./", window.location.href).href;
|
||||
|
||||
initializeIcons();
|
||||
await initializeConfiguration();
|
||||
const galleryViewerProps = GalleryUtils.getGalleryViewerProps(window.location.search);
|
||||
|
||||
const props: GalleryAndNotebookViewerComponentProps = {
|
||||
junoClient: new JunoClient(),
|
||||
selectedTab:
|
||||
galleryViewerProps.selectedTab ||
|
||||
(userContext.features.publicGallery ? GalleryTab.PublicGallery : GalleryTab.OfficialSamples),
|
||||
sortBy: galleryViewerProps.sortBy || SortBy.MostRecent,
|
||||
searchText: galleryViewerProps.searchText,
|
||||
};
|
||||
|
||||
const element = (
|
||||
<div className="standalone-gallery-root">
|
||||
<header>
|
||||
<GalleryHeaderComponent />
|
||||
</header>
|
||||
<div style={{ margin: "auto", width: "85%" }}>
|
||||
<div style={{ paddingLeft: 26, paddingRight: 26, paddingTop: 20 }}>
|
||||
<Text block>
|
||||
Welcome to the Azure Cosmos DB notebooks gallery! View the sample notebooks to learn about use cases, best
|
||||
practices, and how to get started with Azure Cosmos DB.
|
||||
</Text>
|
||||
<Text styles={{ root: { marginTop: 10 } }} block>
|
||||
If {`you'd`} like to run or edit the notebook in your own Azure Cosmos DB account,{" "}
|
||||
<Link href={dataExplorerUrl}>sign in</Link> and select an account with{" "}
|
||||
<Link href={enableNotebooksUrl}>notebooks enabled</Link>. From there, you can download the sample to your
|
||||
account. If you {`don't`} have an account yet, you can{" "}
|
||||
<Link href={createAccountUrl}>create one from the Azure portal</Link>.
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<GalleryAndNotebookViewerComponent {...props} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
ReactDOM.render(element, document.getElementById("galleryContent"));
|
||||
};
|
||||
|
||||
// Entry point
|
||||
window.addEventListener("load", onInit);
|
||||
@@ -1,13 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="height=device-height, width=device-width, initial-scale=1.0" />
|
||||
<title>Gallery Viewer</title>
|
||||
<link rel="shortcut icon" href="../../images/CosmosDB_rgb_ui_lighttheme.ico" type="image/x-icon" />
|
||||
</head>
|
||||
|
||||
<body style="overflow-y: scroll">
|
||||
<div class="galleryContent" id="galleryContent"></div>
|
||||
</body>
|
||||
</html>
|
||||
+2
-297
@@ -1,24 +1,5 @@
|
||||
import { HttpHeaders, HttpStatusCodes } from "../Common/Constants";
|
||||
import { DatabaseAccount } from "../Contracts/DataModels";
|
||||
import { updateUserContext, userContext } from "../UserContext";
|
||||
import { getAuthorizationHeader } from "../Utils/AuthorizationUtils";
|
||||
import { IPinnedRepo, IPublishNotebookRequest, JunoClient } from "./JunoClient";
|
||||
|
||||
const sampleSubscriptionId = "subscriptionId";
|
||||
|
||||
const sampleDatabaseAccount: DatabaseAccount = {
|
||||
id: "id",
|
||||
name: "name",
|
||||
location: "location",
|
||||
type: "type",
|
||||
kind: "kind",
|
||||
properties: {
|
||||
documentEndpoint: "documentEndpoint",
|
||||
gremlinEndpoint: "gremlinEndpoint",
|
||||
tableEndpoint: "tableEndpoint",
|
||||
cassandraEndpoint: "cassandraEndpoint",
|
||||
},
|
||||
};
|
||||
import { HttpStatusCodes } from "../Common/Constants";
|
||||
import { IPinnedRepo, JunoClient } from "./JunoClient";
|
||||
|
||||
const samplePinnedRepos: IPinnedRepo[] = [
|
||||
{
|
||||
@@ -130,279 +111,3 @@ describe("GitHub", () => {
|
||||
expect(fetchUrlParams.get("client_id")).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Gallery", () => {
|
||||
const junoClient = new JunoClient();
|
||||
const originalSubscriptionId = userContext.subscriptionId;
|
||||
|
||||
beforeAll(() => {
|
||||
updateUserContext({
|
||||
databaseAccount: {
|
||||
name: "name",
|
||||
} as DatabaseAccount,
|
||||
subscriptionId: sampleSubscriptionId,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
updateUserContext({ subscriptionId: originalSubscriptionId });
|
||||
});
|
||||
|
||||
it("getSampleNotebooks", async () => {
|
||||
window.fetch = jest.fn().mockReturnValue({
|
||||
status: HttpStatusCodes.OK,
|
||||
json: () => undefined as undefined,
|
||||
});
|
||||
|
||||
const response = await junoClient.getSampleNotebooks();
|
||||
|
||||
expect(response.status).toBe(HttpStatusCodes.OK);
|
||||
expect(window.fetch).toHaveBeenCalledWith(
|
||||
`${JunoClient.getJunoEndpoint()}/api/notebooks/gallery/samples`,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it("getPublicNotebooks", async () => {
|
||||
window.fetch = jest.fn().mockReturnValue({
|
||||
status: HttpStatusCodes.OK,
|
||||
json: () => undefined as undefined,
|
||||
});
|
||||
|
||||
const response = await junoClient.getPublicNotebooks();
|
||||
|
||||
expect(response.status).toBe(HttpStatusCodes.OK);
|
||||
expect(window.fetch).toHaveBeenCalledWith(
|
||||
`${JunoClient.getJunoEndpoint()}/api/notebooks/gallery/public`,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it("getNotebook", async () => {
|
||||
const id = "id";
|
||||
window.fetch = jest.fn().mockReturnValue({
|
||||
status: HttpStatusCodes.OK,
|
||||
json: () => undefined as undefined,
|
||||
});
|
||||
|
||||
const response = await junoClient.getNotebookInfo(id);
|
||||
|
||||
expect(response.status).toBe(HttpStatusCodes.OK);
|
||||
expect(window.fetch).toHaveBeenCalledWith(`${JunoClient.getJunoEndpoint()}/api/notebooks/gallery/${id}`);
|
||||
});
|
||||
|
||||
it("getNotebookContent", async () => {
|
||||
const id = "id";
|
||||
window.fetch = jest.fn().mockReturnValue({
|
||||
status: HttpStatusCodes.OK,
|
||||
text: () => undefined as undefined,
|
||||
});
|
||||
|
||||
const response = await junoClient.getNotebookContent(id);
|
||||
|
||||
expect(response.status).toBe(HttpStatusCodes.OK);
|
||||
expect(window.fetch).toHaveBeenCalledWith(`${JunoClient.getJunoEndpoint()}/api/notebooks/gallery/${id}/content`);
|
||||
});
|
||||
|
||||
it("increaseNotebookViews", async () => {
|
||||
const id = "id";
|
||||
window.fetch = jest.fn().mockReturnValue({
|
||||
status: HttpStatusCodes.OK,
|
||||
json: () => undefined as undefined,
|
||||
});
|
||||
const response = await junoClient.increaseNotebookViews(id);
|
||||
|
||||
expect(response.status).toBe(HttpStatusCodes.OK);
|
||||
expect(window.fetch).toHaveBeenCalledWith(`${JunoClient.getJunoEndpoint()}/api/notebooks/gallery/${id}/views`, {
|
||||
method: "PATCH",
|
||||
});
|
||||
});
|
||||
|
||||
it("increaseNotebookDownloadCount", async () => {
|
||||
const id = "id";
|
||||
window.fetch = jest.fn().mockReturnValue({
|
||||
status: HttpStatusCodes.OK,
|
||||
json: () => undefined as undefined,
|
||||
});
|
||||
|
||||
const response = await junoClient.increaseNotebookDownloadCount(id);
|
||||
|
||||
const authorizationHeader = getAuthorizationHeader();
|
||||
expect(response.status).toBe(HttpStatusCodes.OK);
|
||||
expect(window.fetch).toHaveBeenCalledWith(
|
||||
`${JunoClient.getJunoEndpoint()}/api/notebooks/subscriptions/${sampleSubscriptionId}/databaseAccounts/${
|
||||
sampleDatabaseAccount.name
|
||||
}/gallery/${id}/downloads`,
|
||||
{
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
[authorizationHeader.header]: authorizationHeader.token,
|
||||
[HttpHeaders.contentType]: "application/json",
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("favoriteNotebook", async () => {
|
||||
const id = "id";
|
||||
window.fetch = jest.fn().mockReturnValue({
|
||||
status: HttpStatusCodes.OK,
|
||||
json: () => undefined as undefined,
|
||||
});
|
||||
|
||||
const response = await junoClient.favoriteNotebook(id);
|
||||
|
||||
const authorizationHeader = getAuthorizationHeader();
|
||||
expect(response.status).toBe(HttpStatusCodes.OK);
|
||||
expect(window.fetch).toHaveBeenCalledWith(
|
||||
`${JunoClient.getJunoEndpoint()}/api/notebooks/subscriptions/${sampleSubscriptionId}/databaseAccounts/${
|
||||
sampleDatabaseAccount.name
|
||||
}/gallery/${id}/favorite`,
|
||||
{
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
[authorizationHeader.header]: authorizationHeader.token,
|
||||
[HttpHeaders.contentType]: "application/json",
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("unfavoriteNotebook", async () => {
|
||||
const id = "id";
|
||||
window.fetch = jest.fn().mockReturnValue({
|
||||
status: HttpStatusCodes.OK,
|
||||
json: () => undefined as undefined,
|
||||
});
|
||||
|
||||
const response = await junoClient.unfavoriteNotebook(id);
|
||||
|
||||
const authorizationHeader = getAuthorizationHeader();
|
||||
expect(response.status).toBe(HttpStatusCodes.OK);
|
||||
expect(window.fetch).toHaveBeenCalledWith(
|
||||
`${JunoClient.getJunoEndpoint()}/api/notebooks/subscriptions/${sampleSubscriptionId}/databaseAccounts/${
|
||||
sampleDatabaseAccount.name
|
||||
}/gallery/${id}/unfavorite`,
|
||||
{
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
[authorizationHeader.header]: authorizationHeader.token,
|
||||
[HttpHeaders.contentType]: "application/json",
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("getFavoriteNotebooks", async () => {
|
||||
window.fetch = jest.fn().mockReturnValue({
|
||||
status: HttpStatusCodes.OK,
|
||||
json: () => undefined as undefined,
|
||||
});
|
||||
|
||||
const response = await junoClient.getFavoriteNotebooks();
|
||||
|
||||
const authorizationHeader = getAuthorizationHeader();
|
||||
expect(response.status).toBe(HttpStatusCodes.OK);
|
||||
expect(window.fetch).toHaveBeenCalledWith(
|
||||
`${JunoClient.getJunoEndpoint()}/api/notebooks/subscriptions/${sampleSubscriptionId}/databaseAccounts/${
|
||||
sampleDatabaseAccount.name
|
||||
}/gallery/favorites`,
|
||||
{
|
||||
headers: {
|
||||
[authorizationHeader.header]: authorizationHeader.token,
|
||||
[HttpHeaders.contentType]: "application/json",
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("getPublishedNotebooks", async () => {
|
||||
window.fetch = jest.fn().mockReturnValue({
|
||||
status: HttpStatusCodes.OK,
|
||||
json: () => undefined as undefined,
|
||||
});
|
||||
|
||||
const response = await junoClient.getPublishedNotebooks();
|
||||
|
||||
const authorizationHeader = getAuthorizationHeader();
|
||||
expect(response.status).toBe(HttpStatusCodes.OK);
|
||||
expect(window.fetch).toHaveBeenCalledWith(
|
||||
`${JunoClient.getJunoEndpoint()}/api/notebooks/subscriptions/${sampleSubscriptionId}/databaseAccounts/${
|
||||
sampleDatabaseAccount.name
|
||||
}/gallery/published`,
|
||||
{
|
||||
headers: {
|
||||
[authorizationHeader.header]: authorizationHeader.token,
|
||||
[HttpHeaders.contentType]: "application/json",
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("deleteNotebook", async () => {
|
||||
const id = "id";
|
||||
window.fetch = jest.fn().mockReturnValue({
|
||||
status: HttpStatusCodes.OK,
|
||||
json: () => undefined as undefined,
|
||||
});
|
||||
|
||||
const response = await junoClient.deleteNotebook(id);
|
||||
|
||||
const authorizationHeader = getAuthorizationHeader();
|
||||
expect(response.status).toBe(HttpStatusCodes.OK);
|
||||
expect(window.fetch).toHaveBeenCalledWith(
|
||||
`${JunoClient.getJunoEndpoint()}/api/notebooks/subscriptions/${sampleSubscriptionId}/databaseAccounts/${
|
||||
sampleDatabaseAccount.name
|
||||
}/gallery/${id}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
[authorizationHeader.header]: authorizationHeader.token,
|
||||
[HttpHeaders.contentType]: "application/json",
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("publishNotebook", async () => {
|
||||
const name = "name";
|
||||
const description = "description";
|
||||
const tags = ["tag"];
|
||||
const thumbnailUrl = "thumbnailUrl";
|
||||
const content = `{ "key": "value" }`;
|
||||
const addLinkToNotebookViewer = true;
|
||||
window.fetch = jest.fn().mockReturnValue({
|
||||
status: HttpStatusCodes.OK,
|
||||
json: () => undefined as undefined,
|
||||
});
|
||||
|
||||
const response = await junoClient.publishNotebook(name, description, tags, thumbnailUrl, content);
|
||||
|
||||
const authorizationHeader = getAuthorizationHeader();
|
||||
expect(response.status).toBe(HttpStatusCodes.OK);
|
||||
expect(window.fetch).toHaveBeenCalledWith(
|
||||
`${JunoClient.getJunoEndpoint()}/api/notebooks/subscriptions/${sampleSubscriptionId}/databaseAccounts/${
|
||||
sampleDatabaseAccount.name
|
||||
}/gallery`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: {
|
||||
[authorizationHeader.header]: authorizationHeader.token,
|
||||
[HttpHeaders.contentType]: "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name,
|
||||
description,
|
||||
tags,
|
||||
thumbnailUrl,
|
||||
content: JSON.parse(content),
|
||||
addLinkToNotebookViewer,
|
||||
} as IPublishNotebookRequest),
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -44,30 +44,6 @@ export interface IGalleryItem {
|
||||
pendingScanJobIds: string[];
|
||||
}
|
||||
|
||||
export interface IPublicGalleryData {
|
||||
metadata: IPublicGalleryMetaData;
|
||||
notebooksData: IGalleryItem[];
|
||||
}
|
||||
|
||||
export interface IPublicGalleryMetaData {
|
||||
acceptedCodeOfConduct: boolean;
|
||||
}
|
||||
|
||||
export interface IUserGallery {
|
||||
favorites: string[];
|
||||
published: string[];
|
||||
}
|
||||
|
||||
// Only exported for unit test
|
||||
export interface IPublishNotebookRequest {
|
||||
name: string;
|
||||
description: string;
|
||||
tags: string[];
|
||||
thumbnailUrl: string;
|
||||
content: unknown;
|
||||
addLinkToNotebookViewer: boolean;
|
||||
}
|
||||
|
||||
export class JunoClient {
|
||||
private cachedPinnedRepos: ko.Observable<IPinnedRepo[]>;
|
||||
|
||||
@@ -176,90 +152,6 @@ export class JunoClient {
|
||||
};
|
||||
}
|
||||
|
||||
public async getSampleNotebooks(): Promise<IJunoResponse<IGalleryItem[]>> {
|
||||
return this.getNotebooks(`${this.getNotebooksUrl()}/gallery/samples`);
|
||||
}
|
||||
|
||||
public async getPublicNotebooks(): Promise<IJunoResponse<IGalleryItem[]>> {
|
||||
return this.getNotebooks(`${this.getNotebooksUrl()}/gallery/public`);
|
||||
}
|
||||
|
||||
public async getPublicGalleryData(): Promise<IJunoResponse<IPublicGalleryData>> {
|
||||
const url = `${this.getNotebooksSubscriptionIdAccountUrl()}/gallery/public`;
|
||||
const response = await window.fetch(url, { headers: JunoClient.getHeaders() });
|
||||
|
||||
let data: IPublicGalleryData;
|
||||
if (response.status === HttpStatusCodes.OK) {
|
||||
data = await response.json();
|
||||
}
|
||||
|
||||
return {
|
||||
status: response.status,
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
public async acceptCodeOfConduct(): Promise<IJunoResponse<boolean>> {
|
||||
const url = `${this.getNotebooksSubscriptionIdAccountUrl()}/gallery/acceptCodeOfConduct`;
|
||||
const response = await window.fetch(url, {
|
||||
method: "PATCH",
|
||||
headers: JunoClient.getHeaders(),
|
||||
});
|
||||
|
||||
let data: boolean;
|
||||
if (response.status === HttpStatusCodes.OK) {
|
||||
data = await response.json();
|
||||
}
|
||||
|
||||
return {
|
||||
status: response.status,
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
public async isCodeOfConductAccepted(): Promise<IJunoResponse<boolean>> {
|
||||
const url = `${this.getNotebooksSubscriptionIdAccountUrl()}/gallery/isCodeOfConductAccepted`;
|
||||
const response = await window.fetch(url, { headers: JunoClient.getHeaders() });
|
||||
|
||||
let data: boolean;
|
||||
if (response.status === HttpStatusCodes.OK) {
|
||||
data = await response.json();
|
||||
}
|
||||
|
||||
return {
|
||||
status: response.status,
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
public async getNotebookInfo(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",
|
||||
@@ -276,151 +168,6 @@ export class JunoClient {
|
||||
};
|
||||
}
|
||||
|
||||
public async increaseNotebookDownloadCount(id: string): Promise<IJunoResponse<IGalleryItem>> {
|
||||
const response = await window.fetch(`${this.getNotebooksSubscriptionIdAccountUrl()}/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.getNotebooksSubscriptionIdAccountUrl()}/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.getNotebooksSubscriptionIdAccountUrl()}/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.getNotebooksSubscriptionIdAccountUrl()}/gallery/favorites`, {
|
||||
headers: JunoClient.getHeaders(),
|
||||
});
|
||||
}
|
||||
|
||||
public async getPublishedNotebooks(): Promise<IJunoResponse<IGalleryItem[]>> {
|
||||
return await this.getNotebooks(`${this.getNotebooksSubscriptionIdAccountUrl()}/gallery/published`, {
|
||||
headers: JunoClient.getHeaders(),
|
||||
});
|
||||
}
|
||||
|
||||
public async deleteNotebook(id: string): Promise<IJunoResponse<IGalleryItem>> {
|
||||
const response = await window.fetch(`${this.getNotebooksSubscriptionIdAccountUrl()}/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[],
|
||||
thumbnailUrl: string,
|
||||
content: string,
|
||||
): Promise<IJunoResponse<IGalleryItem>> {
|
||||
const response = await window.fetch(`${this.getNotebooksSubscriptionIdAccountUrl()}/gallery`, {
|
||||
method: "PUT",
|
||||
headers: JunoClient.getHeaders(),
|
||||
body: JSON.stringify({
|
||||
name,
|
||||
description,
|
||||
tags,
|
||||
thumbnailUrl,
|
||||
content: JSON.parse(content),
|
||||
addLinkToNotebookViewer: true,
|
||||
} as IPublishNotebookRequest),
|
||||
});
|
||||
|
||||
let data: IGalleryItem;
|
||||
if (response.status === HttpStatusCodes.OK) {
|
||||
data = await response.json();
|
||||
} else {
|
||||
throw new Error(`HTTP status ${response.status} thrown. ${(await response.json()).Message}`);
|
||||
}
|
||||
|
||||
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}`;
|
||||
}
|
||||
|
||||
public async reportAbuse(notebookId: string, abuseCategory: string, notes: string): Promise<IJunoResponse<boolean>> {
|
||||
const response = await window.fetch(`${this.getNotebooksUrl()}/gallery/reportAbuse`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
notebookId,
|
||||
abuseCategory,
|
||||
notes,
|
||||
}),
|
||||
headers: {
|
||||
[HttpHeaders.contentType]: "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
let data: boolean;
|
||||
if (response.status === HttpStatusCodes.OK) {
|
||||
data = await response.json();
|
||||
}
|
||||
|
||||
return {
|
||||
status: response.status,
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
public async requestSchema(
|
||||
schemaRequest: DataModels.ISchemaRequest,
|
||||
): Promise<IJunoResponse<DataModels.ISchemaRequest>> {
|
||||
|
||||
@@ -44,7 +44,6 @@ export type Features = {
|
||||
phoenixNotebooks?: boolean;
|
||||
phoenixFeatures?: boolean;
|
||||
notebooksDownBanner: boolean;
|
||||
publicGallery?: boolean;
|
||||
};
|
||||
|
||||
export function extractFeatures(given = new URLSearchParams(window.location.search)): Features {
|
||||
|
||||
@@ -99,28 +99,7 @@ export enum Action {
|
||||
SettingsV2Updated,
|
||||
SettingsV2Discarded,
|
||||
MongoIndexUpdated,
|
||||
NotebooksGalleryPublish,
|
||||
NotebooksGalleryReportAbuse,
|
||||
NotebooksGalleryClickReportAbuse,
|
||||
NotebooksGalleryViewCodeOfConduct,
|
||||
NotebooksGalleryAcceptCodeOfConduct,
|
||||
NotebooksGalleryFavorite,
|
||||
NotebooksGalleryUnfavorite,
|
||||
NotebooksGalleryClickDelete,
|
||||
NotebooksGalleryDelete,
|
||||
NotebooksGalleryClickDownload,
|
||||
NotebooksGalleryDownload,
|
||||
NotebooksGalleryViewNotebook,
|
||||
NotebooksGalleryViewGallery,
|
||||
NotebooksGalleryViewOfficialSamples,
|
||||
NotebooksGalleryViewPublicGallery,
|
||||
NotebooksGalleryViewFavorites,
|
||||
NotebooksGalleryViewPublishedNotebooks,
|
||||
NotebooksGalleryClickPublishToGallery,
|
||||
NotebooksGalleryOfficialSamplesCount,
|
||||
NotebooksGalleryPublicGalleryCount,
|
||||
NotebooksGalleryFavoritesCount,
|
||||
NotebooksGalleryPublishedCount,
|
||||
SelfServe,
|
||||
ExpandAddCollectionPaneAdvancedSection,
|
||||
ExpandAddGlobalSecondaryIndexPaneAdvancedSection,
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
import { HttpStatusCodes } from "../Common/Constants";
|
||||
import { useDialog } from "../Explorer/Controls/Dialog";
|
||||
import { GalleryTab, SortBy } from "../Explorer/Controls/NotebookGallery/GalleryViewerComponent";
|
||||
import Explorer from "../Explorer/Explorer";
|
||||
import { useNotebook } from "../Explorer/Notebook/useNotebook";
|
||||
import { IGalleryItem, JunoClient } from "../Juno/JunoClient";
|
||||
import * as GalleryUtils from "./GalleryUtils";
|
||||
|
||||
const galleryItem: IGalleryItem = {
|
||||
id: "id",
|
||||
name: "name",
|
||||
description: "description",
|
||||
gitSha: "gitSha",
|
||||
tags: ["tag1"],
|
||||
author: "author",
|
||||
thumbnailUrl: "thumbnailUrl",
|
||||
created: "created",
|
||||
isSample: false,
|
||||
downloads: 0,
|
||||
favorites: 0,
|
||||
views: 0,
|
||||
newCellId: undefined,
|
||||
policyViolations: undefined,
|
||||
pendingScanJobIds: undefined,
|
||||
};
|
||||
|
||||
describe("GalleryUtils", () => {
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it("downloadItem shows dialog in data explorer", () => {
|
||||
const container = new Explorer();
|
||||
GalleryUtils.downloadItem(container, undefined, galleryItem, undefined);
|
||||
|
||||
expect(useDialog.getState().visible).toBe(true);
|
||||
expect(useDialog.getState().dialogProps).toBeDefined();
|
||||
expect(useDialog.getState().dialogProps.title).toBe(`Download to ${useNotebook.getState().notebookFolderName}`);
|
||||
});
|
||||
|
||||
it("favoriteItem favorites item", async () => {
|
||||
const container = {} as Explorer;
|
||||
const junoClient = new JunoClient();
|
||||
junoClient.favoriteNotebook = jest
|
||||
.fn()
|
||||
.mockReturnValue(Promise.resolve({ status: HttpStatusCodes.OK, data: galleryItem }));
|
||||
const onComplete = jest.fn().mockImplementation();
|
||||
|
||||
await GalleryUtils.favoriteItem(container, junoClient, galleryItem, onComplete);
|
||||
|
||||
expect(junoClient.favoriteNotebook).toHaveBeenCalledWith(galleryItem.id);
|
||||
expect(onComplete).toHaveBeenCalledWith(galleryItem);
|
||||
});
|
||||
|
||||
it("unfavoriteItem unfavorites item", async () => {
|
||||
const container = {} as Explorer;
|
||||
const junoClient = new JunoClient();
|
||||
junoClient.unfavoriteNotebook = jest
|
||||
.fn()
|
||||
.mockReturnValue(Promise.resolve({ status: HttpStatusCodes.OK, data: galleryItem }));
|
||||
const onComplete = jest.fn().mockImplementation();
|
||||
|
||||
await GalleryUtils.unfavoriteItem(container, junoClient, galleryItem, onComplete);
|
||||
|
||||
expect(junoClient.unfavoriteNotebook).toHaveBeenCalledWith(galleryItem.id);
|
||||
expect(onComplete).toHaveBeenCalledWith(galleryItem);
|
||||
});
|
||||
|
||||
it("deleteItem shows dialog in data explorer", () => {
|
||||
const container = {} as Explorer;
|
||||
GalleryUtils.deleteItem(container, undefined, galleryItem, undefined);
|
||||
|
||||
expect(useDialog.getState().visible).toBe(true);
|
||||
expect(useDialog.getState().dialogProps).toBeDefined();
|
||||
expect(useDialog.getState().dialogProps.title).toBe("Remove published notebook");
|
||||
});
|
||||
|
||||
it("getGalleryViewerProps gets gallery viewer props correctly", () => {
|
||||
const selectedTab: GalleryTab = GalleryTab.OfficialSamples;
|
||||
const sortBy: SortBy = SortBy.MostDownloaded;
|
||||
const searchText = "my-complicated%20search%20query!!!";
|
||||
|
||||
const response = GalleryUtils.getGalleryViewerProps(
|
||||
`?${GalleryUtils.GalleryViewerParams.SelectedTab}=${GalleryTab[selectedTab]}&${GalleryUtils.GalleryViewerParams.SortBy}=${SortBy[sortBy]}&${GalleryUtils.GalleryViewerParams.SearchText}=${searchText}`,
|
||||
);
|
||||
|
||||
expect(response).toEqual({
|
||||
selectedTab,
|
||||
sortBy,
|
||||
searchText: decodeURIComponent(searchText),
|
||||
} as GalleryUtils.GalleryViewerProps);
|
||||
});
|
||||
|
||||
it("getNotebookViewerProps gets notebook viewer props correctly", () => {
|
||||
const notebookUrl = "https%3A%2F%2Fnotebook.url";
|
||||
const galleryItemId = "1234-abcd-efgh";
|
||||
const hideInputs = "true";
|
||||
|
||||
const response = GalleryUtils.getNotebookViewerProps(
|
||||
`?${GalleryUtils.NotebookViewerParams.NotebookUrl}=${notebookUrl}&${GalleryUtils.NotebookViewerParams.GalleryItemId}=${galleryItemId}&${GalleryUtils.NotebookViewerParams.HideInputs}=${hideInputs}`,
|
||||
);
|
||||
|
||||
expect(response).toEqual({
|
||||
notebookUrl: decodeURIComponent(notebookUrl),
|
||||
galleryItemId,
|
||||
hideInputs: true,
|
||||
} as GalleryUtils.NotebookViewerProps);
|
||||
});
|
||||
|
||||
it("getTabTitle returns correct title for official samples", () => {
|
||||
expect(GalleryUtils.getTabTitle(GalleryTab.OfficialSamples)).toBe("Official samples");
|
||||
});
|
||||
});
|
||||
@@ -1,528 +0,0 @@
|
||||
import { IChoiceGroupOption, IChoiceGroupProps, IProgressIndicatorProps } from "@fluentui/react";
|
||||
import { Notebook } from "@nteract/commutable";
|
||||
import { NotebookV4 } from "@nteract/commutable/lib/v4";
|
||||
import { HttpStatusCodes } from "../Common/Constants";
|
||||
import { getErrorMessage, getErrorStack, handleError } from "../Common/ErrorHandlingUtils";
|
||||
import { TextFieldProps, useDialog } from "../Explorer/Controls/Dialog";
|
||||
import {
|
||||
GalleryTab,
|
||||
GalleryViewerComponent,
|
||||
SortBy,
|
||||
} from "../Explorer/Controls/NotebookGallery/GalleryViewerComponent";
|
||||
import Explorer from "../Explorer/Explorer";
|
||||
import { useNotebook } from "../Explorer/Notebook/useNotebook";
|
||||
import { IGalleryItem, JunoClient } from "../Juno/JunoClient";
|
||||
import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants";
|
||||
import { trace, traceFailure, traceStart, traceSuccess } from "../Shared/Telemetry/TelemetryProcessor";
|
||||
import { logConsoleInfo, logConsoleProgress } from "./NotificationConsoleUtils";
|
||||
|
||||
const defaultSelectedAbuseCategory = "Other";
|
||||
const abuseCategories: IChoiceGroupOption[] = [
|
||||
{
|
||||
key: "ChildEndangermentExploitation",
|
||||
text: "Child endangerment or exploitation",
|
||||
},
|
||||
{
|
||||
key: "ContentInfringement",
|
||||
text: "Content infringement",
|
||||
},
|
||||
{
|
||||
key: "OffensiveContent",
|
||||
text: "Offensive content",
|
||||
},
|
||||
{
|
||||
key: "Terrorism",
|
||||
text: "Terrorism",
|
||||
},
|
||||
{
|
||||
key: "ThreatsCyberbullyingHarassment",
|
||||
text: "Threats, cyber bullying or harassment",
|
||||
},
|
||||
{
|
||||
key: "VirusSpywareMalware",
|
||||
text: "Virus, spyware or malware",
|
||||
},
|
||||
{
|
||||
key: "Fraud",
|
||||
text: "Fraud",
|
||||
},
|
||||
{
|
||||
key: "HateSpeech",
|
||||
text: "Hate speech",
|
||||
},
|
||||
{
|
||||
key: "ImminentHarmToPersonsOrProperty",
|
||||
text: "Imminent harm to persons or property",
|
||||
},
|
||||
{
|
||||
key: "Other",
|
||||
text: "Other",
|
||||
},
|
||||
];
|
||||
|
||||
export enum NotebookViewerParams {
|
||||
NotebookUrl = "notebookUrl",
|
||||
GalleryItemId = "galleryItemId",
|
||||
HideInputs = "hideInputs",
|
||||
}
|
||||
|
||||
export interface NotebookViewerProps {
|
||||
notebookUrl: string;
|
||||
galleryItemId: string;
|
||||
hideInputs: boolean;
|
||||
}
|
||||
|
||||
export enum GalleryViewerParams {
|
||||
SelectedTab = "tab",
|
||||
SortBy = "sort",
|
||||
SearchText = "q",
|
||||
}
|
||||
|
||||
export interface GalleryViewerProps {
|
||||
selectedTab: GalleryTab;
|
||||
sortBy: SortBy;
|
||||
searchText: string;
|
||||
}
|
||||
|
||||
export interface DialogHost {
|
||||
showOkModalDialog(
|
||||
title: string,
|
||||
msg: string,
|
||||
okLabel: string,
|
||||
onOk: () => void,
|
||||
progressIndicatorProps?: IProgressIndicatorProps,
|
||||
): void;
|
||||
|
||||
showOkCancelModalDialog(
|
||||
title: string,
|
||||
msg: string,
|
||||
okLabel: string,
|
||||
onOk: () => void,
|
||||
cancelLabel: string,
|
||||
onCancel: () => void,
|
||||
progressIndicatorProps?: IProgressIndicatorProps,
|
||||
choiceGroupProps?: IChoiceGroupProps,
|
||||
textFieldProps?: TextFieldProps,
|
||||
primaryButtonDisabled?: boolean,
|
||||
): void;
|
||||
}
|
||||
|
||||
export function reportAbuse(
|
||||
junoClient: JunoClient,
|
||||
data: IGalleryItem,
|
||||
dialogHost: DialogHost,
|
||||
onComplete: (success: boolean) => void,
|
||||
): void {
|
||||
trace(Action.NotebooksGalleryClickReportAbuse, ActionModifiers.Mark, { notebookId: data.id });
|
||||
|
||||
const notebookId = data.id;
|
||||
let abuseCategory = defaultSelectedAbuseCategory;
|
||||
let additionalDetails: string;
|
||||
|
||||
dialogHost.showOkCancelModalDialog(
|
||||
"Report Abuse",
|
||||
undefined,
|
||||
"Report Abuse",
|
||||
async () => {
|
||||
dialogHost.showOkCancelModalDialog(
|
||||
"Report Abuse",
|
||||
`Submitting your report on ${data.name} violating code of conduct`,
|
||||
"Reporting...",
|
||||
undefined,
|
||||
"Cancel",
|
||||
undefined,
|
||||
{},
|
||||
undefined,
|
||||
undefined,
|
||||
true,
|
||||
);
|
||||
|
||||
const startKey = traceStart(Action.NotebooksGalleryReportAbuse, { notebookId: data.id, abuseCategory });
|
||||
|
||||
try {
|
||||
const response = await junoClient.reportAbuse(notebookId, abuseCategory, additionalDetails);
|
||||
if (response.status !== HttpStatusCodes.Accepted) {
|
||||
throw new Error(`Received HTTP ${response.status} when submitting report for ${data.name}`);
|
||||
}
|
||||
|
||||
dialogHost.showOkModalDialog(
|
||||
"Report Abuse",
|
||||
`Your report on ${data.name} has been submitted. Thank you for reporting the violation.`,
|
||||
"OK",
|
||||
undefined,
|
||||
{
|
||||
percentComplete: 1,
|
||||
},
|
||||
);
|
||||
|
||||
traceSuccess(Action.NotebooksGalleryReportAbuse, { notebookId: data.id, abuseCategory }, startKey);
|
||||
|
||||
onComplete(response.data);
|
||||
} catch (error) {
|
||||
traceFailure(
|
||||
Action.NotebooksGalleryReportAbuse,
|
||||
{
|
||||
notebookId: data.id,
|
||||
abuseCategory,
|
||||
error: getErrorMessage(error),
|
||||
errorStack: getErrorStack(error),
|
||||
},
|
||||
startKey,
|
||||
);
|
||||
|
||||
handleError(
|
||||
error,
|
||||
"GalleryUtils/reportAbuse",
|
||||
`Failed to submit report on ${data.name} violating code of conduct`,
|
||||
);
|
||||
|
||||
dialogHost.showOkModalDialog(
|
||||
"Report Abuse",
|
||||
`Failed to submit report on ${data.name} violating code of conduct`,
|
||||
"OK",
|
||||
undefined,
|
||||
{
|
||||
percentComplete: 1,
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
"Cancel",
|
||||
undefined,
|
||||
undefined,
|
||||
{
|
||||
label: "How does this content violate the code of conduct?",
|
||||
options: abuseCategories,
|
||||
defaultSelectedKey: defaultSelectedAbuseCategory,
|
||||
onChange: (_event?: React.FormEvent<HTMLElement | HTMLInputElement>, option?: IChoiceGroupOption) => {
|
||||
abuseCategory = option?.key;
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "You can also include additional relevant details on the offensive content",
|
||||
multiline: true,
|
||||
rows: 3,
|
||||
autoAdjustHeight: false,
|
||||
onChange: (_event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>, newValue?: string) => {
|
||||
additionalDetails = newValue;
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function downloadItem(
|
||||
container: Explorer,
|
||||
junoClient: JunoClient,
|
||||
data: IGalleryItem,
|
||||
onComplete: (item: IGalleryItem) => void,
|
||||
): void {
|
||||
trace(Action.NotebooksGalleryClickDownload, ActionModifiers.Mark, {
|
||||
notebookId: data.id,
|
||||
downloadCount: data.downloads,
|
||||
isSample: data.isSample,
|
||||
});
|
||||
|
||||
const name = data.name;
|
||||
useDialog.getState().showOkCancelModalDialog(
|
||||
`Download to ${useNotebook.getState().notebookFolderName}`,
|
||||
undefined,
|
||||
"Download",
|
||||
async () => {
|
||||
if (useNotebook.getState().isPhoenixNotebooks) {
|
||||
await container.allocateContainer();
|
||||
}
|
||||
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
||||
if (notebookServerInfo && notebookServerInfo.notebookServerEndpoint !== undefined) {
|
||||
downloadNotebookItem(name, data, junoClient, container, onComplete);
|
||||
} else {
|
||||
useDialog
|
||||
.getState()
|
||||
.showOkModalDialog(
|
||||
"Failed to connect",
|
||||
"Failed to connect to temporary workspace. Please refresh the page and try again.",
|
||||
);
|
||||
}
|
||||
},
|
||||
"Cancel",
|
||||
undefined,
|
||||
container.getDownloadModalContent(name),
|
||||
);
|
||||
}
|
||||
export async function downloadNotebookItem(
|
||||
fileName: string,
|
||||
data: IGalleryItem,
|
||||
junoClient: JunoClient,
|
||||
container: Explorer,
|
||||
onComplete: (item: IGalleryItem) => void,
|
||||
) {
|
||||
const clearInProgressMessage = logConsoleProgress(
|
||||
`Downloading ${fileName} to ${useNotebook.getState().notebookFolderName}`,
|
||||
);
|
||||
const startKey = traceStart(Action.NotebooksGalleryDownload, {
|
||||
notebookId: data.id,
|
||||
downloadCount: data.downloads,
|
||||
isSample: data.isSample,
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await junoClient.getNotebookContent(data.id);
|
||||
if (!response.data) {
|
||||
throw new Error(`Received HTTP ${response.status} when fetching ${data.name}`);
|
||||
}
|
||||
|
||||
const notebook = JSON.parse(response.data) as Notebook;
|
||||
removeNotebookViewerLink(notebook, data.newCellId);
|
||||
|
||||
if (!data.isSample) {
|
||||
const metadata = notebook.metadata as { [name: string]: unknown };
|
||||
metadata.untrusted = true;
|
||||
}
|
||||
|
||||
await container.importAndOpenContent(data.name, JSON.stringify(notebook));
|
||||
logConsoleInfo(`Successfully downloaded ${data.name} to ${useNotebook.getState().notebookFolderName}`);
|
||||
|
||||
const increaseDownloadResponse = await junoClient.increaseNotebookDownloadCount(data.id);
|
||||
if (increaseDownloadResponse.data) {
|
||||
traceSuccess(
|
||||
Action.NotebooksGalleryDownload,
|
||||
{ notebookId: data.id, downloadCount: increaseDownloadResponse.data.downloads, isSample: data.isSample },
|
||||
startKey,
|
||||
);
|
||||
onComplete(increaseDownloadResponse.data);
|
||||
}
|
||||
} catch (error) {
|
||||
traceFailure(
|
||||
Action.NotebooksGalleryDownload,
|
||||
{
|
||||
notebookId: data.id,
|
||||
downloadCount: data.downloads,
|
||||
isSample: data.isSample,
|
||||
error: getErrorMessage(error),
|
||||
errorStack: getErrorStack(error),
|
||||
},
|
||||
startKey,
|
||||
);
|
||||
|
||||
handleError(error, "GalleryUtils/downloadItem", `Failed to download ${data.name}`);
|
||||
}
|
||||
|
||||
clearInProgressMessage();
|
||||
}
|
||||
export const removeNotebookViewerLink = (notebook: Notebook, newCellId: string): void => {
|
||||
if (!newCellId) {
|
||||
return;
|
||||
}
|
||||
const notebookV4 = notebook as NotebookV4;
|
||||
if (notebookV4?.cells[0]?.source[0]?.search(newCellId)) {
|
||||
notebookV4.cells.splice(0, 1);
|
||||
notebook = notebookV4;
|
||||
}
|
||||
};
|
||||
|
||||
export async function favoriteItem(
|
||||
container: Explorer,
|
||||
junoClient: JunoClient,
|
||||
data: IGalleryItem,
|
||||
onComplete: (item: IGalleryItem) => void,
|
||||
): Promise<void> {
|
||||
if (container) {
|
||||
const startKey = traceStart(Action.NotebooksGalleryFavorite, {
|
||||
notebookId: data.id,
|
||||
isSample: data.isSample,
|
||||
favoriteCount: data.favorites,
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await junoClient.favoriteNotebook(data.id);
|
||||
if (!response.data) {
|
||||
throw new Error(`Received HTTP ${response.status} when favoriting ${data.name}`);
|
||||
}
|
||||
|
||||
traceSuccess(
|
||||
Action.NotebooksGalleryFavorite,
|
||||
{ notebookId: data.id, isSample: data.isSample, favoriteCount: response.data.favorites },
|
||||
startKey,
|
||||
);
|
||||
|
||||
onComplete(response.data);
|
||||
} catch (error) {
|
||||
traceFailure(
|
||||
Action.NotebooksGalleryFavorite,
|
||||
{
|
||||
notebookId: data.id,
|
||||
isSample: data.isSample,
|
||||
favoriteCount: data.favorites,
|
||||
error: getErrorMessage(error),
|
||||
errorStack: getErrorStack(error),
|
||||
},
|
||||
startKey,
|
||||
);
|
||||
|
||||
handleError(error, "GalleryUtils/favoriteItem", `Failed to favorite ${data.name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function unfavoriteItem(
|
||||
container: Explorer,
|
||||
junoClient: JunoClient,
|
||||
data: IGalleryItem,
|
||||
onComplete: (item: IGalleryItem) => void,
|
||||
): Promise<void> {
|
||||
if (container) {
|
||||
const startKey = traceStart(Action.NotebooksGalleryUnfavorite, {
|
||||
notebookId: data.id,
|
||||
isSample: data.isSample,
|
||||
favoriteCount: data.favorites,
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await junoClient.unfavoriteNotebook(data.id);
|
||||
if (!response.data) {
|
||||
throw new Error(`Received HTTP ${response.status} when unfavoriting ${data.name}`);
|
||||
}
|
||||
|
||||
traceSuccess(
|
||||
Action.NotebooksGalleryUnfavorite,
|
||||
{ notebookId: data.id, isSample: data.isSample, favoriteCount: response.data.favorites },
|
||||
startKey,
|
||||
);
|
||||
|
||||
onComplete(response.data);
|
||||
} catch (error) {
|
||||
traceFailure(
|
||||
Action.NotebooksGalleryUnfavorite,
|
||||
{
|
||||
notebookId: data.id,
|
||||
isSample: data.isSample,
|
||||
favoriteCount: data.favorites,
|
||||
error: getErrorMessage(error),
|
||||
errorStack: getErrorStack(error),
|
||||
},
|
||||
startKey,
|
||||
);
|
||||
|
||||
handleError(error, "GalleryUtils/unfavoriteItem", `Failed to unfavorite ${data.name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function deleteItem(
|
||||
container: Explorer,
|
||||
junoClient: JunoClient,
|
||||
data: IGalleryItem,
|
||||
onComplete: (item: IGalleryItem) => void,
|
||||
beforeDelete?: () => void,
|
||||
afterDelete?: () => void,
|
||||
): void {
|
||||
if (container) {
|
||||
trace(Action.NotebooksGalleryClickDelete, ActionModifiers.Mark, { notebookId: data.id });
|
||||
|
||||
useDialog.getState().showOkCancelModalDialog(
|
||||
"Remove published notebook",
|
||||
`Would you like to remove ${data.name} from the gallery?`,
|
||||
"Remove",
|
||||
async () => {
|
||||
if (beforeDelete) {
|
||||
beforeDelete();
|
||||
}
|
||||
const name = data.name;
|
||||
const clearInProgressMessage = logConsoleProgress(`Removing ${name} from gallery`);
|
||||
const startKey = traceStart(Action.NotebooksGalleryDelete, { notebookId: data.id });
|
||||
|
||||
try {
|
||||
const response = await junoClient.deleteNotebook(data.id);
|
||||
if (!response.data) {
|
||||
throw new Error(`Received HTTP ${response.status} while removing ${name}`);
|
||||
}
|
||||
|
||||
traceSuccess(Action.NotebooksGalleryDelete, { notebookId: data.id }, startKey);
|
||||
|
||||
logConsoleInfo(`Successfully removed ${name} from gallery`);
|
||||
onComplete(response.data);
|
||||
} catch (error) {
|
||||
traceFailure(
|
||||
Action.NotebooksGalleryDelete,
|
||||
{ notebookId: data.id, error: getErrorMessage(error), errorStack: getErrorStack(error) },
|
||||
startKey,
|
||||
);
|
||||
|
||||
handleError(error, "GalleryUtils/deleteItem", `Failed to remove ${name} from gallery`);
|
||||
} finally {
|
||||
if (afterDelete) {
|
||||
afterDelete();
|
||||
}
|
||||
}
|
||||
|
||||
clearInProgressMessage();
|
||||
},
|
||||
"Cancel",
|
||||
undefined,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function getGalleryViewerProps(search: string): GalleryViewerProps {
|
||||
const params = new URLSearchParams(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(search: string): NotebookViewerProps {
|
||||
const params = new URLSearchParams(search);
|
||||
return {
|
||||
notebookUrl: params.get(NotebookViewerParams.NotebookUrl),
|
||||
galleryItemId: params.get(NotebookViewerParams.GalleryItemId),
|
||||
hideInputs: JSON.parse(params.get(NotebookViewerParams.HideInputs)),
|
||||
};
|
||||
}
|
||||
|
||||
export function getTabTitle(tab: GalleryTab): string {
|
||||
switch (tab) {
|
||||
case GalleryTab.PublicGallery:
|
||||
return GalleryViewerComponent.PublicGalleryTitle;
|
||||
case GalleryTab.OfficialSamples:
|
||||
return GalleryViewerComponent.OfficialSamplesTitle;
|
||||
case GalleryTab.Favorites:
|
||||
return GalleryViewerComponent.FavoritesTitle;
|
||||
case GalleryTab.Published:
|
||||
return GalleryViewerComponent.PublishedTitle;
|
||||
default:
|
||||
throw new Error(`Unknown tab ${tab}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function filterPublishedNotebooks(items: IGalleryItem[]): {
|
||||
published: IGalleryItem[];
|
||||
underReview: IGalleryItem[];
|
||||
removed: IGalleryItem[];
|
||||
} {
|
||||
const underReview: IGalleryItem[] = [];
|
||||
const removed: IGalleryItem[] = [];
|
||||
const published: IGalleryItem[] = [];
|
||||
|
||||
items?.forEach((item) => {
|
||||
if (item.policyViolations?.length > 0) {
|
||||
removed.push(item);
|
||||
} else if (item.pendingScanJobIds?.length > 0) {
|
||||
underReview.push(item);
|
||||
} else {
|
||||
published.push(item);
|
||||
}
|
||||
});
|
||||
|
||||
return { published, underReview, removed };
|
||||
}
|
||||
@@ -1008,9 +1008,6 @@ function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) {
|
||||
if (inputs.flights.indexOf(Flights.NotebooksDownBanner) !== -1) {
|
||||
userContext.features.notebooksDownBanner = true;
|
||||
}
|
||||
if (inputs.flights.indexOf(Flights.PublicGallery) !== -1) {
|
||||
userContext.features.publicGallery = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle initial theme from portal
|
||||
|
||||
@@ -113,7 +113,6 @@ module.exports = function (_env = {}, argv = {}) {
|
||||
hostedExplorer: "./src/HostedExplorer.tsx",
|
||||
terminal: "./src/Terminal/index.ts",
|
||||
cellOutputViewer: "./src/CellOutputViewer/CellOutputViewer.tsx",
|
||||
galleryViewer: "./src/GalleryViewer/GalleryViewer.tsx",
|
||||
selfServe: "./src/SelfServe/SelfServe.tsx",
|
||||
connectToGitHub: "./src/GitHub/GitHubConnector.ts",
|
||||
redirectBridge: "./src/redirectBridge.ts",
|
||||
@@ -154,11 +153,6 @@ module.exports = function (_env = {}, argv = {}) {
|
||||
template: "src/CellOutputViewer/cellOutputViewer.html",
|
||||
chunks: ["cellOutputViewer"],
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
filename: "gallery.html",
|
||||
template: "src/GalleryViewer/galleryViewer.html",
|
||||
chunks: ["galleryViewer"],
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
filename: "connectToGitHub.html",
|
||||
template: "src/connectToGitHub.html",
|
||||
|
||||
Reference in New Issue
Block a user