Support async notebook publishing (#275)

Handle cases for async notebook publishing. Now `Your published work` tab shows 3 sections - published, under review, and removed notebooks.

Note: The text labels are design placeholders

![image](https://user-images.githubusercontent.com/693092/95799994-3b5fb100-0cab-11eb-86fc-4ded0aeeddb1.png)
This commit is contained in:
Tanuj Mittal 2020-10-14 20:49:18 -07:00 committed by GitHub
parent 821f665e78
commit bd00e5eb9b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 149 additions and 39 deletions

View File

@ -18,7 +18,9 @@ describe("GalleryCardComponent", () => {
downloads: 0,
favorites: 0,
views: 0,
newCellId: undefined
newCellId: undefined,
policyViolations: undefined,
pendingScanJobIds: undefined
},
isFavorite: false,
showDownload: true,

View File

@ -1,6 +1,7 @@
import {
Dropdown,
FocusZone,
FontWeights,
IDropdownOption,
IPageSpecification,
IPivotItemProps,
@ -11,7 +12,8 @@ import {
Pivot,
PivotItem,
SearchBox,
Stack
Stack,
Text
} from "office-ui-fabric-react";
import * as React from "react";
import * as Logger from "../../../Common/Logger";
@ -151,7 +153,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
// explicitly checking if isCodeOfConductAccepted is not false, as it is initially undefined.
// Displaying code of conduct component on gallery load should not be the default behavior.
if (this.state.isCodeOfConductAccepted !== false) {
tabs.push(this.createTab(GalleryTab.Published, this.state.publishedNotebooks));
tabs.push(this.createPublishedNotebooksTab(GalleryTab.Published, this.state.publishedNotebooks));
}
}
@ -197,10 +199,59 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
private createTab(tab: GalleryTab, data: IGalleryItem[]): GalleryTabInfo {
return {
tab,
content: this.createTabContent(data)
content: this.createSearchBarHeader(this.createCardsTabContent(data))
};
}
private createPublishedNotebooksTab = (tab: GalleryTab, data: IGalleryItem[]): GalleryTabInfo => {
return {
tab,
content: this.createPublishedNotebooksTabContent(data)
};
};
private createPublishedNotebooksTabContent = (data: IGalleryItem[]): JSX.Element => {
const { published, underReview, removed } = GalleryUtils.filterPublishedNotebooks(data);
const content = (
<Stack tokens={{ childrenGap: 10 }}>
{published?.length > 0 &&
this.createPublishedNotebooksSectionContent(
undefined,
"You have successfully published the following notebook(s) to public gallery and shared with other Azure Cosmos DB users.",
this.createCardsTabContent(published)
)}
{underReview?.length > 0 &&
this.createPublishedNotebooksSectionContent(
"Under Review",
"Content of a notebook you published is currently being scanned for illegal content. It will not be available to public gallery until the review is completed (may take a few days)",
this.createCardsTabContent(underReview)
)}
{removed?.length > 0 &&
this.createPublishedNotebooksSectionContent(
"Removed",
"These notebooks were found to contain illegal content and has been taken down.",
this.createPolicyViolationsListContent(removed)
)}
</Stack>
);
return this.createSearchBarHeader(content);
};
private createPublishedNotebooksSectionContent = (
title: string,
description: string,
content: JSX.Element
): JSX.Element => {
return (
<Stack tokens={{ childrenGap: 5 }}>
{title && <Text styles={{ root: { fontWeight: FontWeights.semibold } }}>{title}</Text>}
{description && <Text>{description}</Text>}
{content}
</Stack>
);
};
private createPublicGalleryTabContent(data: IGalleryItem[], acceptedCodeOfConduct: boolean): JSX.Element {
return acceptedCodeOfConduct === false ? (
<CodeOfConductComponent
@ -210,11 +261,11 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
}}
/>
) : (
this.createTabContent(data)
this.createSearchBarHeader(this.createCardsTabContent(data))
);
}
private createTabContent(data: IGalleryItem[]): JSX.Element {
private createSearchBarHeader(content: JSX.Element): JSX.Element {
return (
<Stack tokens={{ childrenGap: 10 }}>
<Stack horizontal tokens={{ childrenGap: 20, padding: 10 }}>
@ -233,7 +284,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
</Stack.Item>
)}
</Stack>
{data && this.createCardsTabContent(data)}
<Stack.Item>{content}</Stack.Item>
</Stack>
);
}
@ -251,6 +302,25 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
);
}
private createPolicyViolationsListContent(data: IGalleryItem[]): JSX.Element {
return (
<table>
<tbody>
<tr>
<th>Name</th>
<th>Policy violations</th>
</tr>
{data.map(item => (
<tr key={`policy-violations-tr-${item.id}`}>
<td>{item.name}</td>
<td>{item.policyViolations.join(", ")}</td>
</tr>
))}
</tbody>
</table>
);
}
private loadTabContent(tab: GalleryTab, searchText: string, sortBy: SortBy, offline: boolean): void {
switch (tab) {
case GalleryTab.OfficialSamples:

View File

@ -29,7 +29,7 @@ export class InfoComponent extends React.Component<InfoComponentProps> {
<Stack.Item>
{this.getInfoPanel("KnowledgeArticle", "Microsoft Terms of Use", CodeOfConductEndpoints.termsOfUse)}
</Stack.Item>
{this.props.onReportAbuseClick !== undefined && (
{this.props.onReportAbuseClick && (
<Stack.Item>
{this.getInfoPanel("ReportHacked", "Report Abuse", undefined, () => this.props.onReportAbuseClick())}
</Stack.Item>

View File

@ -81,6 +81,21 @@ exports[`GalleryViewerComponent renders 1`] = `
<InfoComponent />
</StackItem>
</Stack>
<StackItem>
<FocusZone
direction={2}
isCircularNavigation={false}
shouldRaiseClicks={true}
>
<List
getPageSpecification={[Function]}
onRenderCell={[Function]}
renderedWindowsAhead={3}
renderedWindowsBehind={2}
startIndex={0}
/>
</FocusZone>
</StackItem>
</Stack>
</PivotItem>
</StyledPivotBase>

View File

@ -18,7 +18,9 @@ describe("NotebookMetadataComponent", () => {
downloads: 0,
favorites: 0,
views: 0,
newCellId: undefined
newCellId: undefined,
policyViolations: undefined,
pendingScanJobIds: undefined
},
isFavorite: false,
downloadButtonText: "Download",
@ -48,7 +50,9 @@ describe("NotebookMetadataComponent", () => {
downloads: 0,
favorites: 0,
views: 0,
newCellId: undefined
newCellId: undefined,
policyViolations: undefined,
pendingScanJobIds: undefined
},
isFavorite: true,
downloadButtonText: "Download",

View File

@ -140,10 +140,7 @@ export class PublishNotebookPaneAdapter implements ReactAdapter {
}
public async submit(): Promise<void> {
const notificationId = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`Publishing ${this.name} to gallery`
);
const clearPublishingMessage = NotificationConsoleUtils.logConsoleProgress(`Publishing ${this.name} to gallery`);
this.isExecuting = true;
this.triggerRender();
@ -161,8 +158,16 @@ export class PublishNotebookPaneAdapter implements ReactAdapter {
this.content,
this.isLinkInjectionEnabled
);
if (response.data) {
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, `Published ${name} to gallery`);
const data = response.data;
if (data) {
if (data.pendingScanJobIds?.length > 0) {
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`);
}
}
} catch (error) {
this.formError = `Failed to publish ${this.name} to gallery`;
@ -170,10 +175,10 @@ export class PublishNotebookPaneAdapter implements ReactAdapter {
const message = `${this.formError}: ${this.formErrorDetail}`;
Logger.logError(message, "PublishNotebookPaneAdapter/submit");
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message);
NotificationConsoleUtils.logConsoleError(message);
return;
} finally {
NotificationConsoleUtils.clearInProgressMessageWithId(notificationId);
clearPublishingMessage();
this.isExecuting = false;
this.triggerRender();
}

View File

@ -296,7 +296,9 @@ export class PublishNotebookPaneComponent extends React.Component<PublishNoteboo
downloads: 0,
favorites: 0,
views: 0,
newCellId: undefined
newCellId: undefined,
policyViolations: undefined,
pendingScanJobIds: undefined
}}
isFavorite={false}
showDownload={true}

View File

@ -103,6 +103,8 @@ exports[`PublishNotebookPaneComponent renders 1`] = `
"isSample": false,
"name": "SampleNotebook.ipynb",
"newCellId": undefined,
"pendingScanJobIds": undefined,
"policyViolations": undefined,
"tags": Array [
"",
],

View File

@ -1,6 +1,6 @@
import ko from "knockout";
import { HttpHeaders, HttpStatusCodes } from "../Common/Constants";
import { IPinnedRepo, JunoClient, IGalleryItem } from "./JunoClient";
import { IPinnedRepo, JunoClient } from "./JunoClient";
import { configContext } from "../ConfigContext";
import { getAuthorizationHeader } from "../Utils/AuthorizationUtils";
import { DatabaseAccount } from "../Contracts/DataModels";
@ -33,24 +33,6 @@ const samplePinnedRepos: IPinnedRepo[] = [
}
];
const sampleGalleryItems: IGalleryItem[] = [
{
id: "id",
name: "name",
description: "description",
gitSha: "gitSha",
tags: ["tag1"],
author: "author",
thumbnailUrl: "thumbnailUrl",
created: "created",
isSample: false,
downloads: 0,
favorites: 0,
views: 0,
newCellId: undefined
}
];
describe("Pinned repos", () => {
const junoClient = new JunoClient(ko.observable<DatabaseAccount>(sampleDatabaseAccount));

View File

@ -37,6 +37,8 @@ export interface IGalleryItem {
favorites: number;
views: number;
newCellId: string;
policyViolations: string[];
pendingScanJobIds: string[];
}
export interface IPublicGalleryData {

View File

@ -17,7 +17,9 @@ const galleryItem: IGalleryItem = {
downloads: 0,
favorites: 0,
views: 0,
newCellId: undefined
newCellId: undefined,
policyViolations: undefined,
pendingScanJobIds: undefined
};
describe("GalleryUtils", () => {

View File

@ -323,3 +323,27 @@ export function getTabTitle(tab: GalleryTab): string {
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 };
}