mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-06-29 01:27:22 +01:00
Remove gallery.html and all associated gallery functionality (#2474)
* 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> * Update package-lock * Revert changes to Telemetry Constants --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -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";
|
||||
@@ -714,24 +710,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);
|
||||
}
|
||||
@@ -1051,45 +1029,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
|
||||
|
||||
Reference in New Issue
Block a user