Add Report Abuse dialog for public gallery notebooks (#265)

![image](https://user-images.githubusercontent.com/693092/95408825-3975a680-08d5-11eb-812b-80f922ab9fc8.png)
This commit is contained in:
Tanuj Mittal 2020-10-12 16:48:05 -07:00 committed by GitHub
parent daba1c4ed4
commit 3b64d75322
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 317 additions and 105 deletions

View File

@ -3,7 +3,7 @@ import { Dialog, DialogType, DialogFooter, IDialogProps } from "office-ui-fabric
import { IButtonProps, PrimaryButton, DefaultButton } from "office-ui-fabric-react/lib/Button"; import { IButtonProps, PrimaryButton, DefaultButton } from "office-ui-fabric-react/lib/Button";
import { ITextFieldProps, TextField } from "office-ui-fabric-react/lib/TextField"; import { ITextFieldProps, TextField } from "office-ui-fabric-react/lib/TextField";
import { Link } from "office-ui-fabric-react/lib/Link"; import { Link } from "office-ui-fabric-react/lib/Link";
import { FontIcon } from "office-ui-fabric-react"; import { ChoiceGroup, FontIcon, IChoiceGroupProps } from "office-ui-fabric-react";
export interface TextFieldProps extends ITextFieldProps { export interface TextFieldProps extends ITextFieldProps {
label: string; label: string;
@ -24,6 +24,7 @@ export interface DialogProps {
subText: string; subText: string;
isModal: boolean; isModal: boolean;
visible: boolean; visible: boolean;
choiceGroupProps?: IChoiceGroupProps;
textFieldProps?: TextFieldProps; textFieldProps?: TextFieldProps;
linkProps?: LinkProps; linkProps?: LinkProps;
primaryButtonText: string; primaryButtonText: string;
@ -65,6 +66,7 @@ export class DialogComponent extends React.Component<DialogProps, {}> {
minWidth: DIALOG_MIN_WIDTH, minWidth: DIALOG_MIN_WIDTH,
maxWidth: DIALOG_MAX_WIDTH maxWidth: DIALOG_MAX_WIDTH
}; };
const choiceGroupProps: IChoiceGroupProps = this.props.choiceGroupProps;
const textFieldProps: ITextFieldProps = this.props.textFieldProps; const textFieldProps: ITextFieldProps = this.props.textFieldProps;
const linkProps: LinkProps = this.props.linkProps; const linkProps: LinkProps = this.props.linkProps;
const primaryButtonProps: IButtonProps = { const primaryButtonProps: IButtonProps = {
@ -82,6 +84,7 @@ export class DialogComponent extends React.Component<DialogProps, {}> {
return ( return (
<Dialog {...dialogProps}> <Dialog {...dialogProps}>
{choiceGroupProps && <ChoiceGroup {...choiceGroupProps} />}
{textFieldProps && <TextField {...textFieldProps} />} {textFieldProps && <TextField {...textFieldProps} />}
{linkProps && ( {linkProps && (
<Link href={linkProps.linkUrl} target="_blank"> <Link href={linkProps.linkUrl} target="_blank">

View File

@ -227,7 +227,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
<Stack.Item styles={{ root: { minWidth: 200 } }}> <Stack.Item styles={{ root: { minWidth: 200 } }}>
<Dropdown options={this.sortingOptions} selectedKey={this.state.sortBy} onChange={this.onDropdownChange} /> <Dropdown options={this.sortingOptions} selectedKey={this.state.sortBy} onChange={this.onDropdownChange} />
</Stack.Item> </Stack.Item>
{this.props.container?.isGalleryPublishEnabled() && ( {(!this.props.container || this.props.container.isGalleryPublishEnabled()) && (
<Stack.Item> <Stack.Item>
<InfoComponent /> <InfoComponent />
</Stack.Item> </Stack.Item>

View File

@ -3,10 +3,14 @@ import { Icon, Label, Stack, HoverCard, HoverCardType, Link } from "office-ui-fa
import { CodeOfConductEndpoints } from "../../../../Common/Constants"; import { CodeOfConductEndpoints } from "../../../../Common/Constants";
import "./InfoComponent.less"; import "./InfoComponent.less";
export class InfoComponent extends React.Component { export interface InfoComponentProps {
private getInfoPanel = (iconName: string, labelText: string, url: string): JSX.Element => { onReportAbuseClick?: () => void;
}
export class InfoComponent extends React.Component<InfoComponentProps> {
private getInfoPanel = (iconName: string, labelText: string, url?: string, onClick?: () => void): JSX.Element => {
return ( return (
<Link href={url} target="_blank"> <Link href={url} target={url && "_blank"} onClick={onClick}>
<div className="infoPanel"> <div className="infoPanel">
<Icon iconName={iconName} styles={{ root: { verticalAlign: "middle" } }} /> <Icon iconName={iconName} styles={{ root: { verticalAlign: "middle" } }} />
<Label className="infoLabel">{labelText}</Label> <Label className="infoLabel">{labelText}</Label>
@ -25,6 +29,11 @@ export class InfoComponent extends React.Component {
<Stack.Item> <Stack.Item>
{this.getInfoPanel("KnowledgeArticle", "Microsoft Terms of Use", CodeOfConductEndpoints.termsOfUse)} {this.getInfoPanel("KnowledgeArticle", "Microsoft Terms of Use", CodeOfConductEndpoints.termsOfUse)}
</Stack.Item> </Stack.Item>
{this.props.onReportAbuseClick !== undefined && (
<Stack.Item>
{this.getInfoPanel("ReportHacked", "Report Abuse", undefined, () => this.props.onReportAbuseClick())}
</Stack.Item>
)}
</Stack> </Stack>
); );
}; };

View File

@ -77,6 +77,9 @@ exports[`GalleryViewerComponent renders 1`] = `
selectedKey={0} selectedKey={0}
/> />
</StackItem> </StackItem>
<StackItem>
<InfoComponent />
</StackItem>
</Stack> </Stack>
</Stack> </Stack>
</PivotItem> </PivotItem>

View File

@ -25,7 +25,8 @@ describe("NotebookMetadataComponent", () => {
onTagClick: undefined, onTagClick: undefined,
onDownloadClick: undefined, onDownloadClick: undefined,
onFavoriteClick: undefined, onFavoriteClick: undefined,
onUnfavoriteClick: undefined onUnfavoriteClick: undefined,
onReportAbuseClick: undefined
}; };
const wrapper = shallow(<NotebookMetadataComponent {...props} />); const wrapper = shallow(<NotebookMetadataComponent {...props} />);
@ -54,7 +55,8 @@ describe("NotebookMetadataComponent", () => {
onTagClick: undefined, onTagClick: undefined,
onDownloadClick: undefined, onDownloadClick: undefined,
onFavoriteClick: undefined, onFavoriteClick: undefined,
onUnfavoriteClick: undefined onUnfavoriteClick: undefined,
onReportAbuseClick: undefined
}; };
const wrapper = shallow(<NotebookMetadataComponent {...props} />); const wrapper = shallow(<NotebookMetadataComponent {...props} />);

View File

@ -17,6 +17,7 @@ import { IGalleryItem } from "../../../Juno/JunoClient";
import { FileSystemUtil } from "../../Notebook/FileSystemUtil"; import { FileSystemUtil } from "../../Notebook/FileSystemUtil";
import "./NotebookViewerComponent.less"; import "./NotebookViewerComponent.less";
import CosmosDBLogo from "../../../../images/CosmosDB-logo.svg"; import CosmosDBLogo from "../../../../images/CosmosDB-logo.svg";
import { InfoComponent } from "../NotebookGallery/InfoComponent/InfoComponent";
export interface NotebookMetadataComponentProps { export interface NotebookMetadataComponentProps {
data: IGalleryItem; data: IGalleryItem;
@ -26,6 +27,7 @@ export interface NotebookMetadataComponentProps {
onFavoriteClick: () => void; onFavoriteClick: () => void;
onUnfavoriteClick: () => void; onUnfavoriteClick: () => void;
onDownloadClick: () => void; onDownloadClick: () => void;
onReportAbuseClick: () => void;
} }
export class NotebookMetadataComponent extends React.Component<NotebookMetadataComponentProps> { export class NotebookMetadataComponent extends React.Component<NotebookMetadataComponentProps> {
@ -41,24 +43,39 @@ export class NotebookMetadataComponent extends React.Component<NotebookMetadataC
return ( return (
<Stack tokens={{ childrenGap: 10 }}> <Stack tokens={{ childrenGap: 10 }}>
<Stack horizontal verticalAlign="center" tokens={{ childrenGap: 30 }}> <Stack horizontal verticalAlign="center" tokens={{ childrenGap: 30 }}>
<Text variant="xxLarge" nowrap> <Stack.Item>
{FileSystemUtil.stripExtension(this.props.data.name, "ipynb")} <Text variant="xxLarge" nowrap>
</Text> {FileSystemUtil.stripExtension(this.props.data.name, "ipynb")}
<Text> </Text>
{this.props.isFavorite !== undefined && ( </Stack.Item>
<>
<IconButton <Stack.Item>
iconProps={{ iconName: this.props.isFavorite ? "HeartFill" : "Heart" }} <Text>
onClick={this.props.isFavorite ? this.props.onUnfavoriteClick : this.props.onFavoriteClick} {this.props.isFavorite !== undefined && (
/> <>
{this.props.data.favorites} likes <IconButton
</> iconProps={{ iconName: this.props.isFavorite ? "HeartFill" : "Heart" }}
)} onClick={this.props.isFavorite ? this.props.onUnfavoriteClick : this.props.onFavoriteClick}
</Text> />
{this.props.data.favorites} likes
</>
)}
</Text>
</Stack.Item>
{this.props.downloadButtonText && ( {this.props.downloadButtonText && (
<PrimaryButton text={this.props.downloadButtonText} onClick={this.props.onDownloadClick} /> <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} />
</Stack.Item>
</Stack> </Stack>
<Stack horizontal verticalAlign="center" tokens={{ childrenGap: 10 }}> <Stack horizontal verticalAlign="center" tokens={{ childrenGap: 10 }}>

View File

@ -3,11 +3,10 @@
*/ */
import { Notebook } from "@nteract/commutable"; import { Notebook } from "@nteract/commutable";
import { createContentRef } from "@nteract/core"; import { createContentRef } from "@nteract/core";
import { Icon, Link, ProgressIndicator } from "office-ui-fabric-react"; import { IChoiceGroupProps, Icon, Link, ProgressIndicator } from "office-ui-fabric-react";
import * as React from "react"; import * as React from "react";
import { contents } from "rx-jupyter"; import { contents } from "rx-jupyter";
import * as Logger from "../../../Common/Logger"; import * as Logger from "../../../Common/Logger";
import * as ViewModels from "../../../Contracts/ViewModels";
import { IGalleryItem, JunoClient } from "../../../Juno/JunoClient"; import { IGalleryItem, JunoClient } from "../../../Juno/JunoClient";
import * as GalleryUtils from "../../../Utils/GalleryUtils"; import * as GalleryUtils from "../../../Utils/GalleryUtils";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
@ -15,12 +14,13 @@ import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationCon
import { NotebookClientV2 } from "../../Notebook/NotebookClientV2"; import { NotebookClientV2 } from "../../Notebook/NotebookClientV2";
import { NotebookComponentBootstrapper } from "../../Notebook/NotebookComponent/NotebookComponentBootstrapper"; import { NotebookComponentBootstrapper } from "../../Notebook/NotebookComponent/NotebookComponentBootstrapper";
import NotebookReadOnlyRenderer from "../../Notebook/NotebookRenderer/NotebookReadOnlyRenderer"; import NotebookReadOnlyRenderer from "../../Notebook/NotebookRenderer/NotebookReadOnlyRenderer";
import { DialogComponent, DialogProps } from "../DialogReactComponent/DialogComponent"; import { DialogComponent, DialogProps, TextFieldProps } from "../DialogReactComponent/DialogComponent";
import { NotebookMetadataComponent } from "./NotebookMetadataComponent"; import { NotebookMetadataComponent } from "./NotebookMetadataComponent";
import "./NotebookViewerComponent.less"; import "./NotebookViewerComponent.less";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import { NotebookV4 } from "@nteract/commutable/lib/v4"; import { NotebookV4 } from "@nteract/commutable/lib/v4";
import { SessionStorageUtility } from "../../../Shared/StorageUtility"; import { SessionStorageUtility } from "../../../Shared/StorageUtility";
import { DialogHost } from "../../../Utils/GalleryUtils";
export interface NotebookViewerComponentProps { export interface NotebookViewerComponentProps {
container?: Explorer; container?: Explorer;
@ -43,10 +43,8 @@ interface NotebookViewerComponentState {
showProgressBar: boolean; showProgressBar: boolean;
} }
export class NotebookViewerComponent extends React.Component< export class NotebookViewerComponent extends React.Component<NotebookViewerComponentProps, NotebookViewerComponentState>
NotebookViewerComponentProps, implements DialogHost {
NotebookViewerComponentState
> {
private clientManager: NotebookClientV2; private clientManager: NotebookClientV2;
private notebookComponentBootstrapper: NotebookComponentBootstrapper; private notebookComponentBootstrapper: NotebookComponentBootstrapper;
@ -140,6 +138,7 @@ export class NotebookViewerComponent extends React.Component<
onFavoriteClick={this.favoriteItem} onFavoriteClick={this.favoriteItem}
onUnfavoriteClick={this.unfavoriteItem} onUnfavoriteClick={this.unfavoriteItem}
onDownloadClick={this.downloadItem} onDownloadClick={this.downloadItem}
onReportAbuseClick={this.state.galleryItem.isSample ? undefined : this.reportAbuse}
/> />
</div> </div>
) : ( ) : (
@ -179,6 +178,39 @@ export class NotebookViewerComponent extends React.Component<
}; };
} }
// DialogHost
showOkCancelModalDialog(
title: string,
msg: string,
okLabel: string,
onOk: () => void,
cancelLabel: string,
onCancel: () => void,
choiceGroupProps?: IChoiceGroupProps,
textFieldProps?: TextFieldProps
): void {
this.setState({
dialogProps: {
isModal: true,
visible: true,
title,
subText: msg,
primaryButtonText: okLabel,
secondaryButtonText: cancelLabel,
onPrimaryButtonClick: () => {
this.setState({ dialogProps: undefined });
onOk && onOk();
},
onSecondaryButtonClick: () => {
this.setState({ dialogProps: undefined });
onCancel && onCancel();
},
choiceGroupProps,
textFieldProps
}
});
}
private favoriteItem = async (): Promise<void> => { private favoriteItem = async (): Promise<void> => {
GalleryUtils.favoriteItem(this.props.container, this.props.junoClient, this.state.galleryItem, item => GalleryUtils.favoriteItem(this.props.container, this.props.junoClient, this.state.galleryItem, item =>
this.setState({ galleryItem: item, isFavorite: true }) this.setState({ galleryItem: item, isFavorite: true })
@ -196,4 +228,8 @@ export class NotebookViewerComponent extends React.Component<
this.setState({ galleryItem: item }) this.setState({ galleryItem: item })
); );
}; };
private reportAbuse = (): void => {
GalleryUtils.reportAbuse(this.props.junoClient, this.state.galleryItem, this, () => {});
};
} }

View File

@ -17,26 +17,38 @@ exports[`NotebookMetadataComponent renders liked notebook 1`] = `
} }
verticalAlign="center" verticalAlign="center"
> >
<Text <StackItem>
nowrap={true} <Text
variant="xxLarge" nowrap={true}
> variant="xxLarge"
name >
</Text> name
<Text> </Text>
<CustomizedIconButton </StackItem>
iconProps={ <StackItem>
Object { <Text>
"iconName": "HeartFill", <CustomizedIconButton
iconProps={
Object {
"iconName": "HeartFill",
}
} }
} />
0
likes
</Text>
</StackItem>
<StackItem>
<CustomizedPrimaryButton
text="Download"
/> />
0 </StackItem>
likes <StackItem
</Text> grow={true}
<CustomizedPrimaryButton
text="Download"
/> />
<StackItem>
<InfoComponent />
</StackItem>
</Stack> </Stack>
<Stack <Stack
horizontal={true} horizontal={true}
@ -117,26 +129,38 @@ exports[`NotebookMetadataComponent renders un-liked notebook 1`] = `
} }
verticalAlign="center" verticalAlign="center"
> >
<Text <StackItem>
nowrap={true} <Text
variant="xxLarge" nowrap={true}
> variant="xxLarge"
name >
</Text> name
<Text> </Text>
<CustomizedIconButton </StackItem>
iconProps={ <StackItem>
Object { <Text>
"iconName": "Heart", <CustomizedIconButton
iconProps={
Object {
"iconName": "Heart",
}
} }
} />
0
likes
</Text>
</StackItem>
<StackItem>
<CustomizedPrimaryButton
text="Download"
/> />
0 </StackItem>
likes <StackItem
</Text> grow={true}
<CustomizedPrimaryButton
text="Download"
/> />
<StackItem>
<InfoComponent />
</StackItem>
</Stack> </Stack>
<Stack <Stack
horizontal={true} horizontal={true}

View File

@ -88,6 +88,7 @@ import TabsBase from "./Tabs/TabsBase";
import { CommandButtonComponentProps } from "./Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "./Controls/CommandButton/CommandButtonComponent";
import { updateUserContext, userContext } from "../UserContext"; import { updateUserContext, userContext } from "../UserContext";
import { stringToBlob } from "../Utils/BlobUtils"; import { stringToBlob } from "../Utils/BlobUtils";
import { IChoiceGroupProps } from "office-ui-fabric-react";
BindingHandlersRegisterer.registerBindingHandlers(); BindingHandlersRegisterer.registerBindingHandlers();
// Hold a reference to ComponentRegisterer to prevent transpiler to ignore import // Hold a reference to ComponentRegisterer to prevent transpiler to ignore import
@ -2385,42 +2386,16 @@ export default class Explorer {
} }
public showOkCancelModalDialog( public showOkCancelModalDialog(
title: string,
msg: string,
okLabel: string,
onOk: () => void,
cancelLabel: string,
onCancel: () => void
): void {
this._dialogProps({
isModal: true,
visible: true,
title,
subText: msg,
primaryButtonText: okLabel,
secondaryButtonText: cancelLabel,
onPrimaryButtonClick: () => {
this._closeModalDialog();
onOk && onOk();
},
onSecondaryButtonClick: () => {
this._closeModalDialog();
onCancel && onCancel();
}
});
}
public showOkCancelTextFieldModalDialog(
title: string, title: string,
msg: string, msg: string,
okLabel: string, okLabel: string,
onOk: () => void, onOk: () => void,
cancelLabel: string, cancelLabel: string,
onCancel: () => void, onCancel: () => void,
textFieldProps: TextFieldProps, choiceGroupProps?: IChoiceGroupProps,
textFieldProps?: TextFieldProps,
isPrimaryButtonDisabled?: boolean isPrimaryButtonDisabled?: boolean
): void { ): void {
let textFieldValue: string = null;
this._dialogProps({ this._dialogProps({
isModal: true, isModal: true,
visible: true, visible: true,
@ -2436,8 +2411,9 @@ export default class Explorer {
this._closeModalDialog(); this._closeModalDialog();
onCancel && onCancel(); onCancel && onCancel();
}, },
primaryButtonDisabled: isPrimaryButtonDisabled, choiceGroupProps,
textFieldProps textFieldProps,
primaryButtonDisabled: isPrimaryButtonDisabled
}); });
} }

View File

@ -166,7 +166,7 @@ export default class NotebookManager {
private promptForCommitMsg = (title: string, primaryButtonLabel: string) => { private promptForCommitMsg = (title: string, primaryButtonLabel: string) => {
return new Promise<string>((resolve, reject) => { return new Promise<string>((resolve, reject) => {
let commitMsg = "Committed from Azure Cosmos DB Notebooks"; let commitMsg = "Committed from Azure Cosmos DB Notebooks";
this.params.container.showOkCancelTextFieldModalDialog( this.params.container.showOkCancelModalDialog(
title || "Commit", title || "Commit",
undefined, undefined,
primaryButtonLabel || "Commit", primaryButtonLabel || "Commit",
@ -181,6 +181,7 @@ export default class NotebookManager {
}, },
"Cancel", "Cancel",
() => reject(new Error("Commit dialog canceled")), () => reject(new Error("Commit dialog canceled")),
undefined,
{ {
label: "Commit message", label: "Commit message",
autoAdjustHeight: true, autoAdjustHeight: true,

View File

@ -1,6 +1,5 @@
import ko from "knockout"; import ko from "knockout";
import { HttpStatusCodes } from "../Common/Constants"; import { HttpHeaders, HttpStatusCodes } from "../Common/Constants";
import * as ViewModels from "../Contracts/ViewModels";
import { IPinnedRepo, JunoClient, IGalleryItem } from "./JunoClient"; import { IPinnedRepo, JunoClient, IGalleryItem } from "./JunoClient";
import { configContext } from "../ConfigContext"; import { configContext } from "../ConfigContext";
import { getAuthorizationHeader } from "../Utils/AuthorizationUtils"; import { getAuthorizationHeader } from "../Utils/AuthorizationUtils";
@ -237,7 +236,7 @@ describe("Gallery", () => {
method: "PATCH", method: "PATCH",
headers: { headers: {
[authorizationHeader.header]: authorizationHeader.token, [authorizationHeader.header]: authorizationHeader.token,
"content-type": "application/json" [HttpHeaders.contentType]: "application/json"
} }
} }
); );
@ -260,7 +259,7 @@ describe("Gallery", () => {
method: "PATCH", method: "PATCH",
headers: { headers: {
[authorizationHeader.header]: authorizationHeader.token, [authorizationHeader.header]: authorizationHeader.token,
"content-type": "application/json" [HttpHeaders.contentType]: "application/json"
} }
} }
); );
@ -281,7 +280,7 @@ describe("Gallery", () => {
method: "PATCH", method: "PATCH",
headers: { headers: {
[authorizationHeader.header]: authorizationHeader.token, [authorizationHeader.header]: authorizationHeader.token,
"content-type": "application/json" [HttpHeaders.contentType]: "application/json"
} }
}); });
}); });
@ -299,7 +298,7 @@ describe("Gallery", () => {
expect(window.fetch).toBeCalledWith(`${configContext.JUNO_ENDPOINT}/api/notebooks/gallery/favorites`, { expect(window.fetch).toBeCalledWith(`${configContext.JUNO_ENDPOINT}/api/notebooks/gallery/favorites`, {
headers: { headers: {
[authorizationHeader.header]: authorizationHeader.token, [authorizationHeader.header]: authorizationHeader.token,
"content-type": "application/json" [HttpHeaders.contentType]: "application/json"
} }
}); });
}); });
@ -317,7 +316,7 @@ describe("Gallery", () => {
expect(window.fetch).toBeCalledWith(`${configContext.JUNO_ENDPOINT}/api/notebooks/gallery/published`, { expect(window.fetch).toBeCalledWith(`${configContext.JUNO_ENDPOINT}/api/notebooks/gallery/published`, {
headers: { headers: {
[authorizationHeader.header]: authorizationHeader.token, [authorizationHeader.header]: authorizationHeader.token,
"content-type": "application/json" [HttpHeaders.contentType]: "application/json"
} }
}); });
}); });
@ -337,7 +336,7 @@ describe("Gallery", () => {
method: "DELETE", method: "DELETE",
headers: { headers: {
[authorizationHeader.header]: authorizationHeader.token, [authorizationHeader.header]: authorizationHeader.token,
"content-type": "application/json" [HttpHeaders.contentType]: "application/json"
} }
}); });
}); });
@ -364,7 +363,7 @@ describe("Gallery", () => {
method: "PUT", method: "PUT",
headers: { headers: {
[authorizationHeader.header]: authorizationHeader.token, [authorizationHeader.header]: authorizationHeader.token,
"content-type": "application/json" [HttpHeaders.contentType]: "application/json"
}, },
body: JSON.stringify({ body: JSON.stringify({
name, name,

View File

@ -1,5 +1,5 @@
import ko from "knockout"; import ko from "knockout";
import { HttpStatusCodes } from "../Common/Constants"; import { HttpHeaders, HttpStatusCodes } from "../Common/Constants";
import { configContext } from "../ConfigContext"; import { configContext } from "../ConfigContext";
import * as DataModels from "../Contracts/DataModels"; import * as DataModels from "../Contracts/DataModels";
import { AuthorizeAccessComponent } from "../Explorer/Controls/GitHub/AuthorizeAccessComponent"; import { AuthorizeAccessComponent } from "../Explorer/Controls/GitHub/AuthorizeAccessComponent";
@ -404,6 +404,30 @@ export class JunoClient {
return `${this.getNotebooksUrl()}/gallery/${id}`; return `${this.getNotebooksUrl()}/gallery/${id}`;
} }
public async reportAbuse(notebookId: string, abuseCategory: string, notes: string): Promise<IJunoResponse<boolean>> {
const response = await window.fetch(`${this.getNotebooksUrl()}/avert/reportAbuse`, {
method: "POST",
body: JSON.stringify({
notebookId,
abuseCategory,
notes
}),
headers: {
[HttpHeaders.contentType]: "application/json"
}
});
let data: boolean;
if (response.status === HttpStatusCodes.OK) {
data = await response.json();
}
return {
status: response.status,
data
};
}
private async getNotebooks(input: RequestInfo, init?: RequestInit): Promise<IJunoResponse<IGalleryItem[]>> { private async getNotebooks(input: RequestInfo, init?: RequestInit): Promise<IJunoResponse<IGalleryItem[]>> {
const response = await window.fetch(input, init); const response = await window.fetch(input, init);
@ -430,7 +454,7 @@ export class JunoClient {
const authorizationHeader = getAuthorizationHeader(); const authorizationHeader = getAuthorizationHeader();
return { return {
[authorizationHeader.header]: authorizationHeader.token, [authorizationHeader.header]: authorizationHeader.token,
"content-type": "application/json" [HttpHeaders.contentType]: "application/json"
}; };
} }

View File

@ -8,6 +8,52 @@ import {
GalleryViewerComponent GalleryViewerComponent
} from "../Explorer/Controls/NotebookGallery/GalleryViewerComponent"; } from "../Explorer/Controls/NotebookGallery/GalleryViewerComponent";
import Explorer from "../Explorer/Explorer"; import Explorer from "../Explorer/Explorer";
import { IChoiceGroupOption, IChoiceGroupProps } from "office-ui-fabric-react";
import { TextFieldProps } from "../Explorer/Controls/DialogReactComponent/DialogComponent";
const defaultSelectedAbuseCategory = "Other";
const abuseCategories: IChoiceGroupOption[] = [
{
key: "ChildEndangermentExploitation",
text: "Child endangerment or exploitation"
},
{
key: "ContentInfringement",
text: "Content infringement"
},
{
key: "OffensiveContent",
text: "Offensive content"
},
{
key: "Terrorism",
text: "Terrorism"
},
{
key: "ThreatsCyberbullyingHarassment",
text: "Threats, cyber bullying or harassment"
},
{
key: "VirusSpywareMalware",
text: "Virus, spyware or malware"
},
{
key: "Fraud",
text: "Fraud"
},
{
key: "HateSpeech",
text: "Hate speech"
},
{
key: "ImminentHarmToPersonsOrProperty",
text: "Imminent harm to persons or property"
},
{
key: "Other",
text: "Other"
}
];
export enum NotebookViewerParams { export enum NotebookViewerParams {
NotebookUrl = "notebookUrl", NotebookUrl = "notebookUrl",
@ -33,6 +79,78 @@ export interface GalleryViewerProps {
searchText: string; searchText: string;
} }
export interface DialogHost {
showOkCancelModalDialog(
title: string,
msg: string,
okLabel: string,
onOk: () => void,
cancelLabel: string,
onCancel: () => void,
choiceGroupProps?: IChoiceGroupProps,
textFieldProps?: TextFieldProps
): void;
}
export function reportAbuse(
junoClient: JunoClient,
data: IGalleryItem,
dialogHost: DialogHost,
onComplete: (success: boolean) => void
): void {
const notebookId = data.id;
let abuseCategory = defaultSelectedAbuseCategory;
let additionalDetails: string;
dialogHost.showOkCancelModalDialog(
"Report Abuse",
undefined,
"Report Abuse",
async () => {
const clearSubmitReportNotification = NotificationConsoleUtils.logConsoleProgress(
`Submitting your report on ${data.name} violating code of conduct`
);
try {
const response = await junoClient.reportAbuse(notebookId, abuseCategory, additionalDetails);
if (!response.data) {
throw new Error(`Received HTTP ${response.status} when submitting report for ${data.name}`);
}
NotificationConsoleUtils.logConsoleInfo(
`Your report on ${data.name} has been submitted. Thank you for reporting the violation.`
);
onComplete(response.data);
} catch (error) {
const message = `Failed to submit report on ${data.name} violating code of conduct: ${error}`;
Logger.logError(message, "GalleryUtils/reportAbuse");
NotificationConsoleUtils.logConsoleInfo(message);
}
clearSubmitReportNotification();
},
"Cancel",
undefined,
{
label: "How does this content violate the code of conduct?",
options: abuseCategories,
defaultSelectedKey: defaultSelectedAbuseCategory,
onChange: (_event?: React.FormEvent<HTMLElement | HTMLInputElement>, option?: IChoiceGroupOption) => {
abuseCategory = option?.key;
}
},
{
label: "You can also include additional relevant details on the offensive content",
multiline: true,
rows: 3,
autoAdjustHeight: false,
onChange: (_event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>, newValue?: string) => {
additionalDetails = newValue;
}
}
);
}
export function downloadItem( export function downloadItem(
container: Explorer, container: Explorer,
junoClient: JunoClient, junoClient: JunoClient,