import { IChoiceGroupOption, IChoiceGroupProps, IProgressIndicatorProps } from "@fluentui/react"; import { Notebook } from "@nteract/commutable"; import { NotebookV4 } from "@nteract/commutable/lib/v4"; import { HttpStatusCodes } from "../Common/Constants"; import { getErrorMessage, getErrorStack, handleError } from "../Common/ErrorHandlingUtils"; import { TextFieldProps, useDialog } from "../Explorer/Controls/Dialog"; import { GalleryTab, GalleryViewerComponent, SortBy, } from "../Explorer/Controls/NotebookGallery/GalleryViewerComponent"; import Explorer from "../Explorer/Explorer"; import { useNotebook } from "../Explorer/Notebook/useNotebook"; import { IGalleryItem, JunoClient } from "../Juno/JunoClient"; import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants"; import { trace, traceFailure, traceStart, traceSuccess } from "../Shared/Telemetry/TelemetryProcessor"; import { logConsoleInfo, logConsoleProgress } from "./NotificationConsoleUtils"; const defaultSelectedAbuseCategory = "Other"; const abuseCategories: IChoiceGroupOption[] = [ { key: "ChildEndangermentExploitation", text: "Child endangerment or exploitation", }, { key: "ContentInfringement", text: "Content infringement", }, { key: "OffensiveContent", text: "Offensive content", }, { key: "Terrorism", text: "Terrorism", }, { key: "ThreatsCyberbullyingHarassment", text: "Threats, cyber bullying or harassment", }, { key: "VirusSpywareMalware", text: "Virus, spyware or malware", }, { key: "Fraud", text: "Fraud", }, { key: "HateSpeech", text: "Hate speech", }, { key: "ImminentHarmToPersonsOrProperty", text: "Imminent harm to persons or property", }, { key: "Other", text: "Other", }, ]; export enum NotebookViewerParams { NotebookUrl = "notebookUrl", GalleryItemId = "galleryItemId", HideInputs = "hideInputs", } export interface NotebookViewerProps { notebookUrl: string; galleryItemId: string; hideInputs: boolean; } export enum GalleryViewerParams { SelectedTab = "tab", SortBy = "sort", SearchText = "q", } export interface GalleryViewerProps { selectedTab: GalleryTab; sortBy: SortBy; searchText: string; } export interface DialogHost { showOkModalDialog( title: string, msg: string, okLabel: string, onOk: () => void, progressIndicatorProps?: IProgressIndicatorProps ): void; showOkCancelModalDialog( title: string, msg: string, okLabel: string, onOk: () => void, cancelLabel: string, onCancel: () => void, progressIndicatorProps?: IProgressIndicatorProps, choiceGroupProps?: IChoiceGroupProps, textFieldProps?: TextFieldProps, primaryButtonDisabled?: boolean ): void; } export function reportAbuse( junoClient: JunoClient, data: IGalleryItem, dialogHost: DialogHost, onComplete: (success: boolean) => void ): void { trace(Action.NotebooksGalleryClickReportAbuse, ActionModifiers.Mark, { notebookId: data.id }); const notebookId = data.id; let abuseCategory = defaultSelectedAbuseCategory; let additionalDetails: string; dialogHost.showOkCancelModalDialog( "Report Abuse", undefined, "Report Abuse", async () => { dialogHost.showOkCancelModalDialog( "Report Abuse", `Submitting your report on ${data.name} violating code of conduct`, "Reporting...", undefined, "Cancel", undefined, {}, undefined, undefined, true ); const startKey = traceStart(Action.NotebooksGalleryReportAbuse, { notebookId: data.id, abuseCategory }); try { const response = await junoClient.reportAbuse(notebookId, abuseCategory, additionalDetails); if (response.status !== HttpStatusCodes.Accepted) { throw new Error(`Received HTTP ${response.status} when submitting report for ${data.name}`); } dialogHost.showOkModalDialog( "Report Abuse", `Your report on ${data.name} has been submitted. Thank you for reporting the violation.`, "OK", undefined, { percentComplete: 1, } ); traceSuccess(Action.NotebooksGalleryReportAbuse, { notebookId: data.id, abuseCategory }, startKey); onComplete(response.data); } catch (error) { traceFailure( Action.NotebooksGalleryReportAbuse, { notebookId: data.id, abuseCategory, error: getErrorMessage(error), errorStack: getErrorStack(error), }, startKey ); handleError( error, "GalleryUtils/reportAbuse", `Failed to submit report on ${data.name} violating code of conduct` ); dialogHost.showOkModalDialog( "Report Abuse", `Failed to submit report on ${data.name} violating code of conduct`, "OK", undefined, { percentComplete: 1, } ); } }, "Cancel", undefined, undefined, { label: "How does this content violate the code of conduct?", options: abuseCategories, defaultSelectedKey: defaultSelectedAbuseCategory, onChange: (_event?: React.FormEvent, 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, newValue?: string) => { additionalDetails = newValue; }, } ); } export function downloadItem( container: Explorer, junoClient: JunoClient, data: IGalleryItem, onComplete: (item: IGalleryItem) => void ): void { trace(Action.NotebooksGalleryClickDownload, ActionModifiers.Mark, { notebookId: data.id, downloadCount: data.downloads, isSample: data.isSample, }); const name = data.name; useDialog.getState().showOkCancelModalDialog( `Download to ${useNotebook.getState().NotebookFolderName}`, `Download ${name} from gallery as a copy to your notebooks to run and/or edit the notebook.`, "Download", async () => { const clearInProgressMessage = logConsoleProgress( `Downloading ${name} to ${useNotebook.getState().NotebookFolderName}` ); const startKey = traceStart(Action.NotebooksGalleryDownload, { notebookId: data.id, downloadCount: data.downloads, isSample: data.isSample, }); try { const response = await junoClient.getNotebookContent(data.id); if (!response.data) { throw new Error(`Received HTTP ${response.status} when fetching ${data.name}`); } const notebook = JSON.parse(response.data) as Notebook; removeNotebookViewerLink(notebook, data.newCellId); if (!data.isSample) { const metadata = notebook.metadata as { [name: string]: unknown }; metadata.untrusted = true; } await container.importAndOpenContent(data.name, JSON.stringify(notebook)); logConsoleInfo(`Successfully downloaded ${name} to My Notebooks`); const increaseDownloadResponse = await junoClient.increaseNotebookDownloadCount(data.id); if (increaseDownloadResponse.data) { traceSuccess( Action.NotebooksGalleryDownload, { notebookId: data.id, downloadCount: increaseDownloadResponse.data.downloads, isSample: data.isSample }, startKey ); onComplete(increaseDownloadResponse.data); } } catch (error) { traceFailure( Action.NotebooksGalleryDownload, { notebookId: data.id, downloadCount: data.downloads, isSample: data.isSample, error: getErrorMessage(error), errorStack: getErrorStack(error), }, startKey ); handleError(error, "GalleryUtils/downloadItem", `Failed to download ${data.name}`); } clearInProgressMessage(); }, "Cancel", undefined ); } export const removeNotebookViewerLink = (notebook: Notebook, newCellId: string): void => { if (!newCellId) { return; } const notebookV4 = notebook as NotebookV4; if (notebookV4?.cells[0]?.source[0]?.search(newCellId)) { notebookV4.cells.splice(0, 1); notebook = notebookV4; } }; export async function favoriteItem( container: Explorer, junoClient: JunoClient, data: IGalleryItem, onComplete: (item: IGalleryItem) => void ): Promise { if (container) { const startKey = traceStart(Action.NotebooksGalleryFavorite, { notebookId: data.id, isSample: data.isSample, favoriteCount: data.favorites, }); try { const response = await junoClient.favoriteNotebook(data.id); if (!response.data) { throw new Error(`Received HTTP ${response.status} when favoriting ${data.name}`); } traceSuccess( Action.NotebooksGalleryFavorite, { notebookId: data.id, isSample: data.isSample, favoriteCount: response.data.favorites }, startKey ); onComplete(response.data); } catch (error) { traceFailure( Action.NotebooksGalleryFavorite, { notebookId: data.id, isSample: data.isSample, favoriteCount: data.favorites, error: getErrorMessage(error), errorStack: getErrorStack(error), }, startKey ); handleError(error, "GalleryUtils/favoriteItem", `Failed to favorite ${data.name}`); } } } export async function unfavoriteItem( container: Explorer, junoClient: JunoClient, data: IGalleryItem, onComplete: (item: IGalleryItem) => void ): Promise { if (container) { const startKey = traceStart(Action.NotebooksGalleryUnfavorite, { notebookId: data.id, isSample: data.isSample, favoriteCount: data.favorites, }); try { const response = await junoClient.unfavoriteNotebook(data.id); if (!response.data) { throw new Error(`Received HTTP ${response.status} when unfavoriting ${data.name}`); } traceSuccess( Action.NotebooksGalleryUnfavorite, { notebookId: data.id, isSample: data.isSample, favoriteCount: response.data.favorites }, startKey ); onComplete(response.data); } catch (error) { traceFailure( Action.NotebooksGalleryUnfavorite, { notebookId: data.id, isSample: data.isSample, favoriteCount: data.favorites, error: getErrorMessage(error), errorStack: getErrorStack(error), }, startKey ); handleError(error, "GalleryUtils/unfavoriteItem", `Failed to unfavorite ${data.name}`); } } } export function deleteItem( container: Explorer, junoClient: JunoClient, data: IGalleryItem, onComplete: (item: IGalleryItem) => void, beforeDelete?: () => void, afterDelete?: () => void ): void { if (container) { trace(Action.NotebooksGalleryClickDelete, ActionModifiers.Mark, { notebookId: data.id }); useDialog.getState().showOkCancelModalDialog( "Remove published notebook", `Would you like to remove ${data.name} from the gallery?`, "Remove", async () => { if (beforeDelete) { beforeDelete(); } const name = data.name; const clearInProgressMessage = logConsoleProgress(`Removing ${name} from gallery`); const startKey = traceStart(Action.NotebooksGalleryDelete, { notebookId: data.id }); try { const response = await junoClient.deleteNotebook(data.id); if (!response.data) { throw new Error(`Received HTTP ${response.status} while removing ${name}`); } traceSuccess(Action.NotebooksGalleryDelete, { notebookId: data.id }, startKey); logConsoleInfo(`Successfully removed ${name} from gallery`); onComplete(response.data); } catch (error) { traceFailure( Action.NotebooksGalleryDelete, { notebookId: data.id, error: getErrorMessage(error), errorStack: getErrorStack(error) }, startKey ); handleError(error, "GalleryUtils/deleteItem", `Failed to remove ${name} from gallery`); } finally { if (afterDelete) { afterDelete(); } } clearInProgressMessage(); }, "Cancel", undefined ); } } export function getGalleryViewerProps(search: string): GalleryViewerProps { const params = new URLSearchParams(search); let selectedTab: GalleryTab; if (params.has(GalleryViewerParams.SelectedTab)) { selectedTab = GalleryTab[params.get(GalleryViewerParams.SelectedTab) as keyof typeof GalleryTab]; } let sortBy: SortBy; if (params.has(GalleryViewerParams.SortBy)) { sortBy = SortBy[params.get(GalleryViewerParams.SortBy) as keyof typeof SortBy]; } return { selectedTab, sortBy, searchText: params.get(GalleryViewerParams.SearchText), }; } export function getNotebookViewerProps(search: string): NotebookViewerProps { const params = new URLSearchParams(search); return { notebookUrl: params.get(NotebookViewerParams.NotebookUrl), galleryItemId: params.get(NotebookViewerParams.GalleryItemId), hideInputs: JSON.parse(params.get(NotebookViewerParams.HideInputs)), }; } export function getTabTitle(tab: GalleryTab): string { switch (tab) { case GalleryTab.PublicGallery: return GalleryViewerComponent.PublicGalleryTitle; case GalleryTab.OfficialSamples: return GalleryViewerComponent.OfficialSamplesTitle; case GalleryTab.Favorites: return GalleryViewerComponent.FavoritesTitle; case GalleryTab.Published: return GalleryViewerComponent.PublishedTitle; default: throw new Error(`Unknown tab ${tab}`); } } export function filterPublishedNotebooks( items: IGalleryItem[] ): { published: IGalleryItem[]; underReview: IGalleryItem[]; removed: IGalleryItem[]; } { const underReview: IGalleryItem[] = []; const removed: IGalleryItem[] = []; const published: IGalleryItem[] = []; items?.forEach((item) => { if (item.policyViolations?.length > 0) { removed.push(item); } else if (item.pendingScanJobIds?.length > 0) { underReview.push(item); } else { published.push(item); } }); return { published, underReview, removed }; }