mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2024-11-29 00:47:01 +00:00
Migrate Publish Notebook Pane to React (#641)
Co-authored-by: Steve Faulkner <southpolesteve@gmail.com>
This commit is contained in:
parent
154db1dcd5
commit
8f3cb7282b
@ -1,25 +1,25 @@
|
|||||||
import { Card } from "@uifabric/react-cards";
|
import { Card } from "@uifabric/react-cards";
|
||||||
import {
|
import {
|
||||||
|
BaseButton,
|
||||||
|
Button,
|
||||||
FontWeights,
|
FontWeights,
|
||||||
Icon,
|
Icon,
|
||||||
IconButton,
|
IconButton,
|
||||||
Image,
|
Image,
|
||||||
ImageFit,
|
ImageFit,
|
||||||
Persona,
|
|
||||||
Text,
|
|
||||||
Link,
|
Link,
|
||||||
BaseButton,
|
|
||||||
Button,
|
|
||||||
LinkBase,
|
LinkBase,
|
||||||
|
Persona,
|
||||||
Separator,
|
Separator,
|
||||||
TooltipHost,
|
|
||||||
Spinner,
|
Spinner,
|
||||||
SpinnerSize,
|
SpinnerSize,
|
||||||
|
Text,
|
||||||
|
TooltipHost,
|
||||||
} from "office-ui-fabric-react";
|
} from "office-ui-fabric-react";
|
||||||
import * as React from "react";
|
import React, { FunctionComponent, useState } from "react";
|
||||||
|
import CosmosDBLogo from "../../../../../images/CosmosDB-logo.svg";
|
||||||
import { IGalleryItem } from "../../../../Juno/JunoClient";
|
import { IGalleryItem } from "../../../../Juno/JunoClient";
|
||||||
import * as FileSystemUtil from "../../../Notebook/FileSystemUtil";
|
import * as FileSystemUtil from "../../../Notebook/FileSystemUtil";
|
||||||
import CosmosDBLogo from "../../../../../images/CosmosDB-logo.svg";
|
|
||||||
|
|
||||||
export interface GalleryCardComponentProps {
|
export interface GalleryCardComponentProps {
|
||||||
data: IGalleryItem;
|
data: IGalleryItem;
|
||||||
@ -34,166 +34,48 @@ export interface GalleryCardComponentProps {
|
|||||||
onDeleteClick: (beforeDelete: () => void, afterDelete: () => void) => void;
|
onDeleteClick: (beforeDelete: () => void, afterDelete: () => void) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GalleryCardComponentState {
|
export const GalleryCardComponent: FunctionComponent<GalleryCardComponentProps> = ({
|
||||||
isDeletingPublishedNotebook: boolean;
|
data,
|
||||||
}
|
isFavorite,
|
||||||
|
showDownload,
|
||||||
|
showDelete,
|
||||||
|
onClick,
|
||||||
|
onTagClick,
|
||||||
|
onFavoriteClick,
|
||||||
|
onUnfavoriteClick,
|
||||||
|
onDownloadClick,
|
||||||
|
onDeleteClick,
|
||||||
|
}: GalleryCardComponentProps) => {
|
||||||
|
const CARD_WIDTH = 256;
|
||||||
|
const cardImageHeight = 144;
|
||||||
|
const cardDescriptionMaxChars = 80;
|
||||||
|
const cardItemGapBig = 10;
|
||||||
|
const cardItemGapSmall = 8;
|
||||||
|
const cardDeleteSpinnerHeight = 360;
|
||||||
|
const smallTextLineHeight = 18;
|
||||||
|
|
||||||
export class GalleryCardComponent extends React.Component<GalleryCardComponentProps, GalleryCardComponentState> {
|
const [isDeletingPublishedNotebook, setIsDeletingPublishedNotebook] = useState<boolean>(false);
|
||||||
public static readonly CARD_WIDTH = 256;
|
|
||||||
private static readonly cardImageHeight = 144;
|
|
||||||
public static readonly cardHeightToWidthRatio =
|
|
||||||
GalleryCardComponent.cardImageHeight / GalleryCardComponent.CARD_WIDTH;
|
|
||||||
private static readonly cardDescriptionMaxChars = 80;
|
|
||||||
private static readonly cardItemGapBig = 10;
|
|
||||||
private static readonly cardItemGapSmall = 8;
|
|
||||||
private static readonly cardDeleteSpinnerHeight = 360;
|
|
||||||
private static readonly smallTextLineHeight = 18;
|
|
||||||
|
|
||||||
constructor(props: GalleryCardComponentProps) {
|
const cardButtonsVisible = isFavorite !== undefined || showDownload || showDelete;
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
isDeletingPublishedNotebook: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public render(): JSX.Element {
|
|
||||||
const cardButtonsVisible = this.props.isFavorite !== undefined || this.props.showDownload || this.props.showDelete;
|
|
||||||
const options: Intl.DateTimeFormatOptions = {
|
const options: Intl.DateTimeFormatOptions = {
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
month: "short",
|
month: "short",
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
};
|
};
|
||||||
const dateString = new Date(this.props.data.created).toLocaleString("default", options);
|
const dateString = new Date(data.created).toLocaleString("default", options);
|
||||||
const cardTitle = FileSystemUtil.stripExtension(this.props.data.name, "ipynb");
|
const cardTitle = FileSystemUtil.stripExtension(data.name, "ipynb");
|
||||||
|
|
||||||
return (
|
const renderTruncatedDescription = (): string => {
|
||||||
<Card
|
let truncatedDescription = data.description.substr(0, cardDescriptionMaxChars);
|
||||||
style={{ background: "white" }}
|
if (data.description.length > cardDescriptionMaxChars) {
|
||||||
aria-label={cardTitle}
|
|
||||||
data-is-focusable="true"
|
|
||||||
tokens={{ width: GalleryCardComponent.CARD_WIDTH, childrenGap: 0 }}
|
|
||||||
onClick={(event) => this.onClick(event, this.props.onClick)}
|
|
||||||
>
|
|
||||||
{this.state.isDeletingPublishedNotebook && (
|
|
||||||
<Card.Item tokens={{ padding: GalleryCardComponent.cardItemGapBig }}>
|
|
||||||
<Spinner
|
|
||||||
size={SpinnerSize.large}
|
|
||||||
label={`Deleting '${cardTitle}'`}
|
|
||||||
styles={{ root: { height: GalleryCardComponent.cardDeleteSpinnerHeight } }}
|
|
||||||
/>
|
|
||||||
</Card.Item>
|
|
||||||
)}
|
|
||||||
{!this.state.isDeletingPublishedNotebook && (
|
|
||||||
<>
|
|
||||||
<Card.Item tokens={{ padding: GalleryCardComponent.cardItemGapBig }}>
|
|
||||||
<Persona
|
|
||||||
imageUrl={this.props.data.isSample && CosmosDBLogo}
|
|
||||||
text={this.props.data.author}
|
|
||||||
secondaryText={dateString}
|
|
||||||
/>
|
|
||||||
</Card.Item>
|
|
||||||
|
|
||||||
<Card.Item>
|
|
||||||
<Image
|
|
||||||
src={this.props.data.thumbnailUrl}
|
|
||||||
width={GalleryCardComponent.CARD_WIDTH}
|
|
||||||
height={GalleryCardComponent.cardImageHeight}
|
|
||||||
imageFit={ImageFit.cover}
|
|
||||||
alt={`${cardTitle} cover image`}
|
|
||||||
/>
|
|
||||||
</Card.Item>
|
|
||||||
|
|
||||||
<Card.Section styles={{ root: { padding: GalleryCardComponent.cardItemGapBig } }}>
|
|
||||||
<Text variant="small" nowrap styles={{ root: { height: GalleryCardComponent.smallTextLineHeight } }}>
|
|
||||||
{this.props.data.tags ? (
|
|
||||||
this.props.data.tags.map((tag, index, array) => (
|
|
||||||
<span key={tag}>
|
|
||||||
<Link onClick={(event) => this.onClick(event, () => this.props.onTagClick(tag))}>{tag}</Link>
|
|
||||||
{index === array.length - 1 ? <></> : ", "}
|
|
||||||
</span>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<br />
|
|
||||||
)}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<Text
|
|
||||||
styles={{
|
|
||||||
root: {
|
|
||||||
fontWeight: FontWeights.semibold,
|
|
||||||
paddingTop: GalleryCardComponent.cardItemGapSmall,
|
|
||||||
paddingBottom: GalleryCardComponent.cardItemGapSmall,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
nowrap
|
|
||||||
>
|
|
||||||
{cardTitle}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<Text variant="small" styles={{ root: { height: GalleryCardComponent.smallTextLineHeight * 2 } }}>
|
|
||||||
{this.renderTruncatedDescription()}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<span>
|
|
||||||
{this.props.data.views !== undefined &&
|
|
||||||
this.generateIconText("RedEye", this.props.data.views.toString())}
|
|
||||||
{this.props.data.downloads !== undefined &&
|
|
||||||
this.generateIconText("Download", this.props.data.downloads.toString())}
|
|
||||||
{this.props.data.favorites !== undefined &&
|
|
||||||
this.generateIconText("Heart", this.props.data.favorites.toString())}
|
|
||||||
</span>
|
|
||||||
</Card.Section>
|
|
||||||
|
|
||||||
{cardButtonsVisible && (
|
|
||||||
<Card.Section
|
|
||||||
styles={{
|
|
||||||
root: {
|
|
||||||
marginLeft: GalleryCardComponent.cardItemGapBig,
|
|
||||||
marginRight: GalleryCardComponent.cardItemGapBig,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Separator styles={{ root: { padding: 0, height: 1 } }} />
|
|
||||||
|
|
||||||
<span>
|
|
||||||
{this.props.isFavorite !== undefined &&
|
|
||||||
this.generateIconButtonWithTooltip(
|
|
||||||
this.props.isFavorite ? "HeartFill" : "Heart",
|
|
||||||
this.props.isFavorite ? "Unfavorite" : "Favorite",
|
|
||||||
"left",
|
|
||||||
this.props.isFavorite ? this.props.onUnfavoriteClick : this.props.onFavoriteClick
|
|
||||||
)}
|
|
||||||
|
|
||||||
{this.props.showDownload &&
|
|
||||||
this.generateIconButtonWithTooltip("Download", "Download", "left", this.props.onDownloadClick)}
|
|
||||||
|
|
||||||
{this.props.showDelete &&
|
|
||||||
this.generateIconButtonWithTooltip("Delete", "Remove", "right", () =>
|
|
||||||
this.props.onDeleteClick(
|
|
||||||
() => this.setState({ isDeletingPublishedNotebook: true }),
|
|
||||||
() => this.setState({ isDeletingPublishedNotebook: false })
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</Card.Section>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderTruncatedDescription = (): string => {
|
|
||||||
let truncatedDescription = this.props.data.description.substr(0, GalleryCardComponent.cardDescriptionMaxChars);
|
|
||||||
if (this.props.data.description.length > GalleryCardComponent.cardDescriptionMaxChars) {
|
|
||||||
truncatedDescription = `${truncatedDescription} ...`;
|
truncatedDescription = `${truncatedDescription} ...`;
|
||||||
}
|
}
|
||||||
return truncatedDescription;
|
return truncatedDescription;
|
||||||
};
|
};
|
||||||
|
|
||||||
private generateIconText = (iconName: string, text: string): JSX.Element => {
|
const generateIconText = (iconName: string, text: string): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<Text variant="tiny" styles={{ root: { color: "#605E5C", paddingRight: GalleryCardComponent.cardItemGapSmall } }}>
|
<Text variant="tiny" styles={{ root: { color: "#605E5C", paddingRight: cardItemGapSmall } }}>
|
||||||
<Icon iconName={iconName} styles={{ root: { verticalAlign: "middle" } }} /> {text}
|
<Icon iconName={iconName} styles={{ root: { verticalAlign: "middle" } }} /> {text}
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
@ -203,7 +85,7 @@ export class GalleryCardComponent extends React.Component<GalleryCardComponentPr
|
|||||||
* Fluent UI doesn't support tooltips on IconButtons out of the box. In the meantime the recommendation is
|
* 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)
|
* to do the following (from https://developer.microsoft.com/en-us/fluentui#/controls/web/button)
|
||||||
*/
|
*/
|
||||||
private generateIconButtonWithTooltip = (
|
const generateIconButtonWithTooltip = (
|
||||||
iconName: string,
|
iconName: string,
|
||||||
title: string,
|
title: string,
|
||||||
horizontalAlign: "right" | "left",
|
horizontalAlign: "right" | "left",
|
||||||
@ -220,13 +102,13 @@ export class GalleryCardComponent extends React.Component<GalleryCardComponentPr
|
|||||||
iconProps={{ iconName }}
|
iconProps={{ iconName }}
|
||||||
title={title}
|
title={title}
|
||||||
ariaLabel={title}
|
ariaLabel={title}
|
||||||
onClick={(event) => this.onClick(event, activate)}
|
onClick={(event) => handlerOnClick(event, activate)}
|
||||||
/>
|
/>
|
||||||
</TooltipHost>
|
</TooltipHost>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
private onClick = (
|
const handlerOnClick = (
|
||||||
event:
|
event:
|
||||||
| React.MouseEvent<HTMLElement | HTMLAnchorElement | HTMLButtonElement | LinkBase, MouseEvent>
|
| React.MouseEvent<HTMLElement | HTMLAnchorElement | HTMLButtonElement | LinkBase, MouseEvent>
|
||||||
| React.MouseEvent<
|
| React.MouseEvent<
|
||||||
@ -239,4 +121,112 @@ export class GalleryCardComponent extends React.Component<GalleryCardComponentPr
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
activate();
|
activate();
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
style={{ background: "white" }}
|
||||||
|
aria-label={cardTitle}
|
||||||
|
data-is-focusable="true"
|
||||||
|
tokens={{ width: CARD_WIDTH, childrenGap: 0 }}
|
||||||
|
onClick={(event) => handlerOnClick(event, onClick)}
|
||||||
|
>
|
||||||
|
{isDeletingPublishedNotebook && (
|
||||||
|
<Card.Item tokens={{ padding: cardItemGapBig }}>
|
||||||
|
<Spinner
|
||||||
|
size={SpinnerSize.large}
|
||||||
|
label={`Deleting '${cardTitle}'`}
|
||||||
|
styles={{ root: { height: cardDeleteSpinnerHeight } }}
|
||||||
|
/>
|
||||||
|
</Card.Item>
|
||||||
|
)}
|
||||||
|
{!isDeletingPublishedNotebook && (
|
||||||
|
<>
|
||||||
|
<Card.Item tokens={{ padding: cardItemGapBig }}>
|
||||||
|
<Persona imageUrl={data.isSample && CosmosDBLogo} text={data.author} secondaryText={dateString} />
|
||||||
|
</Card.Item>
|
||||||
|
|
||||||
|
<Card.Item>
|
||||||
|
<Image
|
||||||
|
src={data.thumbnailUrl}
|
||||||
|
width={CARD_WIDTH}
|
||||||
|
height={cardImageHeight}
|
||||||
|
imageFit={ImageFit.cover}
|
||||||
|
alt={`${cardTitle} cover image`}
|
||||||
|
/>
|
||||||
|
</Card.Item>
|
||||||
|
|
||||||
|
<Card.Section styles={{ root: { padding: cardItemGapBig } }}>
|
||||||
|
<Text variant="small" nowrap styles={{ root: { height: smallTextLineHeight } }}>
|
||||||
|
{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>
|
||||||
|
|
||||||
|
<Text
|
||||||
|
styles={{
|
||||||
|
root: {
|
||||||
|
fontWeight: FontWeights.semibold,
|
||||||
|
paddingTop: cardItemGapSmall,
|
||||||
|
paddingBottom: cardItemGapSmall,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
nowrap
|
||||||
|
>
|
||||||
|
{cardTitle}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text variant="small" styles={{ root: { height: smallTextLineHeight * 2 } }}>
|
||||||
|
{renderTruncatedDescription()}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<span>
|
||||||
|
{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>
|
||||||
|
</Card.Section>
|
||||||
|
|
||||||
|
{cardButtonsVisible && (
|
||||||
|
<Card.Section
|
||||||
|
styles={{
|
||||||
|
root: {
|
||||||
|
marginLeft: cardItemGapBig,
|
||||||
|
marginRight: cardItemGapBig,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Separator styles={{ root: { padding: 0, height: 1 } }} />
|
||||||
|
|
||||||
|
<span>
|
||||||
|
{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>
|
||||||
|
</Card.Section>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
@ -1,123 +0,0 @@
|
|||||||
import * as React from "react";
|
|
||||||
import { JunoClient } from "../../../Juno/JunoClient";
|
|
||||||
import { HttpStatusCodes, CodeOfConductEndpoints } from "../../../Common/Constants";
|
|
||||||
import { Stack, Text, Checkbox, PrimaryButton, Link } from "office-ui-fabric-react";
|
|
||||||
import { getErrorMessage, getErrorStack, handleError } from "../../../Common/ErrorHandlingUtils";
|
|
||||||
import { trace, traceFailure, traceStart, traceSuccess } from "../../../Shared/Telemetry/TelemetryProcessor";
|
|
||||||
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
|
|
||||||
|
|
||||||
export interface CodeOfConductComponentProps {
|
|
||||||
junoClient: JunoClient;
|
|
||||||
onAcceptCodeOfConduct: (result: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CodeOfConductComponentState {
|
|
||||||
readCodeOfConduct: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class CodeOfConductComponent extends React.Component<CodeOfConductComponentProps, CodeOfConductComponentState> {
|
|
||||||
private viewCodeOfConductTraced: boolean;
|
|
||||||
private descriptionPara1: string;
|
|
||||||
private descriptionPara2: string;
|
|
||||||
private descriptionPara3: string;
|
|
||||||
private link1: { label: string; url: string };
|
|
||||||
|
|
||||||
constructor(props: CodeOfConductComponentProps) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
readCodeOfConduct: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.descriptionPara1 = "Azure Cosmos DB Notebook Gallery - Code of Conduct";
|
|
||||||
this.descriptionPara2 = "The notebook public gallery contains notebook samples shared by users of Azure Cosmos DB.";
|
|
||||||
this.descriptionPara3 = "In order to view and publish your samples to the gallery, you must accept the ";
|
|
||||||
this.link1 = { label: "code of conduct.", url: CodeOfConductEndpoints.codeOfConduct };
|
|
||||||
}
|
|
||||||
|
|
||||||
private async acceptCodeOfConduct(): Promise<void> {
|
|
||||||
const startKey = traceStart(Action.NotebooksGalleryAcceptCodeOfConduct);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await this.props.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);
|
|
||||||
|
|
||||||
this.props.onAcceptCodeOfConduct(response.data);
|
|
||||||
} catch (error) {
|
|
||||||
traceFailure(
|
|
||||||
Action.NotebooksGalleryAcceptCodeOfConduct,
|
|
||||||
{
|
|
||||||
error: getErrorMessage(error),
|
|
||||||
errorStack: getErrorStack(error),
|
|
||||||
},
|
|
||||||
startKey
|
|
||||||
);
|
|
||||||
|
|
||||||
handleError(error, "CodeOfConductComponent/acceptCodeOfConduct", "Failed to accept code of conduct");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private onChangeCheckbox = (): void => {
|
|
||||||
this.setState({ readCodeOfConduct: !this.state.readCodeOfConduct });
|
|
||||||
};
|
|
||||||
|
|
||||||
public render(): JSX.Element {
|
|
||||||
if (!this.viewCodeOfConductTraced) {
|
|
||||||
this.viewCodeOfConductTraced = true;
|
|
||||||
trace(Action.NotebooksGalleryViewCodeOfConduct);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack tokens={{ childrenGap: 20 }}>
|
|
||||||
<Stack.Item>
|
|
||||||
<Text style={{ fontWeight: 500, fontSize: "20px" }}>{this.descriptionPara1}</Text>
|
|
||||||
</Stack.Item>
|
|
||||||
|
|
||||||
<Stack.Item>
|
|
||||||
<Text>{this.descriptionPara2}</Text>
|
|
||||||
</Stack.Item>
|
|
||||||
|
|
||||||
<Stack.Item>
|
|
||||||
<Text>
|
|
||||||
{this.descriptionPara3}
|
|
||||||
<Link href={this.link1.url} target="_blank">
|
|
||||||
{this.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={this.onChangeCheckbox}
|
|
||||||
/>
|
|
||||||
</Stack.Item>
|
|
||||||
|
|
||||||
<Stack.Item>
|
|
||||||
<PrimaryButton
|
|
||||||
ariaLabel="Continue"
|
|
||||||
title="Continue"
|
|
||||||
onClick={async () => await this.acceptCodeOfConduct()}
|
|
||||||
tabIndex={0}
|
|
||||||
className="genericPaneSubmitBtn"
|
|
||||||
text="Continue"
|
|
||||||
disabled={!this.state.readCodeOfConduct}
|
|
||||||
/>
|
|
||||||
</Stack.Item>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,9 +1,9 @@
|
|||||||
jest.mock("../../../Juno/JunoClient");
|
jest.mock("../../../../Juno/JunoClient");
|
||||||
import { shallow } from "enzyme";
|
import { shallow } from "enzyme";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { CodeOfConductComponent, CodeOfConductComponentProps } from "./CodeOfConductComponent";
|
import { CodeOfConductComponent, CodeOfConductComponentProps } from ".";
|
||||||
import { JunoClient } from "../../../Juno/JunoClient";
|
import { HttpStatusCodes } from "../../../../Common/Constants";
|
||||||
import { HttpStatusCodes } from "../../../Common/Constants";
|
import { JunoClient } from "../../../../Juno/JunoClient";
|
||||||
|
|
||||||
describe("CodeOfConductComponent", () => {
|
describe("CodeOfConductComponent", () => {
|
||||||
let codeOfConductProps: CodeOfConductComponentProps;
|
let codeOfConductProps: CodeOfConductComponentProps;
|
@ -0,0 +1,110 @@
|
|||||||
|
import { Checkbox, Link, PrimaryButton, Stack, Text } from "office-ui-fabric-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 CodeOfConductComponentProps {
|
||||||
|
junoClient: JunoClient;
|
||||||
|
onAcceptCodeOfConduct: (result: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CodeOfConductComponent: FunctionComponent<CodeOfConductComponentProps> = ({
|
||||||
|
junoClient,
|
||||||
|
onAcceptCodeOfConduct,
|
||||||
|
}: CodeOfConductComponentProps) => {
|
||||||
|
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, "CodeOfConductComponent/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>
|
||||||
|
);
|
||||||
|
};
|
@ -34,6 +34,7 @@ import { CodeOfConductComponent } from "./CodeOfConductComponent";
|
|||||||
import "./GalleryViewerComponent.less";
|
import "./GalleryViewerComponent.less";
|
||||||
import { InfoComponent } from "./InfoComponent/InfoComponent";
|
import { InfoComponent } from "./InfoComponent/InfoComponent";
|
||||||
|
|
||||||
|
const CARD_WIDTH = 256;
|
||||||
export interface GalleryViewerComponentProps {
|
export interface GalleryViewerComponentProps {
|
||||||
container?: Explorer;
|
container?: Explorer;
|
||||||
junoClient: JunoClient;
|
junoClient: JunoClient;
|
||||||
@ -643,7 +644,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
|||||||
|
|
||||||
private getPageSpecification = (itemIndex?: number, visibleRect?: IRectangle): IPageSpecification => {
|
private getPageSpecification = (itemIndex?: number, visibleRect?: IRectangle): IPageSpecification => {
|
||||||
if (itemIndex === 0) {
|
if (itemIndex === 0) {
|
||||||
this.columnCount = Math.floor(visibleRect.width / GalleryCardComponent.CARD_WIDTH) || this.columnCount;
|
this.columnCount = Math.floor(visibleRect.width / CARD_WIDTH) || this.columnCount;
|
||||||
this.rowCount = GalleryViewerComponent.rowsPerPage;
|
this.rowCount = GalleryViewerComponent.rowsPerPage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,7 +6,6 @@ import React from "react";
|
|||||||
import _ from "underscore";
|
import _ from "underscore";
|
||||||
import { AuthType } from "../AuthType";
|
import { AuthType } from "../AuthType";
|
||||||
import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer";
|
import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer";
|
||||||
import { ReactAdapter } from "../Bindings/ReactBindingHandler";
|
|
||||||
import * as Constants from "../Common/Constants";
|
import * as Constants from "../Common/Constants";
|
||||||
import { ExplorerMetrics } from "../Common/Constants";
|
import { ExplorerMetrics } from "../Common/Constants";
|
||||||
import { readCollection } from "../Common/dataAccess/readCollection";
|
import { readCollection } from "../Common/dataAccess/readCollection";
|
||||||
@ -174,7 +173,6 @@ export default class Explorer {
|
|||||||
public graphStylingPane: GraphStylingPane;
|
public graphStylingPane: GraphStylingPane;
|
||||||
public cassandraAddCollectionPane: CassandraAddCollectionPane;
|
public cassandraAddCollectionPane: CassandraAddCollectionPane;
|
||||||
public gitHubReposPane: ContextualPaneBase;
|
public gitHubReposPane: ContextualPaneBase;
|
||||||
public publishNotebookPaneAdapter: ReactAdapter;
|
|
||||||
|
|
||||||
// features
|
// features
|
||||||
public isGitHubPaneEnabled: ko.Observable<boolean>;
|
public isGitHubPaneEnabled: ko.Observable<boolean>;
|
||||||
@ -1410,7 +1408,6 @@ export default class Explorer {
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (this.notebookManager) {
|
if (this.notebookManager) {
|
||||||
await this.notebookManager.openPublishNotebookPane(name, content, parentDomElement);
|
await this.notebookManager.openPublishNotebookPane(name, content, parentDomElement);
|
||||||
this.publishNotebookPaneAdapter = this.notebookManager.publishNotebookPaneAdapter;
|
|
||||||
this.isPublishNotebookPaneEnabled(true);
|
this.isPublishNotebookPaneEnabled(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
* Contains all notebook related stuff meant to be dynamically loaded by explorer
|
* Contains all notebook related stuff meant to be dynamically loaded by explorer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { ImmutableNotebook } from "@nteract/commutable";
|
import { ImmutableNotebook } from "@nteract/commutable";
|
||||||
import type { IContentProvider } from "@nteract/core";
|
import type { IContentProvider } from "@nteract/core";
|
||||||
import ko from "knockout";
|
import ko from "knockout";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
@ -22,7 +22,7 @@ import Explorer from "../Explorer";
|
|||||||
import { ContextualPaneBase } from "../Panes/ContextualPaneBase";
|
import { ContextualPaneBase } from "../Panes/ContextualPaneBase";
|
||||||
import { CopyNotebookPane } from "../Panes/CopyNotebookPane/CopyNotebookPane";
|
import { CopyNotebookPane } from "../Panes/CopyNotebookPane/CopyNotebookPane";
|
||||||
import { GitHubReposPane } from "../Panes/GitHubReposPane";
|
import { GitHubReposPane } from "../Panes/GitHubReposPane";
|
||||||
import { PublishNotebookPaneAdapter } from "../Panes/PublishNotebookPaneAdapter";
|
import { PublishNotebookPane } from "../Panes/PublishNotebookPane/PublishNotebookPane";
|
||||||
import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter";
|
import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter";
|
||||||
import { NotebookContentProvider } from "./NotebookComponent/NotebookContentProvider";
|
import { NotebookContentProvider } from "./NotebookComponent/NotebookContentProvider";
|
||||||
import { NotebookContainerClient } from "./NotebookContainerClient";
|
import { NotebookContainerClient } from "./NotebookContainerClient";
|
||||||
@ -53,7 +53,6 @@ export default class NotebookManager {
|
|||||||
private gitHubClient: GitHubClient;
|
private gitHubClient: GitHubClient;
|
||||||
|
|
||||||
public gitHubReposPane: ContextualPaneBase;
|
public gitHubReposPane: ContextualPaneBase;
|
||||||
public publishNotebookPaneAdapter: PublishNotebookPaneAdapter;
|
|
||||||
|
|
||||||
public initialize(params: NotebookManagerOptions): void {
|
public initialize(params: NotebookManagerOptions): void {
|
||||||
this.params = params;
|
this.params = params;
|
||||||
@ -91,8 +90,6 @@ export default class NotebookManager {
|
|||||||
this.notebookContentProvider
|
this.notebookContentProvider
|
||||||
);
|
);
|
||||||
|
|
||||||
this.publishNotebookPaneAdapter = new PublishNotebookPaneAdapter(this.params.container, this.junoClient);
|
|
||||||
|
|
||||||
this.gitHubOAuthService.getTokenObservable().subscribe((token) => {
|
this.gitHubOAuthService.getTokenObservable().subscribe((token) => {
|
||||||
this.gitHubClient.setToken(token?.access_token);
|
this.gitHubClient.setToken(token?.access_token);
|
||||||
|
|
||||||
@ -123,7 +120,20 @@ export default class NotebookManager {
|
|||||||
content: NotebookPaneContent,
|
content: NotebookPaneContent,
|
||||||
parentDomElement: HTMLElement
|
parentDomElement: HTMLElement
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await this.publishNotebookPaneAdapter.open(name, getFullName(), content, parentDomElement);
|
const explorer = this.params.container;
|
||||||
|
explorer.openSidePanel(
|
||||||
|
"New Collection",
|
||||||
|
<PublishNotebookPane
|
||||||
|
explorer={this.params.container}
|
||||||
|
junoClient={this.junoClient}
|
||||||
|
closePanel={this.params.container.closeSidePanel}
|
||||||
|
openNotificationConsole={this.params.container.expandConsole}
|
||||||
|
name={name}
|
||||||
|
author={getFullName()}
|
||||||
|
notebookContent={content}
|
||||||
|
parentDomElement={parentDomElement}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public openCopyNotebookPane(name: string, content: string): void {
|
public openCopyNotebookPane(name: string, content: string): void {
|
||||||
|
@ -8,14 +8,15 @@ describe("PublishNotebookPaneComponent", () => {
|
|||||||
notebookName: "SampleNotebook.ipynb",
|
notebookName: "SampleNotebook.ipynb",
|
||||||
notebookDescription: "sample description",
|
notebookDescription: "sample description",
|
||||||
notebookTags: "tag1, tag2",
|
notebookTags: "tag1, tag2",
|
||||||
|
imageSrc: "https://i.ytimg.com/vi/E_lByLdKeKY/maxresdefault.jpg",
|
||||||
notebookAuthor: "CosmosDB",
|
notebookAuthor: "CosmosDB",
|
||||||
notebookCreatedDate: "2020-07-17T00:00:00Z",
|
notebookCreatedDate: "2020-07-17T00:00:00Z",
|
||||||
notebookObject: undefined,
|
notebookObject: undefined,
|
||||||
notebookParentDomElement: undefined,
|
notebookParentDomElement: undefined,
|
||||||
onChangeName: undefined,
|
setNotebookName: undefined,
|
||||||
onChangeDescription: undefined,
|
setNotebookDescription: undefined,
|
||||||
onChangeTags: undefined,
|
setNotebookTags: undefined,
|
||||||
onChangeImageSrc: undefined,
|
setImageSrc: undefined,
|
||||||
onError: undefined,
|
onError: undefined,
|
||||||
clearFormError: undefined,
|
clearFormError: undefined,
|
||||||
};
|
};
|
205
src/Explorer/Panes/PublishNotebookPane/PublishNotebookPane.tsx
Normal file
205
src/Explorer/Panes/PublishNotebookPane/PublishNotebookPane.tsx
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
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 { JunoClient } from "../../../Juno/JunoClient";
|
||||||
|
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||||
|
import { traceFailure, traceStart, traceSuccess } from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||||
|
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
|
||||||
|
import { CodeOfConductComponent } from "../../Controls/NotebookGallery/CodeOfConductComponent";
|
||||||
|
import { GalleryTab } from "../../Controls/NotebookGallery/GalleryViewerComponent";
|
||||||
|
import Explorer from "../../Explorer";
|
||||||
|
import * as FileSystemUtil from "../../Notebook/FileSystemUtil";
|
||||||
|
import {
|
||||||
|
GenericRightPaneComponent,
|
||||||
|
GenericRightPaneProps,
|
||||||
|
} from "../GenericRightPaneComponent/GenericRightPaneComponent";
|
||||||
|
import { PublishNotebookPaneComponent, PublishNotebookPaneProps } from "./PublishNotebookPaneComponent";
|
||||||
|
|
||||||
|
export interface PublishNotebookPaneAProps {
|
||||||
|
explorer: Explorer;
|
||||||
|
closePanel: () => void;
|
||||||
|
openNotificationConsole: () => void;
|
||||||
|
junoClient: JunoClient;
|
||||||
|
name: string;
|
||||||
|
author: string;
|
||||||
|
notebookContent: string | ImmutableNotebook;
|
||||||
|
parentDomElement: HTMLElement;
|
||||||
|
}
|
||||||
|
export const PublishNotebookPane: FunctionComponent<PublishNotebookPaneAProps> = ({
|
||||||
|
explorer: container,
|
||||||
|
junoClient,
|
||||||
|
closePanel,
|
||||||
|
name,
|
||||||
|
author,
|
||||||
|
notebookContent,
|
||||||
|
parentDomElement,
|
||||||
|
}: PublishNotebookPaneAProps): JSX.Element => {
|
||||||
|
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 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);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const submit = async (): Promise<void> => {
|
||||||
|
const clearPublishingMessage = NotificationConsoleUtils.logConsoleProgress(`Publishing ${name} to gallery`);
|
||||||
|
setIsExecuting(true);
|
||||||
|
|
||||||
|
let startKey: number;
|
||||||
|
|
||||||
|
if (!notebookName || !notebookDescription || !author || !imageSrc) {
|
||||||
|
setFormError(`Failed to publish ${notebookName} to gallery`);
|
||||||
|
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(","),
|
||||||
|
author,
|
||||||
|
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(`Failed to publish ${FileSystemUtil.stripExtension(notebookName, "ipynb")} to gallery`);
|
||||||
|
setFormErrorDetail(`${errorMessage}`);
|
||||||
|
handleError(errorMessage, "PublishNotebookPaneAdapter/submit", formError);
|
||||||
|
return;
|
||||||
|
} finally {
|
||||||
|
clearPublishingMessage();
|
||||||
|
setIsExecuting(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
closePanel();
|
||||||
|
};
|
||||||
|
|
||||||
|
const createFormError = (formError: string, formErrorDetail: string, area: string): void => {
|
||||||
|
setFormError(formError);
|
||||||
|
setFormErrorDetail(formErrorDetail);
|
||||||
|
handleError(formErrorDetail, area, formError);
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearFormError = (): void => {
|
||||||
|
setFormError("");
|
||||||
|
setFormErrorDetail("");
|
||||||
|
};
|
||||||
|
|
||||||
|
const props: GenericRightPaneProps = {
|
||||||
|
container: container,
|
||||||
|
formError: formError,
|
||||||
|
formErrorDetail: formErrorDetail,
|
||||||
|
id: "publishnotebookpane",
|
||||||
|
isExecuting: isExecuting,
|
||||||
|
title: "Publish to gallery",
|
||||||
|
submitButtonText: "Publish",
|
||||||
|
onSubmit: () => submit(),
|
||||||
|
onClose: closePanel,
|
||||||
|
isSubmitButtonHidden: !isCodeOfConductAccepted,
|
||||||
|
};
|
||||||
|
|
||||||
|
const publishNotebookPaneProps: PublishNotebookPaneProps = {
|
||||||
|
notebookDescription,
|
||||||
|
notebookTags,
|
||||||
|
imageSrc,
|
||||||
|
notebookName,
|
||||||
|
notebookAuthor: author,
|
||||||
|
notebookCreatedDate: new Date().toISOString(),
|
||||||
|
notebookObject: notebookObject,
|
||||||
|
notebookParentDomElement: parentDomElement,
|
||||||
|
onError: createFormError,
|
||||||
|
clearFormError: clearFormError,
|
||||||
|
setNotebookName,
|
||||||
|
setNotebookDescription,
|
||||||
|
setNotebookTags,
|
||||||
|
setImageSrc,
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<GenericRightPaneComponent {...props}>
|
||||||
|
{!isCodeOfConductAccepted ? (
|
||||||
|
<div style={{ padding: "25px", marginTop: "10px" }}>
|
||||||
|
<CodeOfConductComponent
|
||||||
|
junoClient={junoClient}
|
||||||
|
onAcceptCodeOfConduct={(isAccepted) => {
|
||||||
|
setIsCodeOfConductAccepted(isAccepted);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<PublishNotebookPaneComponent {...publishNotebookPaneProps} />
|
||||||
|
)}
|
||||||
|
</GenericRightPaneComponent>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,297 @@
|
|||||||
|
import { ImmutableNotebook } from "@nteract/commutable";
|
||||||
|
import Html2Canvas from "html2canvas";
|
||||||
|
import { Dropdown, IDropdownProps, ITextFieldProps, Stack, Text, TextField } from "office-ui-fabric-react";
|
||||||
|
import React, { FunctionComponent, useState } from "react";
|
||||||
|
import { GalleryCardComponent } from "../../Controls/NotebookGallery/Cards/GalleryCardComponent";
|
||||||
|
import * as FileSystemUtil from "../../Notebook/FileSystemUtil";
|
||||||
|
import { NotebookUtil } from "../../Notebook/NotebookUtil";
|
||||||
|
import "./styled.less";
|
||||||
|
|
||||||
|
export interface PublishNotebookPaneProps {
|
||||||
|
notebookName: string;
|
||||||
|
notebookAuthor: string;
|
||||||
|
notebookTags: string;
|
||||||
|
imageSrc: string;
|
||||||
|
notebookDescription: string;
|
||||||
|
notebookCreatedDate: string;
|
||||||
|
notebookObject: ImmutableNotebook;
|
||||||
|
notebookParentDomElement?: HTMLElement;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ImageTypes {
|
||||||
|
Url = "URL",
|
||||||
|
CustomImage = "Custom Image",
|
||||||
|
TakeScreenshot = "Take Screenshot",
|
||||||
|
UseFirstDisplayOutput = "Use First Display Output",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PublishNotebookPaneComponent: FunctionComponent<PublishNotebookPaneProps> = ({
|
||||||
|
notebookName,
|
||||||
|
notebookTags,
|
||||||
|
imageSrc,
|
||||||
|
notebookDescription,
|
||||||
|
notebookAuthor,
|
||||||
|
notebookCreatedDate,
|
||||||
|
notebookObject,
|
||||||
|
notebookParentDomElement,
|
||||||
|
onError,
|
||||||
|
clearFormError,
|
||||||
|
setNotebookName,
|
||||||
|
setNotebookDescription,
|
||||||
|
setNotebookTags,
|
||||||
|
setImageSrc,
|
||||||
|
}: 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 =
|
||||||
|
"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.";
|
||||||
|
|
||||||
|
const descriptionPara2 = `Would you like to publish and share "${FileSystemUtil.stripExtension(
|
||||||
|
notebookName,
|
||||||
|
"ipynb"
|
||||||
|
)}" to the gallery?`;
|
||||||
|
|
||||||
|
const options: ImageTypes[] = [ImageTypes.CustomImage, ImageTypes.Url];
|
||||||
|
const thumbnailSelectorProps: IDropdownProps = {
|
||||||
|
label: "Cover image",
|
||||||
|
defaultSelectedKey: ImageTypes.CustomImage,
|
||||||
|
ariaLabel: "Cover image",
|
||||||
|
options: options.map((value: string) => ({ text: value, key: value })),
|
||||||
|
onChange: async (event, options) => {
|
||||||
|
setImageSrc("");
|
||||||
|
clearFormError();
|
||||||
|
if (options.text === ImageTypes.TakeScreenshot) {
|
||||||
|
try {
|
||||||
|
await takeScreenshot(notebookParentDomElement, screenshotErrorHandler);
|
||||||
|
} catch (error) {
|
||||||
|
screenshotErrorHandler(error);
|
||||||
|
}
|
||||||
|
} else if (options.text === ImageTypes.UseFirstDisplayOutput) {
|
||||||
|
try {
|
||||||
|
await takeScreenshot(findFirstOutput(), firstOutputErrorHandler);
|
||||||
|
} catch (error) {
|
||||||
|
firstOutputErrorHandler(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setType(options.text);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const thumbnailUrlProps: ITextFieldProps = {
|
||||||
|
label: "Cover image url",
|
||||||
|
ariaLabel: "Cover image url",
|
||||||
|
required: true,
|
||||||
|
onChange: (event, newValue) => {
|
||||||
|
setImageSrc(newValue);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const screenshotErrorHandler = (error: Error) => {
|
||||||
|
const formError = "Failed to take screen shot";
|
||||||
|
const formErrorDetail = `${error}`;
|
||||||
|
const area = "PublishNotebookPaneComponent/takeScreenshot";
|
||||||
|
onError(formError, formErrorDetail, area);
|
||||||
|
};
|
||||||
|
|
||||||
|
const firstOutputErrorHandler = (error: Error) => {
|
||||||
|
const formError = "Failed to capture first output";
|
||||||
|
const formErrorDetail = `${error}`;
|
||||||
|
const area = "PublishNotebookPaneComponent/UseFirstOutput";
|
||||||
|
onError(formError, formErrorDetail, area);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (notebookParentDomElement) {
|
||||||
|
options.push(ImageTypes.TakeScreenshot);
|
||||||
|
if (notebookObject) {
|
||||||
|
options.push(ImageTypes.UseFirstDisplayOutput);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = `Failed to convert ${file.name} to base64 format`;
|
||||||
|
const formErrorDetail = `${error}`;
|
||||||
|
const area = "PublishNotebookPaneComponent/selectImageFile";
|
||||||
|
onError(formError, formErrorDetail, area);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const takeScreenshot = (target: HTMLElement, onError: (error: Error) => void): void => {
|
||||||
|
const updateImageSrcWithScreenshot = (canvasUrl: string): void => {
|
||||||
|
setImageSrc(canvasUrl);
|
||||||
|
};
|
||||||
|
|
||||||
|
target.scrollIntoView();
|
||||||
|
Html2Canvas(target, {
|
||||||
|
useCORS: true,
|
||||||
|
allowTaint: true,
|
||||||
|
scale: 1,
|
||||||
|
logging: true,
|
||||||
|
})
|
||||||
|
.then((canvas) => {
|
||||||
|
//redraw canvas to fit Card Cover Image dimensions
|
||||||
|
const originalImageData = canvas.toDataURL();
|
||||||
|
const requiredHeight = parseInt(canvas.style.width.split("px")[0]) * cardHeightToWidthRatio;
|
||||||
|
canvas.height = requiredHeight;
|
||||||
|
const context = canvas.getContext("2d");
|
||||||
|
const image = new Image();
|
||||||
|
image.src = originalImageData;
|
||||||
|
image.onload = () => {
|
||||||
|
context.drawImage(image, 0, 0);
|
||||||
|
updateImageSrcWithScreenshot(canvas.toDataURL());
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
onError(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
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 = `Failed to upload ${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 <></>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findFirstOutput = (): HTMLElement => {
|
||||||
|
const indexOfFirstCodeCellWithDisplay = NotebookUtil.findFirstCodeCellWithDisplay(notebookObject);
|
||||||
|
const cellOutputDomElements = notebookParentDomElement.querySelectorAll<HTMLElement>(".nteract-cell-outputs");
|
||||||
|
return cellOutputDomElements[indexOfFirstCodeCellWithDisplay];
|
||||||
|
};
|
||||||
|
|
||||||
|
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="Name"
|
||||||
|
ariaLabel="Name"
|
||||||
|
defaultValue={FileSystemUtil.stripExtension(notebookName, "ipynb")}
|
||||||
|
required
|
||||||
|
onChange={(event, newValue) => {
|
||||||
|
const notebookName = newValue + ".ipynb";
|
||||||
|
setNotebookName(notebookName);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Stack.Item>
|
||||||
|
|
||||||
|
<Stack.Item>
|
||||||
|
<TextField
|
||||||
|
label="Description"
|
||||||
|
ariaLabel="Description"
|
||||||
|
multiline
|
||||||
|
rows={3}
|
||||||
|
required
|
||||||
|
onChange={(event, newValue) => {
|
||||||
|
setNotebookDescription(newValue);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Stack.Item>
|
||||||
|
|
||||||
|
<Stack.Item>
|
||||||
|
<TextField
|
||||||
|
label="Tags"
|
||||||
|
ariaLabel="Tags"
|
||||||
|
placeholder="Optional tag 1, Optional tag 2"
|
||||||
|
onChange={(event, newValue) => {
|
||||||
|
setNotebookTags(newValue);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Stack.Item>
|
||||||
|
|
||||||
|
<Stack.Item>
|
||||||
|
<Dropdown {...thumbnailSelectorProps} />
|
||||||
|
</Stack.Item>
|
||||||
|
|
||||||
|
<Stack.Item>{renderThumbnailSelectors(type)}</Stack.Item>
|
||||||
|
|
||||||
|
<Stack.Item>
|
||||||
|
<Text>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>
|
||||||
|
);
|
||||||
|
};
|
@ -88,7 +88,7 @@ exports[`PublishNotebookPaneComponent renders 1`] = `
|
|||||||
Object {
|
Object {
|
||||||
"author": "CosmosDB",
|
"author": "CosmosDB",
|
||||||
"created": "2020-07-17T00:00:00Z",
|
"created": "2020-07-17T00:00:00Z",
|
||||||
"description": "",
|
"description": "sample description",
|
||||||
"downloads": undefined,
|
"downloads": undefined,
|
||||||
"favorites": undefined,
|
"favorites": undefined,
|
||||||
"gitSha": undefined,
|
"gitSha": undefined,
|
||||||
@ -99,12 +99,14 @@ exports[`PublishNotebookPaneComponent renders 1`] = `
|
|||||||
"pendingScanJobIds": undefined,
|
"pendingScanJobIds": undefined,
|
||||||
"policyViolations": undefined,
|
"policyViolations": undefined,
|
||||||
"tags": Array [
|
"tags": Array [
|
||||||
"",
|
"tag1",
|
||||||
|
" tag2",
|
||||||
],
|
],
|
||||||
"thumbnailUrl": undefined,
|
"thumbnailUrl": "https://i.ytimg.com/vi/E_lByLdKeKY/maxresdefault.jpg",
|
||||||
"views": undefined,
|
"views": undefined,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
onClick={[Function]}
|
||||||
showDelete={false}
|
showDelete={false}
|
||||||
showDownload={false}
|
showDownload={false}
|
||||||
/>
|
/>
|
@ -1,244 +0,0 @@
|
|||||||
import { toJS } from "@nteract/commutable";
|
|
||||||
import { ImmutableNotebook } from "@nteract/commutable/src";
|
|
||||||
import ko from "knockout";
|
|
||||||
import * as React from "react";
|
|
||||||
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
|
|
||||||
import { HttpStatusCodes } from "../../Common/Constants";
|
|
||||||
import { getErrorMessage, getErrorStack, handleError } from "../../Common/ErrorHandlingUtils";
|
|
||||||
import { JunoClient } from "../../Juno/JunoClient";
|
|
||||||
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
|
|
||||||
import { traceFailure, traceStart, traceSuccess } from "../../Shared/Telemetry/TelemetryProcessor";
|
|
||||||
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
|
|
||||||
import { CodeOfConductComponent } from "../Controls/NotebookGallery/CodeOfConductComponent";
|
|
||||||
import { GalleryTab } from "../Controls/NotebookGallery/GalleryViewerComponent";
|
|
||||||
import Explorer from "../Explorer";
|
|
||||||
import * as FileSystemUtil from "../Notebook/FileSystemUtil";
|
|
||||||
import {
|
|
||||||
GenericRightPaneComponent,
|
|
||||||
GenericRightPaneProps,
|
|
||||||
} from "./GenericRightPaneComponent/GenericRightPaneComponent";
|
|
||||||
import { PublishNotebookPaneComponent, PublishNotebookPaneProps } from "./PublishNotebookPaneComponent";
|
|
||||||
|
|
||||||
export class PublishNotebookPaneAdapter implements ReactAdapter {
|
|
||||||
parameters: ko.Observable<number>;
|
|
||||||
private isOpened: boolean;
|
|
||||||
private isExecuting: boolean;
|
|
||||||
private formError: string;
|
|
||||||
private formErrorDetail: string;
|
|
||||||
|
|
||||||
private name: string;
|
|
||||||
private author: string;
|
|
||||||
private content: string;
|
|
||||||
private description: string;
|
|
||||||
private tags: string;
|
|
||||||
private imageSrc: string;
|
|
||||||
private notebookObject: ImmutableNotebook;
|
|
||||||
private parentDomElement: HTMLElement;
|
|
||||||
private isCodeOfConductAccepted: boolean;
|
|
||||||
|
|
||||||
constructor(private container: Explorer, private junoClient: JunoClient) {
|
|
||||||
this.parameters = ko.observable(Date.now());
|
|
||||||
this.reset();
|
|
||||||
this.triggerRender();
|
|
||||||
}
|
|
||||||
|
|
||||||
public renderComponent(): JSX.Element {
|
|
||||||
if (!this.isOpened) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const props: GenericRightPaneProps = {
|
|
||||||
container: this.container,
|
|
||||||
formError: this.formError,
|
|
||||||
formErrorDetail: this.formErrorDetail,
|
|
||||||
id: "publishnotebookpane",
|
|
||||||
isExecuting: this.isExecuting,
|
|
||||||
title: "Publish to gallery",
|
|
||||||
submitButtonText: "Publish",
|
|
||||||
onClose: () => this.close(),
|
|
||||||
onSubmit: () => this.submit(),
|
|
||||||
isSubmitButtonHidden: !this.isCodeOfConductAccepted,
|
|
||||||
};
|
|
||||||
|
|
||||||
const publishNotebookPaneProps: PublishNotebookPaneProps = {
|
|
||||||
notebookName: this.name,
|
|
||||||
notebookDescription: "",
|
|
||||||
notebookTags: "",
|
|
||||||
notebookAuthor: this.author,
|
|
||||||
notebookCreatedDate: new Date().toISOString(),
|
|
||||||
notebookObject: this.notebookObject,
|
|
||||||
notebookParentDomElement: this.parentDomElement,
|
|
||||||
onChangeName: (newValue: string) => (this.name = newValue),
|
|
||||||
onChangeDescription: (newValue: string) => (this.description = newValue),
|
|
||||||
onChangeTags: (newValue: string) => (this.tags = newValue),
|
|
||||||
onChangeImageSrc: (newValue: string) => (this.imageSrc = newValue),
|
|
||||||
onError: this.createFormError,
|
|
||||||
clearFormError: this.clearFormError,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<GenericRightPaneComponent {...props}>
|
|
||||||
{!this.isCodeOfConductAccepted ? (
|
|
||||||
<div style={{ padding: "15px", marginTop: "10px" }}>
|
|
||||||
<CodeOfConductComponent
|
|
||||||
junoClient={this.junoClient}
|
|
||||||
onAcceptCodeOfConduct={() => {
|
|
||||||
this.isCodeOfConductAccepted = true;
|
|
||||||
this.triggerRender();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<PublishNotebookPaneComponent {...publishNotebookPaneProps} />
|
|
||||||
)}
|
|
||||||
</GenericRightPaneComponent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public triggerRender(): void {
|
|
||||||
window.requestAnimationFrame(() => this.parameters(Date.now()));
|
|
||||||
}
|
|
||||||
|
|
||||||
public async open(
|
|
||||||
name: string,
|
|
||||||
author: string,
|
|
||||||
notebookContent: string | ImmutableNotebook,
|
|
||||||
parentDomElement: HTMLElement
|
|
||||||
): Promise<void> {
|
|
||||||
try {
|
|
||||||
const response = await this.junoClient.isCodeOfConductAccepted();
|
|
||||||
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
|
|
||||||
throw new Error(`Received HTTP ${response.status} when accepting code of conduct`);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.isCodeOfConductAccepted = response.data;
|
|
||||||
} catch (error) {
|
|
||||||
handleError(
|
|
||||||
error,
|
|
||||||
"PublishNotebookPaneAdapter/isCodeOfConductAccepted",
|
|
||||||
"Failed to check if code of conduct was accepted"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.name = name;
|
|
||||||
this.author = author;
|
|
||||||
if (typeof notebookContent === "string") {
|
|
||||||
this.content = notebookContent as string;
|
|
||||||
} else {
|
|
||||||
this.content = JSON.stringify(toJS(notebookContent));
|
|
||||||
this.notebookObject = notebookContent;
|
|
||||||
}
|
|
||||||
this.parentDomElement = parentDomElement;
|
|
||||||
|
|
||||||
this.isOpened = true;
|
|
||||||
this.triggerRender();
|
|
||||||
}
|
|
||||||
|
|
||||||
public close(): void {
|
|
||||||
this.reset();
|
|
||||||
this.triggerRender();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async submit(): Promise<void> {
|
|
||||||
const clearPublishingMessage = NotificationConsoleUtils.logConsoleProgress(`Publishing ${this.name} to gallery`);
|
|
||||||
this.isExecuting = true;
|
|
||||||
this.triggerRender();
|
|
||||||
|
|
||||||
let startKey: number;
|
|
||||||
|
|
||||||
if (!this.name || !this.description || !this.author || !this.imageSrc) {
|
|
||||||
const formError = `Failed to publish ${this.name} to gallery`;
|
|
||||||
const formErrorDetail = "Name, description, author and cover image are required";
|
|
||||||
this.createFormError(formError, formErrorDetail, "PublishNotebookPaneAdapter/submit");
|
|
||||||
this.isExecuting = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
startKey = traceStart(Action.NotebooksGalleryPublish, {});
|
|
||||||
|
|
||||||
const response = await this.junoClient.publishNotebook(
|
|
||||||
this.name,
|
|
||||||
this.description,
|
|
||||||
this.tags?.split(","),
|
|
||||||
this.author,
|
|
||||||
this.imageSrc,
|
|
||||||
this.content
|
|
||||||
);
|
|
||||||
|
|
||||||
const data = response.data;
|
|
||||||
if (data) {
|
|
||||||
let isPublishPending = false;
|
|
||||||
|
|
||||||
if (data.pendingScanJobIds?.length > 0) {
|
|
||||||
isPublishPending = true;
|
|
||||||
NotificationConsoleUtils.logConsoleInfo(
|
|
||||||
`Content of ${this.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 ${this.name} to gallery`);
|
|
||||||
this.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);
|
|
||||||
this.formError = `Failed to publish ${FileSystemUtil.stripExtension(this.name, "ipynb")} to gallery`;
|
|
||||||
this.formErrorDetail = `${errorMessage}`;
|
|
||||||
handleError(errorMessage, "PublishNotebookPaneAdapter/submit", this.formError);
|
|
||||||
return;
|
|
||||||
} finally {
|
|
||||||
clearPublishingMessage();
|
|
||||||
this.isExecuting = false;
|
|
||||||
this.triggerRender();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
private createFormError = (formError: string, formErrorDetail: string, area: string): void => {
|
|
||||||
this.formError = formError;
|
|
||||||
this.formErrorDetail = formErrorDetail;
|
|
||||||
handleError(formErrorDetail, area, formError);
|
|
||||||
this.triggerRender();
|
|
||||||
};
|
|
||||||
|
|
||||||
private clearFormError = (): void => {
|
|
||||||
this.formError = undefined;
|
|
||||||
this.formErrorDetail = undefined;
|
|
||||||
this.triggerRender();
|
|
||||||
};
|
|
||||||
|
|
||||||
private reset = (): void => {
|
|
||||||
this.isOpened = false;
|
|
||||||
this.isExecuting = false;
|
|
||||||
this.formError = undefined;
|
|
||||||
this.formErrorDetail = undefined;
|
|
||||||
this.name = undefined;
|
|
||||||
this.author = undefined;
|
|
||||||
this.content = undefined;
|
|
||||||
this.description = undefined;
|
|
||||||
this.tags = undefined;
|
|
||||||
this.imageSrc = undefined;
|
|
||||||
this.notebookObject = undefined;
|
|
||||||
this.parentDomElement = undefined;
|
|
||||||
this.isCodeOfConductAccepted = undefined;
|
|
||||||
};
|
|
||||||
}
|
|
@ -1,326 +0,0 @@
|
|||||||
import { ITextFieldProps, Stack, Text, TextField, Dropdown, IDropdownProps } from "office-ui-fabric-react";
|
|
||||||
import * as React from "react";
|
|
||||||
import { GalleryCardComponent } from "../Controls/NotebookGallery/Cards/GalleryCardComponent";
|
|
||||||
import * as FileSystemUtil from "../Notebook/FileSystemUtil";
|
|
||||||
import "./PublishNotebookPaneComponent.less";
|
|
||||||
import Html2Canvas from "html2canvas";
|
|
||||||
import { ImmutableNotebook } from "@nteract/commutable/src";
|
|
||||||
import { NotebookUtil } from "../Notebook/NotebookUtil";
|
|
||||||
|
|
||||||
export interface PublishNotebookPaneProps {
|
|
||||||
notebookName: string;
|
|
||||||
notebookDescription: string;
|
|
||||||
notebookTags: string;
|
|
||||||
notebookAuthor: string;
|
|
||||||
notebookCreatedDate: string;
|
|
||||||
notebookObject: ImmutableNotebook;
|
|
||||||
notebookParentDomElement?: HTMLElement;
|
|
||||||
onChangeName: (newValue: string) => void;
|
|
||||||
onChangeDescription: (newValue: string) => void;
|
|
||||||
onChangeTags: (newValue: string) => void;
|
|
||||||
onChangeImageSrc: (newValue: string) => void;
|
|
||||||
onError: (formError: string, formErrorDetail: string, area: string) => void;
|
|
||||||
clearFormError: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PublishNotebookPaneState {
|
|
||||||
type: string;
|
|
||||||
notebookName: string;
|
|
||||||
notebookDescription: string;
|
|
||||||
notebookTags: string;
|
|
||||||
imageSrc: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
enum ImageTypes {
|
|
||||||
Url = "URL",
|
|
||||||
CustomImage = "Custom Image",
|
|
||||||
TakeScreenshot = "Take Screenshot",
|
|
||||||
UseFirstDisplayOutput = "Use First Display Output",
|
|
||||||
}
|
|
||||||
|
|
||||||
export class PublishNotebookPaneComponent extends React.Component<PublishNotebookPaneProps, PublishNotebookPaneState> {
|
|
||||||
private static readonly maxImageSizeInMib = 1.5;
|
|
||||||
private descriptionPara1: string;
|
|
||||||
private descriptionPara2: string;
|
|
||||||
private nameProps: ITextFieldProps;
|
|
||||||
private descriptionProps: ITextFieldProps;
|
|
||||||
private tagsProps: ITextFieldProps;
|
|
||||||
private thumbnailUrlProps: ITextFieldProps;
|
|
||||||
private thumbnailSelectorProps: IDropdownProps;
|
|
||||||
private imageToBase64: (file: File, updateImageSrc: (result: string) => void) => void;
|
|
||||||
private takeScreenshot: (target: HTMLElement, onError: (error: Error) => void) => void;
|
|
||||||
|
|
||||||
constructor(props: PublishNotebookPaneProps) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
type: ImageTypes.CustomImage,
|
|
||||||
notebookName: props.notebookName,
|
|
||||||
notebookDescription: "",
|
|
||||||
notebookTags: "",
|
|
||||||
imageSrc: undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.imageToBase64 = (file: File, updateImageSrc: (result: string) => void) => {
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
reader.onload = () => {
|
|
||||||
updateImageSrc(reader.result.toString());
|
|
||||||
};
|
|
||||||
|
|
||||||
const onError = this.props.onError;
|
|
||||||
reader.onerror = (error) => {
|
|
||||||
const formError = `Failed to convert ${file.name} to base64 format`;
|
|
||||||
const formErrorDetail = `${error}`;
|
|
||||||
const area = "PublishNotebookPaneComponent/selectImageFile";
|
|
||||||
onError(formError, formErrorDetail, area);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
this.takeScreenshot = (target: HTMLElement, onError: (error: Error) => void): void => {
|
|
||||||
const updateImageSrcWithScreenshot = (canvasUrl: string): void => {
|
|
||||||
this.props.onChangeImageSrc(canvasUrl);
|
|
||||||
this.setState({ imageSrc: canvasUrl });
|
|
||||||
};
|
|
||||||
|
|
||||||
target.scrollIntoView();
|
|
||||||
Html2Canvas(target, {
|
|
||||||
useCORS: true,
|
|
||||||
allowTaint: true,
|
|
||||||
scale: 1,
|
|
||||||
logging: true,
|
|
||||||
})
|
|
||||||
.then((canvas) => {
|
|
||||||
//redraw canvas to fit Card Cover Image dimensions
|
|
||||||
const originalImageData = canvas.toDataURL();
|
|
||||||
const requiredHeight =
|
|
||||||
parseInt(canvas.style.width.split("px")[0]) * GalleryCardComponent.cardHeightToWidthRatio;
|
|
||||||
canvas.height = requiredHeight;
|
|
||||||
const context = canvas.getContext("2d");
|
|
||||||
const image = new Image();
|
|
||||||
image.src = originalImageData;
|
|
||||||
image.onload = () => {
|
|
||||||
context.drawImage(image, 0, 0);
|
|
||||||
updateImageSrcWithScreenshot(canvas.toDataURL());
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
onError(error);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
this.descriptionPara1 =
|
|
||||||
"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.";
|
|
||||||
|
|
||||||
this.descriptionPara2 = `Would you like to publish and share "${FileSystemUtil.stripExtension(
|
|
||||||
this.props.notebookName,
|
|
||||||
"ipynb"
|
|
||||||
)}" to the gallery?`;
|
|
||||||
|
|
||||||
this.thumbnailUrlProps = {
|
|
||||||
label: "Cover image url",
|
|
||||||
ariaLabel: "Cover image url",
|
|
||||||
required: true,
|
|
||||||
onChange: (event, newValue) => {
|
|
||||||
this.props.onChangeImageSrc(newValue);
|
|
||||||
this.setState({ imageSrc: newValue });
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const screenshotErrorHandler = (error: Error) => {
|
|
||||||
const formError = "Failed to take screen shot";
|
|
||||||
const formErrorDetail = `${error}`;
|
|
||||||
const area = "PublishNotebookPaneComponent/takeScreenshot";
|
|
||||||
this.props.onError(formError, formErrorDetail, area);
|
|
||||||
};
|
|
||||||
|
|
||||||
const firstOutputErrorHandler = (error: Error) => {
|
|
||||||
const formError = "Failed to capture first output";
|
|
||||||
const formErrorDetail = `${error}`;
|
|
||||||
const area = "PublishNotebookPaneComponent/UseFirstOutput";
|
|
||||||
this.props.onError(formError, formErrorDetail, area);
|
|
||||||
};
|
|
||||||
|
|
||||||
const options: ImageTypes[] = [ImageTypes.CustomImage, ImageTypes.Url];
|
|
||||||
|
|
||||||
if (this.props.notebookParentDomElement) {
|
|
||||||
options.push(ImageTypes.TakeScreenshot);
|
|
||||||
if (this.props.notebookObject) {
|
|
||||||
options.push(ImageTypes.UseFirstDisplayOutput);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.thumbnailSelectorProps = {
|
|
||||||
label: "Cover image",
|
|
||||||
defaultSelectedKey: ImageTypes.CustomImage,
|
|
||||||
ariaLabel: "Cover image",
|
|
||||||
options: options.map((value: string) => ({ text: value, key: value })),
|
|
||||||
onChange: async (event, options) => {
|
|
||||||
this.setState({ imageSrc: undefined });
|
|
||||||
this.props.onChangeImageSrc(undefined);
|
|
||||||
this.props.clearFormError();
|
|
||||||
if (options.text === ImageTypes.TakeScreenshot) {
|
|
||||||
try {
|
|
||||||
await this.takeScreenshot(this.props.notebookParentDomElement, screenshotErrorHandler);
|
|
||||||
} catch (error) {
|
|
||||||
screenshotErrorHandler(error);
|
|
||||||
}
|
|
||||||
} else if (options.text === ImageTypes.UseFirstDisplayOutput) {
|
|
||||||
try {
|
|
||||||
await this.takeScreenshot(this.findFirstOutput(), firstOutputErrorHandler);
|
|
||||||
} catch (error) {
|
|
||||||
firstOutputErrorHandler(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.setState({ type: options.text });
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
this.nameProps = {
|
|
||||||
label: "Name",
|
|
||||||
ariaLabel: "Name",
|
|
||||||
defaultValue: FileSystemUtil.stripExtension(this.props.notebookName, "ipynb"),
|
|
||||||
required: true,
|
|
||||||
onChange: (event, newValue) => {
|
|
||||||
const notebookName = newValue + ".ipynb";
|
|
||||||
this.props.onChangeName(notebookName);
|
|
||||||
this.setState({ notebookName });
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
this.descriptionProps = {
|
|
||||||
label: "Description",
|
|
||||||
ariaLabel: "Description",
|
|
||||||
multiline: true,
|
|
||||||
rows: 3,
|
|
||||||
required: true,
|
|
||||||
onChange: (event, newValue) => {
|
|
||||||
this.props.onChangeDescription(newValue);
|
|
||||||
this.setState({ notebookDescription: newValue });
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
this.tagsProps = {
|
|
||||||
label: "Tags",
|
|
||||||
ariaLabel: "Tags",
|
|
||||||
placeholder: "Optional tag 1, Optional tag 2",
|
|
||||||
onChange: (event, newValue) => {
|
|
||||||
this.props.onChangeTags(newValue);
|
|
||||||
this.setState({ notebookTags: newValue });
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderThumbnailSelectors(type: string) {
|
|
||||||
switch (type) {
|
|
||||||
case ImageTypes.Url:
|
|
||||||
return <TextField {...this.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 > PublishNotebookPaneComponent.maxImageSizeInMib) {
|
|
||||||
event.target.value = "";
|
|
||||||
const formError = `Failed to upload ${file.name}`;
|
|
||||||
const formErrorDetail = `Image is larger than ${PublishNotebookPaneComponent.maxImageSizeInMib} MiB. Please Choose a different image.`;
|
|
||||||
const area = "PublishNotebookPaneComponent/selectImageFile";
|
|
||||||
|
|
||||||
this.props.onError(formError, formErrorDetail, area);
|
|
||||||
this.props.onChangeImageSrc(undefined);
|
|
||||||
this.setState({ imageSrc: undefined });
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
this.props.clearFormError();
|
|
||||||
}
|
|
||||||
this.imageToBase64(file, (result: string) => {
|
|
||||||
this.props.onChangeImageSrc(result);
|
|
||||||
this.setState({ imageSrc: result });
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
return <></>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private findFirstOutput(): HTMLElement {
|
|
||||||
const indexOfFirstCodeCellWithDisplay = NotebookUtil.findFirstCodeCellWithDisplay(this.props.notebookObject);
|
|
||||||
const cellOutputDomElements = this.props.notebookParentDomElement.querySelectorAll<HTMLElement>(
|
|
||||||
".nteract-cell-outputs"
|
|
||||||
);
|
|
||||||
return cellOutputDomElements[indexOfFirstCodeCellWithDisplay];
|
|
||||||
}
|
|
||||||
|
|
||||||
public render(): JSX.Element {
|
|
||||||
return (
|
|
||||||
<div className="publishNotebookPanelContent">
|
|
||||||
<Stack className="panelMainContent" tokens={{ childrenGap: 20 }}>
|
|
||||||
<Stack.Item>
|
|
||||||
<Text>{this.descriptionPara1}</Text>
|
|
||||||
</Stack.Item>
|
|
||||||
|
|
||||||
<Stack.Item>
|
|
||||||
<Text>{this.descriptionPara2}</Text>
|
|
||||||
</Stack.Item>
|
|
||||||
|
|
||||||
<Stack.Item>
|
|
||||||
<TextField {...this.nameProps} />
|
|
||||||
</Stack.Item>
|
|
||||||
|
|
||||||
<Stack.Item>
|
|
||||||
<TextField {...this.descriptionProps} />
|
|
||||||
</Stack.Item>
|
|
||||||
|
|
||||||
<Stack.Item>
|
|
||||||
<TextField {...this.tagsProps} />
|
|
||||||
</Stack.Item>
|
|
||||||
|
|
||||||
<Stack.Item>
|
|
||||||
<Dropdown {...this.thumbnailSelectorProps} />
|
|
||||||
</Stack.Item>
|
|
||||||
|
|
||||||
<Stack.Item>{this.renderThumbnailSelectors(this.state.type)}</Stack.Item>
|
|
||||||
|
|
||||||
<Stack.Item>
|
|
||||||
<Text>Preview</Text>
|
|
||||||
</Stack.Item>
|
|
||||||
<Stack.Item>
|
|
||||||
<GalleryCardComponent
|
|
||||||
data={{
|
|
||||||
id: undefined,
|
|
||||||
name: this.state.notebookName,
|
|
||||||
description: this.state.notebookDescription,
|
|
||||||
gitSha: undefined,
|
|
||||||
tags: this.state.notebookTags.split(","),
|
|
||||||
author: this.props.notebookAuthor,
|
|
||||||
thumbnailUrl: this.state.imageSrc,
|
|
||||||
created: this.props.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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -235,9 +235,6 @@ const App: React.FunctionComponent = () => {
|
|||||||
<KOCommentIfStart if="isGitHubPaneEnabled" />
|
<KOCommentIfStart if="isGitHubPaneEnabled" />
|
||||||
<div data-bind='component: { name: "github-repos-pane", params: { data: gitHubReposPane } }' />
|
<div data-bind='component: { name: "github-repos-pane", params: { data: gitHubReposPane } }' />
|
||||||
<KOCommentEnd />
|
<KOCommentEnd />
|
||||||
<KOCommentIfStart if="isPublishNotebookPaneEnabled" />
|
|
||||||
<div data-bind="react: publishNotebookPaneAdapter" />
|
|
||||||
<KOCommentEnd />
|
|
||||||
{showDialog && <Dialog {...dialogProps} />}
|
{showDialog && <Dialog {...dialogProps} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { decryptJWTToken } from "./AuthorizationUtils";
|
|
||||||
import { userContext } from "../UserContext";
|
import { userContext } from "../UserContext";
|
||||||
|
import { decryptJWTToken } from "./AuthorizationUtils";
|
||||||
|
|
||||||
export function getFullName(): string {
|
export const getFullName = (): string => {
|
||||||
const authToken = userContext.authorizationToken;
|
const { authorizationToken } = userContext;
|
||||||
const props = decryptJWTToken(authToken);
|
const { name } = decryptJWTToken(authorizationToken);
|
||||||
return props.name;
|
return name;
|
||||||
}
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user